Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate custom v3 config to local plugins if needed #6967

Merged
merged 1 commit into from
Jan 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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