Skip to content

Commit

Permalink
Migrate custom v3 config to local plugins if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
LabhanshAgrawal committed Jan 7, 2023
1 parent f494781 commit 2981853
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 77 deletions.
59 changes: 4 additions & 55 deletions app/config/import.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,13 @@
import {copySync, existsSync, writeFileSync, readFileSync, copy} from 'fs-extra';
import {readFileSync} from 'fs-extra';
import {sync as mkdirpSync} from 'mkdirp';
import {
defaultCfg,
cfgPath,
legacyCfgPath,
plugs,
defaultPlatformKeyPath,
schemaPath,
cfgDir,
schemaFile
} from './paths';
import {_init, _extractDefault} from './init';
import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths';
import {_init} from './init';
import notify from '../notify';
import {rawConfig} from '../../lib/config';
import _ from 'lodash';
import {resolve} from 'path';
import {migrateHyper3Config} from './migrate';

let defaultConfig: rawConfig;

const _write = (path: string, data: string) => {
// This method will take text formatted as Unix line endings and transform it
// to text formatted with DOS line endings. We do this because the default
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
const crlfify = (str: string) => {
return str.replace(/\r?\n/g, '\r\n');
};
const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
writeFileSync(path, format, 'utf8');
};

// Migrate Hyper3 config to Hyper4 but only if the user hasn't manually
// touched the new config and if the old config is not a symlink
const migrateHyper3Config = () => {
copy(schemaPath, resolve(cfgDir, schemaFile), (err) => {
if (err) {
console.error(err);
}
});

if (existsSync(cfgPath)) {
return;
}

if (!existsSync(legacyCfgPath)) {
copySync(defaultCfg, cfgPath);
return;
}

// Migrate
const defaultCfgData = JSON.parse(readFileSync(defaultCfg, 'utf8'));
const legacyCfgData = _extractDefault(readFileSync(legacyCfgPath, 'utf8'));
const newCfgData = _.merge(defaultCfgData, legacyCfgData);
_write(cfgPath, JSON.stringify(newCfgData, null, 2));

notify(
'Hyper 4',
`Settings location and format has changed.\nWe've automatically migrated your existing config to ${cfgPath}`
);
};

const _importConf = () => {
// init plugin directories if not present
mkdirpSync(plugs.base);
Expand Down
2 changes: 1 addition & 1 deletion app/config/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => {
return {
config: (() => {
if (userCfg?.config) {
return _.merge(defaultCfg.config, userCfg.config);
return _.merge({}, defaultCfg.config, userCfg.config);
} else {
notify('Error reading configuration: `config` key is missing');
return defaultCfg.config || ({} as configOptions);
Expand Down
187 changes: 187 additions & 0 deletions app/config/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {parse, prettyPrint} from 'recast';
import {builders, namedTypes} from 'ast-types';
import * as babelParser from 'recast/parsers/babel';
import {copy, copySync, existsSync, readFileSync, writeFileSync} from 'fs-extra';
import {dirname, resolve} from 'path';
import _ from 'lodash';

import notify from '../notify';
import {_extractDefault} from './init';
import {cfgDir, cfgPath, defaultCfg, legacyCfgPath, plugs, schemaFile, schemaPath} from './paths';

// function to remove all json serializable entries from an array expression
function removeElements(node: namedTypes.ArrayExpression): namedTypes.ArrayExpression {
const newElements = node.elements.filter((element) => {
if (namedTypes.ObjectExpression.check(element)) {
const newElement = removeProperties(element);
if (newElement.properties.length === 0) {
return false;
}
} else if (namedTypes.ArrayExpression.check(element)) {
const newElement = removeElements(element);
if (newElement.elements.length === 0) {
return false;
}
} else if (namedTypes.Literal.check(element)) {
return false;
}
return true;
});
return {...node, elements: newElements};
}

// function to remove all json serializable properties from an object expression
function removeProperties(node: namedTypes.ObjectExpression): namedTypes.ObjectExpression {
const newProperties = node.properties.filter((property) => {
if (
namedTypes.ObjectProperty.check(property) &&
(namedTypes.Literal.check(property.key) || namedTypes.Identifier.check(property.key)) &&
!property.computed
) {
if (namedTypes.ObjectExpression.check(property.value)) {
const newValue = removeProperties(property.value);
if (newValue.properties.length === 0) {
return false;
}
} else if (namedTypes.ArrayExpression.check(property.value)) {
const newValue = removeElements(property.value);
if (newValue.elements.length === 0) {
return false;
}
} else if (namedTypes.Literal.check(property.value)) {
return false;
}
}
return true;
});
return {...node, properties: newProperties};
}

export function configToPlugin(code: string): string {
const ast: namedTypes.File = parse(code, {
parser: babelParser
});
const statements = ast.program.body;
let moduleExportsNode: namedTypes.AssignmentExpression | null = null;
let configNode: any = null;

for (const statement of statements) {
if (namedTypes.ExpressionStatement.check(statement)) {
const expression = statement.expression;
if (
namedTypes.AssignmentExpression.check(expression) &&
expression.operator === '=' &&
namedTypes.MemberExpression.check(expression.left) &&
namedTypes.Identifier.check(expression.left.object) &&
expression.left.object.name === 'module' &&
namedTypes.Identifier.check(expression.left.property) &&
expression.left.property.name === 'exports'
) {
moduleExportsNode = expression;
if (namedTypes.ObjectExpression.check(expression.right)) {
const properties = expression.right.properties;
for (const property of properties) {
if (
namedTypes.ObjectProperty.check(property) &&
namedTypes.Identifier.check(property.key) &&
property.key.name === 'config'
) {
configNode = property.value;
if (namedTypes.ObjectExpression.check(property.value)) {
configNode = removeProperties(property.value);
}
}
}
} else {
configNode = builders.memberExpression(moduleExportsNode.right, builders.identifier('config'));
}
}
}
}

if (!moduleExportsNode) {
console.log('No module.exports found in config');
return '';
}
if (!configNode) {
console.log('No config field found in module.exports');
return '';
}
if (namedTypes.ObjectExpression.check(configNode) && configNode.properties.length === 0) {
return '';
}

moduleExportsNode.right = builders.objectExpression([
builders.property(
'init',
builders.identifier('decorateConfig'),
builders.arrowFunctionExpression(
[builders.identifier('_config')],
builders.callExpression(
builders.memberExpression(builders.identifier('Object'), builders.identifier('assign')),
[builders.objectExpression([]), builders.identifier('_config'), configNode]
)
)
)
]);

return prettyPrint(ast, {tabWidth: 2}).code;
}

export const _write = (path: string, data: string) => {
// This method will take text formatted as Unix line endings and transform it
// to text formatted with DOS line endings. We do this because the default
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
const crlfify = (str: string) => {
return str.replace(/\r?\n/g, '\r\n');
};
const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
writeFileSync(path, format, 'utf8');
};

// Migrate Hyper3 config to Hyper4 but only if the user hasn't manually
// touched the new config and if the old config is not a symlink
export const migrateHyper3Config = () => {
copy(schemaPath, resolve(cfgDir, schemaFile), (err) => {
if (err) {
console.error(err);
}
});

if (existsSync(cfgPath)) {
return;
}

if (!existsSync(legacyCfgPath)) {
copySync(defaultCfg, cfgPath);
return;
}

// Migrate
copySync(resolve(dirname(legacyCfgPath), '.hyper_plugins', 'local'), plugs.local);

const defaultCfgData = JSON.parse(readFileSync(defaultCfg, 'utf8'));
let newCfgData;
try {
const legacyCfgRaw = readFileSync(legacyCfgPath, 'utf8');
const legacyCfgData = _extractDefault(legacyCfgRaw);
newCfgData = _.merge({}, defaultCfgData, legacyCfgData);

const pluginCode = configToPlugin(legacyCfgRaw);
if (pluginCode) {
const pluginPath = resolve(plugs.local, 'migrated-hyper3-config.js');
newCfgData.localPlugins = ['migrated-hyper3-config', ...(newCfgData.localPlugins || [])];
_write(pluginPath, pluginCode);
}
} catch (e) {
console.error(e);
notify(
'Hyper 4',
`Failed to migrate your config from Hyper 3.\nDefault config will be created instead at ${cfgPath}`
);
newCfgData = defaultCfgData;
}
_write(cfgPath, JSON.stringify(newCfgData, null, 2));

notify('Hyper 4', `Settings location and format has changed to ${cfgPath}`);
};
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"repository": "zeit/hyper",
"dependencies": {
"@babel/parser": "7.20.7",
"@electron/remote": "2.0.9",
"async-retry": "1.3.3",
"chokidar": "^3.5.3",
Expand All @@ -31,6 +32,7 @@
"queue": "6.0.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"recast": "0.22.0",
"semver": "7.3.8",
"shell-env": "3.0.1",
"sudo-prompt": "^9.2.1",
Expand Down
7 changes: 5 additions & 2 deletions app/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,12 @@ function requirePlugins(): any[] {
}
};

return plugins_
return [
...localPlugins.filter((p) => basename(p) === 'migrated-hyper3-config'),
...plugins_,
...localPlugins.filter((p) => basename(p) !== 'migrated-hyper3-config')
]
.map(load)
.concat(localPlugins.map(load))
.filter((v) => Boolean(v));
}

Expand Down
Loading

0 comments on commit 2981853

Please sign in to comment.