UNPKG

@yolkai/nx-tao

Version:

CLI for generating code and running commands

432 lines (431 loc) 18.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@angular-devkit/core"); const core = require("@angular-devkit/core/node"); const node_1 = require("@angular-devkit/core/node"); const workflow_1 = require("@angular-devkit/schematics/src/workflow"); const tools_1 = require("@angular-devkit/schematics/tools"); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path = require("path"); const semver_1 = require("semver"); const stripJsonComments = require("strip-json-comments"); const tmp_1 = require("tmp"); const logger_1 = require("../shared/logger"); const params_1 = require("../shared/params"); const minimist = require("minimist"); class Migrator { constructor(opts) { this.versions = opts.versions; this.fetch = opts.fetch; this.from = opts.from; this.to = opts.to; } updatePackageJson(targetPackage, targetVersion) { return __awaiter(this, void 0, void 0, function* () { const packageJson = yield this._updatePackageJson(targetPackage, { version: targetVersion, alwaysAddToPackageJson: false }, {}); const migrations = yield this._createMigrateJson(packageJson); return { packageJson, migrations }; }); } _createMigrateJson(versions) { return __awaiter(this, void 0, void 0, function* () { const migrations = yield Promise.all(Object.keys(versions).map((c) => __awaiter(this, void 0, void 0, function* () { const currentVersion = this.versions(c); if (currentVersion === null) return []; const target = versions[c]; const migrationsJson = yield this.fetch(c, target.version); if (!migrationsJson.schematics) return []; return Object.keys(migrationsJson.schematics) .filter(r => this.gt(migrationsJson.schematics[r].version, currentVersion) & this.lte(migrationsJson.schematics[r].version, target.version)) .map(r => (Object.assign({}, migrationsJson.schematics[r], { package: c, name: r }))); }))); return migrations.reduce((m, c) => [...m, ...c], []); }); } _updatePackageJson(targetPackage, target, collectedVersions) { return __awaiter(this, void 0, void 0, function* () { let targetVersion = target.version; if (this.to[targetPackage]) { targetVersion = this.to[targetPackage]; } if (!this.versions(targetPackage)) { return { [targetPackage]: { version: target.version, alwaysAddToPackageJson: !!target.alwaysAddToPackageJson } }; } let migrationsJson; try { migrationsJson = yield this.fetch(targetPackage, targetVersion); targetVersion = migrationsJson.version; } catch (e) { if (e.message.indexOf('No matching version') > -1) { throw new Error(`${e.message}\nRun migrate with --to="package1@version1,package2@version2"`); } else { throw e; } } const packages = this.collapsePackages(targetPackage, targetVersion, migrationsJson); const childCalls = yield Promise.all(Object.keys(packages) .filter(r => { return (!collectedVersions[r] || this.gt(packages[r].version, collectedVersions[r].version)); }) .map(u => this._updatePackageJson(u, packages[u], Object.assign({}, collectedVersions, { [targetPackage]: target })))); return childCalls.reduce((m, c) => { Object.keys(c).forEach(r => { if (!m[r] || this.gt(c[r].version, m[r].version)) { m[r] = c[r]; } }); return m; }, { [targetPackage]: { version: migrationsJson.version, alwaysAddToPackageJson: target.alwaysAddToPackageJson || false } }); }); } collapsePackages(packageName, targetVersion, m) { // this should be used to know what version to include // we should use from everywhere we use versions if (packageName === '@yolkai/nx-workspace') { if (!m.packageJsonUpdates) m.packageJsonUpdates = {}; m.packageJsonUpdates[targetVersion + '-defaultPackages'] = { version: targetVersion, packages: [ '@yolkai/nx-angular', '@yolkai/nx-cypress', '@yolkai/eslint-plugin-nx', '@yolkai/nx-express', '@yolkai/nx-jest', '@yolkai/nx-linter', '@yolkai/nx-nest', '@yolkai/nx-next', '@yolkai/nx-node', '@yolkai/nx-react', '@yolkai/nx-storybook', '@yolkai/nx-tao', '@yolkai/nx-web' ].reduce((m, c) => (Object.assign({}, m, { [c]: { version: targetVersion, alwaysAddToPackageJson: false } })), {}) }; } if (!m.packageJsonUpdates || !this.versions(packageName)) return {}; return Object.keys(m.packageJsonUpdates) .filter(r => { return (this.gt(m.packageJsonUpdates[r].version, this.versions(packageName)) && this.lte(m.packageJsonUpdates[r].version, targetVersion)); }) .map(r => m.packageJsonUpdates[r].packages) .map(packages => { if (!packages) return {}; return Object.keys(packages) .filter(p => !packages[p].ifPackageInstalled || this.versions(packages[p].ifPackageInstalled)) .reduce((m, c) => (Object.assign({}, m, { [c]: { version: packages[c].version, alwaysAddToPackageJson: packages[c].alwaysAddToPackageJson } })), {}); }) .reduce((m, c) => (Object.assign({}, m, c)), {}); } gt(v1, v2) { return semver_1.gt(this.normalizeVersion(v1), this.normalizeVersion(v2)); } lte(v1, v2) { return semver_1.lte(this.normalizeVersion(v1), this.normalizeVersion(v2)); } normalizeVersion(v) { if (v.startsWith('8-')) return '8.0.0-beta.1'; if (v.startsWith('9-')) return '9.0.0-beta.1'; if (v.startsWith('10-')) return '9.0.0-beta.1'; if (v.startsWith('11-')) return '9.0.0-beta.1'; return v; } } exports.Migrator = Migrator; function parseMigrationsOptions(args) { const options = params_1.convertToCamelCase(minimist(args, { string: ['runMigrations', 'from', 'to'], alias: { runMigrations: 'run-migrations' } })); if (!options.runMigrations) { let from = {}; if (options.from) { options.from.split(',').forEach(p => { const split = p.lastIndexOf('@'); from[p.substring(0, split)] = p.substring(split + 1); }); } let to = {}; if (options.to) { options.to.split(',').forEach(p => { const split = p.lastIndexOf('@'); to[p.substring(0, split)] = p.substring(split + 1); }); } let targetPackage; let targetVersion; if (args[0] && args[0].indexOf('@') > 1) { const i = args[0].lastIndexOf('@'); targetPackage = args[0].substring(0, i); targetVersion = args[0].substring(i + 1); } else if (args[0]) { targetPackage = '@yolkai/nx-workspace'; targetVersion = args[0]; } else { targetPackage = '@yolkai/nx-workspace'; targetVersion = 'latest'; } return { type: 'generateMigrations', targetPackage, targetVersion, from, to }; } else { return { type: 'runMigrations', runMigrations: options.runMigrations }; } } function versions(root, from) { return (packageName) => { try { if (from[packageName]) { return from[packageName]; } const content = fs_1.readFileSync(path.join(root, `./node_modules/${packageName}/package.json`)); return JSON.parse(stripJsonComments(content.toString()))['version']; } catch (e) { return null; } }; } // testing-fetch-start function createFetcher(logger) { let cache = {}; return function f(packageName, packageVersion) { return __awaiter(this, void 0, void 0, function* () { if (!cache[`${packageName}-${packageVersion}`]) { const dir = tmp_1.dirSync().name; logger.info(`Fetching ${packageName}@${packageVersion}`); child_process_1.execSync(`npm install ${packageName}@${packageVersion} --prefix=${dir}`, { stdio: [] }); const json = JSON.parse(stripJsonComments(fs_1.readFileSync(path.join(dir, 'node_modules', packageName, 'package.json')).toString())); let migrationsFile = json['nx-migrations'] || json['ng-update']; // migrationsFile is an object if (migrationsFile && migrationsFile.migrations) { migrationsFile = migrationsFile.migrations; } // packageVersion can be a tag, resolvedVersion works with semver const resolvedVersion = json.version; try { if (migrationsFile && typeof migrationsFile === 'string') { const json = JSON.parse(stripJsonComments(fs_1.readFileSync(path.join(dir, 'node_modules', packageName, migrationsFile)).toString())); cache[`${packageName}-${packageVersion}`] = { version: resolvedVersion, schematics: json.schematics, packageJsonUpdates: json.packageJsonUpdates }; } else { cache[`${packageName}-${packageVersion}`] = { version: resolvedVersion }; } } catch (e) { logger.warn(`Could not find '${migrationsFile}' in '${packageName}'. Skipping it`); cache[`${packageName}-${packageVersion}`] = { version: resolvedVersion }; } } return cache[`${packageName}-${packageVersion}`]; }); }; } // testing-fetch-end function createMigrationsFile(root, migrations) { fs_1.writeFileSync(path.join(root, 'migrations.json'), JSON.stringify({ migrations }, null, 2)); } function updatePackageJson(root, updatedPackages) { const packageJsonPath = path.join(root, 'package.json'); const json = JSON.parse(stripJsonComments(fs_1.readFileSync(packageJsonPath).toString())); Object.keys(updatedPackages).forEach(p => { if (json.devDependencies && json.devDependencies[p]) { json.devDependencies[p] = updatedPackages[p].version; } else if (json.dependencies && json.dependencies[p]) { json.dependencies[p] = updatedPackages[p].version; } else if (updatedPackages[p].alwaysAddToPackageJson) { if (!json.dependencies) json.dependencies = {}; json.dependencies[p] = updatedPackages[p].version; } }); fs_1.writeFileSync(packageJsonPath, JSON.stringify(json, null, 2)); } function generateMigrationsJsonAndUpdatePackageJson(logger, root, opts) { return __awaiter(this, void 0, void 0, function* () { try { logger.info(`Fetching meta data about packages.`); logger.info(`It may take a few minutes.`); const migrator = new Migrator({ versions: versions(root, opts.from), fetch: createFetcher(logger), from: opts.from, to: opts.to }); const { migrations, packageJson } = yield migrator.updatePackageJson(opts.targetPackage, opts.targetVersion); updatePackageJson(root, packageJson); if (migrations.length > 0) { createMigrationsFile(root, migrations); logger.info(`The migrate command has run successfully.`); logger.info(`- package.json has been updated`); logger.info(`- migrations.json has been generated`); logger.info(`Next steps:`); logger.info(`- Make sure package.json changes make sense and then run 'npm install' or 'yarn'`); logger.info(`- Run 'nx migrate --run-migrations=migrations.json'`); } else { logger.info(`The migrate command has run successfully.`); logger.info(`- package.json has been updated`); logger.info(`- there are no migrations to run, so migrations.json has not been created.`); } } catch (e) { const startVersion = versions(root, {})('@yolkai/nx-workspace'); logger.error(`The migrate command failed. Try the following to migrate your workspace:`); logger.error(`> npm install --save-dev @yolkai/nx-workspace@latest`); logger.error(`> nx migrate ${opts.targetPackage}@${opts.targetVersion} --from="@yolkai/nx-workspace@${startVersion}"`); logger.error(`This will use the newest version of the migrate functionality, which might have your issue resolved.`); logger.error(`----------------------------------------------------------------------------------------------------`); throw e; } }); } class MigrationEngineHost extends tools_1.NodeModulesEngineHost { constructor() { super(); } _resolveCollectionPath(name) { let collectionPath = undefined; if (name.replace(/\\/g, '/').split('/').length > (name[0] == '@' ? 2 : 1)) { try { collectionPath = this._resolvePath(name, process.cwd()); } catch (_a) { } } if (!collectionPath) { let packageJsonPath = this._resolvePackageJson(name, process.cwd()); if (!core.fs.isFile(packageJsonPath)) { packageJsonPath = path.join(packageJsonPath, 'package.json'); } let pkgJsonSchematics = require(packageJsonPath)['nx-migrations']; if (!pkgJsonSchematics) { pkgJsonSchematics = require(packageJsonPath)['ng-update']; if (!pkgJsonSchematics) { throw new Error(`Could find migrations in package: "${name}"`); } } if (typeof pkgJsonSchematics != 'string') { pkgJsonSchematics = pkgJsonSchematics.migrations; } collectionPath = this._resolvePath(pkgJsonSchematics, path.dirname(packageJsonPath)); } try { if (collectionPath) { JSON.parse(stripJsonComments(fs_1.readFileSync(collectionPath).toString())); return collectionPath; } } catch (e) { throw new Error(`Invalid migration file in package: "${name}"`); } throw new Error(`Collection cannot be resolved: "${name}"`); } } class MigrationsWorkflow extends workflow_1.BaseWorkflow { constructor(host) { super({ host, engineHost: new MigrationEngineHost(), force: true, dryRun: false }); } } function runMigrations(logger, root, opts) { return __awaiter(this, void 0, void 0, function* () { const migrationsFile = JSON.parse(stripJsonComments(fs_1.readFileSync(path.join(root, opts.runMigrations)).toString())); const host = new core_1.virtualFs.ScopedHost(new node_1.NodeJsSyncHost(), core_1.normalize(root)); const workflow = new MigrationsWorkflow(host); let p = Promise.resolve(null); migrationsFile.migrations.forEach(m => { p = p.then(() => { logger.info(`Running migration ${m.package}:${m.name}`); return workflow .execute({ collection: m.package, schematic: m.name, options: {}, debug: false, logger }) .toPromise() .then(() => { logger.info(`Successfully finished ${m.package}:${m.name}`); logger.info(`---------------------------------------------------------`); }); }); }); yield p; }); } function migrate(root, args, isVerbose = false) { return __awaiter(this, void 0, void 0, function* () { const logger = logger_1.getLogger(isVerbose); return params_1.handleErrors(logger, isVerbose, () => __awaiter(this, void 0, void 0, function* () { const opts = parseMigrationsOptions(args); if (opts.type === 'generateMigrations') { yield generateMigrationsJsonAndUpdatePackageJson(logger, root, opts); } else { yield runMigrations(logger, root, opts); } })); }); } exports.migrate = migrate;