warp-drive
Version:
WarpDrive is the data framework for ambitious web applications
475 lines (456 loc) • 17.4 kB
JavaScript
import chalk from 'chalk';
import JSONC from 'comment-json';
import fs from 'fs';
import path from 'path';
import { e as exec, g as getTags, a as getInfo, b as getPackageManagerFromLockfile } from './warp-drive-pO7pDWOi.js';
import { w as write, g as getPkgJson, a as writePkgJson, b as getTypePathFor } from './utils-C3XozAKL.js';
const Main = ['@ember-data/active-record', '@ember-data/adapter', '@ember-data/codemods', '@ember-data/debug', '@ember-data/graph', '@ember-data/json-api', '@ember-data/legacy-compat', '@ember-data/model', '@ember-data/request-utils', '@ember-data/request', '@ember-data/rest', '@ember-data/serializer', '@ember-data/store', '@ember-data/tracking', '@warp-drive/build-config', '@warp-drive/core-types', '@warp-drive/diagnostic', '@warp-drive/ember', '@warp-drive/holodeck', '@warp-drive/schema-record', '@warp-drive/experiments', '@warp-drive/schema', 'ember-data', 'eslint-plugin-ember-data', 'eslint-plugin-warp-drive', 'warp-drive'];
const Types = ['@ember-data-types/active-record', '@ember-data-types/adapter', '@ember-data-types/graph', '@ember-data-types/json-api', '@ember-data-types/legacy-compat', '@ember-data-types/model', '@ember-data-types/request-utils', '@ember-data-types/request', '@ember-data-types/rest', '@ember-data-types/serializer', '@ember-data-types/store', '@ember-data-types/tracking', '@warp-drive-types/core-types', 'ember-data-types'];
const Mirror = ['@ember-data-mirror/active-record', '@ember-data-mirror/adapter', '@ember-data-mirror/graph', '@ember-data-mirror/json-api', '@ember-data-mirror/legacy-compat', '@ember-data-mirror/model', '@ember-data-mirror/request-utils', '@ember-data-mirror/request', '@ember-data-mirror/rest', '@ember-data-mirror/serializer', '@ember-data-mirror/store', '@ember-data-mirror/tracking', '@warp-drive-mirror/build-config', '@warp-drive-mirror/core-types', '@warp-drive-mirror/schema-record', 'ember-data-mirror'];
const DefinitelyTyped = ['@types/ember', '@types/ember-data', '@types/ember-data__adapter', '@types/ember-data__model', '@types/ember-data__serializer', '@types/ember-data__store', '@types/ember__application', '@types/ember__array', '@types/ember__component', '@types/ember__controller', '@types/ember__debug', '@types/ember__destroyable', '@types/ember__engine', '@types/ember__error', '@types/ember__helper', '@types/ember__modifier', '@types/ember__object', '@types/ember__owner', '@types/ember__routing', '@types/ember__runloop', '@types/ember__service', '@types/ember__string', '@types/ember__template', '@types/ember__test', '@types/ember__utils'];
const ALL = [].concat(Main, Types, Mirror);
const TS_CONFIG = {
include: ['app/**/*', 'config/**/*', 'tests/**/*'],
compilerOptions: {
lib: ['DOM', 'ESNext'],
module: 'esnext',
target: 'esnext',
moduleResolution: 'bundler',
moduleDetection: 'force',
strict: true,
pretty: true,
exactOptionalPropertyTypes: false,
downlevelIteration: true,
skipLibCheck: true,
allowSyntheticDefaultImports: true,
forceConsistentCasingInFileNames: true,
allowJs: true,
baseUrl: '.',
noImplicitOverride: false,
noImplicitAny: false,
experimentalDecorators: true,
incremental: true,
noEmit: true,
declaration: false,
types: []
}
};
function assertIsString(value) {
if (!value || typeof value !== 'string') {
throw new Error(`Expected value ${value} to be a string`);
}
}
async function retrofit(flags) {
const fit = flags.full.get('fit');
assertIsString(fit);
switch (fit) {
case 'mirror':
throw new Error('Not Implemented');
case 'types':
return await retrofitTypes(flags.full);
default:
throw new Error(`Unknown retrofit ${fit}`);
}
}
async function retrofitTypes(flags) {
if (!flags.get('monorepo')) {
return retrofitTypesForProject(flags);
}
// get the monorepo packages
const {
getPackageList
} = await import('./repo-CIdo1dpr.js');
const {
packages,
rootDir,
rootPackage,
pkgManager
} = await getPackageList();
const originalDir = process.cwd();
for (const pkg of packages) {
write(`Updating ${chalk.cyan(pkg.packageJson.name)}\n====================\n`);
process.chdir(pkg.dir);
await retrofitTypesForProject(flags);
}
write(`Updating ${chalk.cyan(rootPackage.packageJson.name)} (<monoreporoot>)\n====================\n`);
process.chdir(rootDir);
await retrofitTypesForProject(flags, {
isRoot: true});
const installCmd = `${pkgManager} install`;
await exec(installCmd);
write(`\t✅ Updated lockfile`);
process.chdir(originalDir);
}
async function retrofitTypesForProject(flags, options) {
const version = flags.get('version');
assertIsString(version);
// ensure version exists
const tags = await getTags('ember-data-types');
if (!tags.has(version)) {
throw new Error(`No published types exist for ${version}. You may want to try one of latest|beta|canary`);
}
write(`🚀 Retrofitting types to use @${version} using the separate-type-package strategy`);
// collect installed packages
const installed = new Map();
const needed = new Map();
const pkg = getPkgJson();
const deps = pkg.dependencies ?? {};
const devDeps = pkg.devDependencies ?? {};
Types.forEach(pkgName => {
let found = false;
if (deps[pkgName]) {
found = true;
installed.set(pkgName, {
version: deps[pkgName],
isTypes: true,
source: pkgNameFromTypes(pkgName)
});
}
if (devDeps[pkgName]) {
if (found) {
throw new Error(`${pkgName} is currently in both <pkg>.dependencies and <pkg>.devDependencies. It should be removed from one of these.`);
}
installed.set(pkgName, {
dev: true,
version: devDeps[pkgName],
isTypes: true,
source: pkgNameFromTypes(pkgName)
});
}
});
// for any main packages installed, we ensure we have the matching types
// package
Main.forEach(pkgName => {
const typesPkg = getTypesPackageName(pkgName);
let found = false;
if (deps[pkgName]) {
found = true;
if (typesPkg && !installed.has(typesPkg)) {
needed.set(typesPkg, {
version: deps[pkgName]
});
}
}
if (devDeps[pkgName]) {
if (found) {
throw new Error(`${pkgName} is currently in both <pkg>.dependencies and <pkg>.devDependencies. It should be removed from one of these.`);
}
if (typesPkg && !installed.has(typesPkg)) {
needed.set(typesPkg, {
dev: true,
version: devDeps[pkgName],
isTypes: true,
source: pkgName
});
}
}
});
// if any mirror packages are installed, we recommend bumping them
// to match the same types version
// if (flags.get('mirror')) {
Mirror.forEach(pkgName => {
let found = false;
if (deps[pkgName]) {
found = true;
installed.set(pkgName, {
version: deps[pkgName]
});
}
if (devDeps[pkgName]) {
if (found) {
throw new Error(`${pkgName} is currently in both <pkg>.dependencies and <pkg>.devDependencies. It should be removed from one of these.`);
}
installed.set(pkgName, {
dev: true,
version: devDeps[pkgName]
});
}
});
// }
// get matching version of each installed package
// from npm based on the dist-tag
write(`\tGenerating update for ${installed.size} installed dependencies`);
const seen = new Set();
const toInstall = new Map();
for (const [pkgName, available] of installed) {
seen.add(pkgName);
const pkgInfo = await getInfo(`${pkgName}@${version}`);
const mainPkgInfo = available.isTypes ? await getInfo(`${available.source}@${available.version}`) : null;
if (!pkgInfo) {
throw new Error(`No published version for ${pkgName}@${version}`);
}
if (available.isTypes && !mainPkgInfo) {
throw new Error(`No published version for ${available.source}@${available.version}`);
}
// if the version is the same
// we don't need to install it
if (available.version !== pkgInfo.version) {
toInstall.set(pkgName, {
dev: available.dev,
version: pkgInfo.version,
existing: true
});
}
// collect deps and peerDeps
const relatedInfo = mainPkgInfo || pkgInfo;
let relatedDeps = Object.assign({}, relatedInfo.dependencies, relatedInfo.peerDependencies);
if (available.isTypes) {
const mainPkgDeps = relatedDeps;
relatedDeps = {};
for (const depName in mainPkgDeps) {
if (ALL.includes(depName)) {
const typesPkg = getTypesPackageName(depName);
if (typesPkg) {
relatedDeps[typesPkg] = mainPkgDeps[depName];
}
}
}
}
for (const depName in relatedDeps) {
if (!ALL.includes(depName) || seen.has(depName) || installed.has(depName)) {
continue;
}
// don't install optional deps
if (relatedInfo.peerDependenciesMeta?.[depName]?.optional) {
continue;
}
seen.add(depName);
const depInfo = await getInfo(`${depName}@${relatedDeps[depName]}`);
if (!depInfo) {
throw new Error(`No published version for ${depName}@${relatedDeps[depName]}`);
}
toInstall.set(depName, {
dev: available.dev,
version: depInfo.version
});
}
}
// same for needed packages
write(`\tGenerating update for ${needed.size} missing dependencies`);
for (const [pkgName, available] of needed) {
if (seen.has(pkgName)) {
continue;
}
seen.add(pkgName);
const pkgInfo = await getInfo(`${pkgName}@${version}`);
const mainPkgInfo = available.isTypes ? await getInfo(`${available.source}@${version}`) : null;
if (!pkgInfo) {
throw new Error(`No published version for ${pkgName}@${version}`);
}
if (available.isTypes && !mainPkgInfo) {
throw new Error(`No published version for ${available.source}@${available.version}`);
}
toInstall.set(pkgName, {
dev: available.dev,
version: pkgInfo.version
});
// collect deps and peerDeps
const relatedInfo = mainPkgInfo || pkgInfo;
let relatedDeps = Object.assign({}, relatedInfo.dependencies, relatedInfo.peerDependencies);
if (available.isTypes) {
const mainPkgDeps = relatedDeps;
relatedDeps = {};
for (const depName in mainPkgDeps) {
const typesPkg = getTypesPackageName(depName);
if (typesPkg) {
relatedDeps[typesPkg] = mainPkgDeps[depName];
}
}
}
for (const depName in relatedDeps) {
if (!ALL.includes(depName) || seen.has(depName) || needed.has(depName)) {
continue;
}
// don't install optional deps
if (relatedInfo.peerDependenciesMeta?.[depName]?.optional) {
continue;
}
seen.add(depName);
const depInfo = await getInfo(`${depName}@${relatedDeps[depName]}`);
if (!depInfo) {
throw new Error(`No published version for ${depName}@${relatedDeps[depName]}`);
}
toInstall.set(depName, {
dev: available.dev,
version: depInfo.version
});
}
}
// add the packages to the package.json
// and install them
write(chalk.grey(`\t📦 Updating versions for ${toInstall.size} packages`));
if (toInstall.size > 0) {
// add the packages to the package.json
for (const [pkgName, config] of toInstall) {
if (config.dev) {
pkg.devDependencies = pkg.devDependencies ?? {};
pkg.devDependencies[pkgName] = config.version;
} else {
pkg.dependencies = pkg.dependencies ?? {};
pkg.dependencies[pkgName] = config.version;
}
}
// resort the package.json
if (pkg.dependencies) {
const keys = Object.keys(pkg.dependencies ?? {}).sort();
const sortedDeps = {};
for (const key of keys) {
sortedDeps[key] = pkg.dependencies[key];
}
pkg.dependencies = sortedDeps;
}
if (pkg.devDependencies) {
const keys = Object.keys(pkg.devDependencies ?? {}).sort();
const sortedDeps = {};
for (const key of keys) {
sortedDeps[key] = pkg.devDependencies[key];
}
pkg.devDependencies = sortedDeps;
}
if (pkg.pnpm?.overrides) {
const keys = Object.keys(pkg.pnpm.overrides ?? {}).sort();
const sortedDeps = {};
for (const key of keys) {
sortedDeps[key] = pkg.pnpm.overrides[key];
}
pkg.pnpm.overrides = sortedDeps;
}
}
const overrideChanges = new Set();
if (pkg.pnpm?.overrides) {
write(chalk.grey(`\t🔍 Checking for pnpm overrides to update`));
for (const pkgName of Object.keys(pkg.pnpm.overrides)) {
if (toInstall.has(pkgName)) {
const value = pkg.pnpm.overrides[pkgName];
const info = await getInfo(`${pkgName}@${version}`);
const newValue = info.version;
if (value !== newValue) {
overrideChanges.add(pkgName);
pkg.pnpm.overrides[pkgName] = newValue;
}
}
}
write(chalk.grey(`\t✅ Updated ${overrideChanges.size} pnpm overrides`));
}
const removed = new Set();
for (const [pkgName] of DefinitelyTyped) {
if (deps[pkgName]) {
removed.add(pkgName);
delete deps[pkgName];
}
if (devDeps[pkgName]) {
removed.add(pkgName);
delete devDeps[pkgName];
}
}
write(chalk.grey(`\t🗑 Removing ${removed.size} DefinitelyTyped packages`));
if (removed.size > 0 || toInstall.size > 0 || overrideChanges.size > 0) {
writePkgJson(pkg);
write(`\t✅ Updated package.json`);
// determine which package manager to use
// and install the packages
if (!flags.get('monorepo')) {
const pkgManager = getPackageManagerFromLockfile();
const installCmd = `${pkgManager} install`;
await exec(installCmd);
write(`\t✅ Updated lockfile`);
} else {
write(`\t☑️ Skipped lockfile update`);
}
}
const hasAtLeastOnePackage = toInstall.size > 0 || needed.size > 0 || installed.size > 0;
if (!hasAtLeastOnePackage) {
write(`\tNo WarpDrive/EmberData packages detected`);
return;
}
if (options?.isRoot) {
write(chalk.grey(`\t☑️ Skipped tsconfig.json update for monorepo root`));
return;
}
// ensure tsconfig for each installed and needed package
const fullTsConfigPath = path.join(process.cwd(), 'tsconfig.json');
const hasTsConfig = fs.existsSync(fullTsConfigPath);
if (!hasTsConfig) {
write(chalk.yellow(`\t⚠️ No tsconfig.json found in the current working directory`));
const tsConfig = structuredClone(TS_CONFIG);
tsConfig.compilerOptions.types = ['ember-source/types'];
for (const [pkgName, details] of toInstall) {
if (Types.includes(pkgName)) {
const typePath = await getTypePathFor(pkgName, details.version);
if (!typePath) {
throw new Error(`Could not find type path for ${pkgName}`);
}
tsConfig.compilerOptions.types.push(`./node_modules/${pkgName}/${typePath}`);
}
}
tsConfig.compilerOptions.types.sort();
fs.writeFileSync(fullTsConfigPath, JSON.stringify(tsConfig, null, 2) + '\n');
write(chalk.grey(`\t✅ created a tsconfig.json`));
} else {
let edited = false;
const tsConfig = JSONC.parse(fs.readFileSync(fullTsConfigPath, {
encoding: 'utf-8'
}));
if (!tsConfig.compilerOptions) {
tsConfig.compilerOptions = {
types: []
};
}
if (!tsConfig.compilerOptions.types) {
tsConfig.compilerOptions.types = [];
}
if (!tsConfig.compilerOptions.types.includes('ember-source/types')) {
edited = true;
tsConfig.compilerOptions.types.push('ember-source/types');
}
for (const [pkgName, details] of toInstall) {
if (Types.includes(pkgName)) {
const typePath = await getTypePathFor(pkgName, details.version);
if (!typePath) {
throw new Error(`Could not find type path for ${pkgName}`);
}
const fullTypePath = `./node_modules/${pkgName}/${typePath}`;
if (!tsConfig.compilerOptions.types.includes(fullTypePath)) {
edited = true;
tsConfig.compilerOptions.types.push(fullTypePath);
}
}
}
if (edited) {
tsConfig.compilerOptions.types.sort();
fs.writeFileSync(fullTsConfigPath, JSONC.stringify(tsConfig, null, 2) + '\n');
write(chalk.grey(`\t✅ updated tsconfig.json`));
} else {
write(`\tNo tsconfig updates required!`);
}
}
}
function getTypesPackageName(pkgName) {
let typesPkgName;
if (!pkgName.startsWith('@')) {
typesPkgName = pkgName + '-types';
} else {
const parts = pkgName.split('/');
parts[0] = parts[0] + '-types';
typesPkgName = parts.join('/');
}
if (Types.includes(typesPkgName)) {
return typesPkgName;
}
return null;
}
function pkgNameFromTypes(pkgName) {
let mainPkgName;
if (!pkgName.startsWith('@')) {
mainPkgName = pkgName.slice(0, pkgName.length - '-types'.length);
} else {
const parts = pkgName.split('/');
parts[0] = parts[0].slice(0, parts[0].length - '-types'.length);
mainPkgName = parts.join('/');
}
if (Main.includes(mainPkgName)) {
return mainPkgName;
}
throw new Error(`Could not find main package for ${pkgName}`);
}
export { retrofit };