Skip to content

Commit 583668e

Browse files
authored
feat: monorepo support for npx warp-drive (emberjs#9499)
1 parent bf1f992 commit 583668e

File tree

7 files changed

+292
-77
lines changed

7 files changed

+292
-77
lines changed

packages/-warp-drive/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@
5151
},
5252
"dependencies": {
5353
"@embroider/macros": "^1.16.1",
54+
"@manypkg/get-packages": "^2.2.1",
5455
"@warp-drive/build-config": "workspace:0.0.0-alpha.27",
5556
"semver": "^7.6.2",
57+
"debug": "^4.3.5",
5658
"chalk": "^5.3.0",
5759
"comment-json": "^4.2.3"
5860
},

packages/-warp-drive/src/-private/shared/npm.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
1+
import makeDebug from 'debug';
12
import fs from 'fs';
23
import { execSync } from 'node:child_process';
34
import path from 'path';
45

6+
const debug = makeDebug('warp-drive');
7+
58
type NpmInfo = {
69
'dist-tags': Record<string, string>;
710
version: string;
811
dependencies: Record<string, string>;
912
devDependencies: Record<string, string>;
1013
peerDependencies: Record<string, string>;
1114
peerDependenciesMeta: Record<string, { optional?: boolean }>;
15+
dist: {
16+
tarball: string;
17+
};
1218
};
1319

1420
const InfoCache: Record<string, NpmInfo> = {};
1521

1622
// eslint-disable-next-line @typescript-eslint/require-await
17-
export async function exec(cmd: string) {
18-
return execSync(cmd);
23+
export async function exec(cmd: string, args?: Parameters<typeof execSync>[1]) {
24+
debug(`exec: ${cmd}`);
25+
return execSync(cmd, { ...args });
1926
}
2027

2128
export async function getTags(project: string): Promise<Set<string>> {
2229
if (!InfoCache[project]) {
30+
const start = performance.now();
2331
const info = await exec(`npm view ${project} --json`);
32+
const end = performance.now();
33+
debug(`Fetched info for ${project} in ${end - start}ms`);
2434
InfoCache[project] = JSON.parse(String(info)) as unknown as NpmInfo;
2535
}
2636

@@ -30,7 +40,10 @@ export async function getTags(project: string): Promise<Set<string>> {
3040

3141
export async function getInfo(project: string): Promise<NpmInfo> {
3242
if (!InfoCache[project]) {
43+
const start = performance.now();
3344
const info = await exec(`npm view ${project} --json`);
45+
const end = performance.now();
46+
debug(`Fetched info for ${project} in ${end - start}ms`);
3447
InfoCache[project] = JSON.parse(String(info)) as unknown as NpmInfo;
3548
}
3649

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export async function getPackageList() {
2+
const { getPackages } = await import('@manypkg/get-packages');
3+
const { tool, packages, rootPackage, rootDir } = await getPackages(process.cwd());
4+
return {
5+
pkgManager: tool.type,
6+
packages,
7+
rootPackage,
8+
rootDir,
9+
};
10+
}

packages/-warp-drive/src/-private/shared/utils.ts

+56-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'fs';
33
import path from 'path';
44

55
import type { SEMVER_VERSION } from './channel';
6+
import { exec, getInfo } from './npm';
67

78
/**
89
* Like Pick but returns an object type instead of a union type.
@@ -272,6 +273,9 @@ export type PACKAGEJSON = {
272273
url: string;
273274
directory?: string;
274275
};
276+
pnpm?: {
277+
overrides?: Record<string, string>;
278+
};
275279
};
276280

277281
export function getPkgJson() {
@@ -284,14 +288,63 @@ export function writePkgJson(json: PACKAGEJSON) {
284288
fs.writeFileSync(path.join(process.cwd(), 'package.json'), JSON.stringify(json, null, 2) + '\n');
285289
}
286290

287-
export function getLocalPkgJson(name: string) {
291+
const pkgJsonCache: Record<string, PACKAGEJSON> = {};
292+
export function getLocalPkgJson(name: string, version: string) {
293+
if (pkgJsonCache[`${name}@${version}`]) {
294+
return pkgJsonCache[`${name}@${version}`];
295+
}
296+
288297
const file = fs.readFileSync(path.join(process.cwd(), 'node_modules', name, 'package.json'), { encoding: 'utf-8' });
289298
const json = JSON.parse(file) as PACKAGEJSON;
299+
300+
pkgJsonCache[`${name}@${json.version}`] = json;
301+
if (json.version === version) {
302+
return json;
303+
}
304+
305+
return null;
306+
}
307+
308+
export async function getRemotePkgJson(name: string, version: string) {
309+
if (pkgJsonCache[`${name}@${version}`]) {
310+
return pkgJsonCache[`${name}@${version}`];
311+
}
312+
313+
const dir = path.join(process.cwd(), 'tmp');
314+
fs.mkdirSync(dir, { recursive: true });
315+
316+
// get the tarball path
317+
const info = await getInfo(`${name}@${version}`);
318+
const remoteTarball = info.dist.tarball;
319+
320+
const tarballName = `${name}-${version}`.replace('/', '_');
321+
322+
// download the package if needed
323+
if (!fs.existsSync(path.join(dir, tarballName, 'package.json'))) {
324+
await exec(`curl -L ${remoteTarball} -o ${tarballName}.tgz`, { cwd: dir });
325+
await exec(`tar -xzf ${tarballName}.tgz && mv package ${tarballName}`, { cwd: dir });
326+
}
327+
const file = fs.readFileSync(path.join(dir, tarballName, 'package.json'), { encoding: 'utf-8' });
328+
const json = JSON.parse(file) as PACKAGEJSON;
329+
pkgJsonCache[`${name}@${json.version}`] = json;
290330
return json;
291331
}
292332

293-
export function getTypePathFor(name: string) {
294-
const pkg = getLocalPkgJson(name);
333+
export async function getPkgJsonFor(name: string, version: string) {
334+
try {
335+
const pkg = getLocalPkgJson(name, version);
336+
if (pkg) {
337+
return pkg;
338+
}
339+
} catch {
340+
// ignore
341+
}
342+
343+
return getRemotePkgJson(name, version);
344+
}
345+
346+
export async function getTypePathFor(name: string, version: string) {
347+
const pkg = await getPkgJsonFor(name, version);
295348
const isAlpha = pkg.files?.includes('unstable-preview-types');
296349
const isBeta = pkg.files?.includes('preview-types');
297350
const base = pkg.exports?.['.'];

packages/-warp-drive/src/-private/warp-drive/cmd.config.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const RETROFIT_OPTIONS: FlagConfig = {
3333
help: {
3434
name: 'Help',
3535
flag: 'help',
36-
flag_aliases: ['h', 'm'],
36+
flag_aliases: ['h'],
3737
flag_mispellings: [
3838
'desc',
3939
'describe',
@@ -147,6 +147,16 @@ export const RETROFIT_OPTIONS: FlagConfig = {
147147
}
148148
},
149149
},
150+
151+
monorepo: {
152+
name: 'Monorepo',
153+
flag: 'monorepo',
154+
flag_aliases: ['m'],
155+
type: Boolean,
156+
description: 'Retrofit a monorepo setup',
157+
examples: [],
158+
default_value: false,
159+
},
150160
};
151161

152162
export const COMMANDS: CommandConfig = {

packages/-warp-drive/src/-private/warp-drive/commands/retrofit.ts

+82-10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@ export async function retrofit(flags: ParsedFlags) {
3232
}
3333

3434
async function retrofitTypes(flags: Map<string, string | number | boolean | null>) {
35+
if (!flags.get('monorepo')) {
36+
return retrofitTypesForProject(flags);
37+
}
38+
39+
// get the monorepo packages
40+
const { getPackageList } = await import('../../shared/repo');
41+
const { packages, rootDir, rootPackage, pkgManager } = await getPackageList();
42+
const originalDir = process.cwd();
43+
44+
for (const pkg of packages) {
45+
write(`Updating ${chalk.cyan(pkg.packageJson.name)}\n====================\n`);
46+
process.chdir(pkg.dir);
47+
await retrofitTypesForProject(flags);
48+
}
49+
50+
write(`Updating ${chalk.cyan(rootPackage!.packageJson.name)} (<monoreporoot>)\n====================\n`);
51+
process.chdir(rootDir);
52+
await retrofitTypesForProject(flags, { isRoot: true, pkgManager });
53+
54+
const installCmd = `${pkgManager} install`;
55+
await exec(installCmd);
56+
write(`\t✅ Updated lockfile`);
57+
58+
process.chdir(originalDir);
59+
}
60+
61+
async function retrofitTypesForProject(
62+
flags: Map<string, string | number | boolean | null>,
63+
options?: { isRoot: boolean; pkgManager: string }
64+
) {
3565
const version = flags.get('version');
3666
assertIsString(version);
3767

@@ -232,7 +262,7 @@ async function retrofitTypes(flags: Map<string, string | number | boolean | null
232262

233263
// add the packages to the package.json
234264
// and install them
235-
write(chalk.grey(`\t📦 Installing ${toInstall.size} packages`));
265+
write(chalk.grey(`\t📦 Updating versions for ${toInstall.size} packages`));
236266
if (toInstall.size > 0) {
237267
// add the packages to the package.json
238268
for (const [pkgName, config] of toInstall) {
@@ -262,6 +292,32 @@ async function retrofitTypes(flags: Map<string, string | number | boolean | null
262292
}
263293
pkg.devDependencies = sortedDeps;
264294
}
295+
if (pkg.pnpm?.overrides) {
296+
const keys = Object.keys(pkg.pnpm.overrides ?? {}).sort();
297+
const sortedDeps: Record<string, string> = {};
298+
for (const key of keys) {
299+
sortedDeps[key] = pkg.pnpm.overrides[key];
300+
}
301+
pkg.pnpm.overrides = sortedDeps;
302+
}
303+
}
304+
305+
const overrideChanges = new Set<string>();
306+
if (pkg.pnpm?.overrides) {
307+
write(chalk.grey(`\t🔍 Checking for pnpm overrides to update`));
308+
for (const pkgName of Object.keys(pkg.pnpm.overrides)) {
309+
if (toInstall.has(pkgName)) {
310+
const value = pkg.pnpm.overrides[pkgName];
311+
const info = await getInfo(`${pkgName}@${version}`);
312+
const newValue = info.version;
313+
314+
if (value !== newValue) {
315+
overrideChanges.add(pkgName);
316+
pkg.pnpm.overrides[pkgName] = newValue;
317+
}
318+
}
319+
}
320+
write(chalk.grey(`\t✅ Updated ${overrideChanges.size} pnpm overrides`));
265321
}
266322

267323
const removed = new Set();
@@ -277,15 +333,31 @@ async function retrofitTypes(flags: Map<string, string | number | boolean | null
277333
}
278334
write(chalk.grey(`\t🗑 Removing ${removed.size} DefinitelyTyped packages`));
279335

280-
if (removed.size > 0 || toInstall.size > 0) {
336+
if (removed.size > 0 || toInstall.size > 0 || overrideChanges.size > 0) {
281337
writePkgJson(pkg);
338+
write(`\t✅ Updated package.json`);
282339

283340
// determine which package manager to use
284341
// and install the packages
285-
const pkgManager = getPackageManagerFromLockfile();
286-
const installCmd = `${pkgManager} install`;
287-
await exec(installCmd);
288-
write(`\t✅ Updated package.json`);
342+
if (!flags.get('monorepo')) {
343+
const pkgManager = getPackageManagerFromLockfile();
344+
const installCmd = `${pkgManager} install`;
345+
await exec(installCmd);
346+
write(`\t✅ Updated lockfile`);
347+
} else {
348+
write(`\t☑️ Skipped lockfile update`);
349+
}
350+
}
351+
352+
const hasAtLeastOnePackage = toInstall.size > 0 || needed.size > 0 || installed.size > 0;
353+
if (!hasAtLeastOnePackage) {
354+
write(`\tNo WarpDrive/EmberData packages detected`);
355+
return;
356+
}
357+
358+
if (options?.isRoot) {
359+
write(chalk.grey(`\t☑️ Skipped tsconfig.json update for monorepo root`));
360+
return;
289361
}
290362

291363
// ensure tsconfig for each installed and needed package
@@ -296,9 +368,9 @@ async function retrofitTypes(flags: Map<string, string | number | boolean | null
296368
write(chalk.yellow(`\t⚠️ No tsconfig.json found in the current working directory`));
297369
const tsConfig = structuredClone(TS_CONFIG) as { compilerOptions: { types: string[] } };
298370
tsConfig.compilerOptions.types = ['ember-source/types'];
299-
for (const [pkgName] of toInstall) {
371+
for (const [pkgName, details] of toInstall) {
300372
if (Types.includes(pkgName)) {
301-
const typePath = getTypePathFor(pkgName);
373+
const typePath = await getTypePathFor(pkgName, details.version);
302374
if (!typePath) {
303375
throw new Error(`Could not find type path for ${pkgName}`);
304376
}
@@ -326,9 +398,9 @@ async function retrofitTypes(flags: Map<string, string | number | boolean | null
326398
tsConfig.compilerOptions.types.push('ember-source/types');
327399
}
328400

329-
for (const [pkgName] of toInstall) {
401+
for (const [pkgName, details] of toInstall) {
330402
if (Types.includes(pkgName)) {
331-
const typePath = getTypePathFor(pkgName);
403+
const typePath = await getTypePathFor(pkgName, details.version);
332404
if (!typePath) {
333405
throw new Error(`Could not find type path for ${pkgName}`);
334406
}

0 commit comments

Comments
 (0)