Skip to content

Commit 1125da7

Browse files
refactor(scripts): add helper script for scoped beta package testing
Add a utility script that creates packages with beta scope for testing. This makes it easier to install modules locally with while maintaining proper scoped versioning. Issue: BTC-1933
1 parent 1d6e922 commit 1125da7

File tree

4 files changed

+242
-8
lines changed

4 files changed

+242
-8
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ lab/
1414
.env
1515
.yarn
1616
modules/**/dist/
17-
coverage
17+
modules/**/pack-scoped/
18+
coverage

scripts/pack-scoped.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/**
2+
* This is a helper that creates a archive package with a new scope (similar to what `prepare-release.ts` does).
3+
*
4+
* The archive can be used with `npm install path/to/tgz` to test the package locally.
5+
*/
6+
7+
import * as fs from 'fs';
8+
import * as execa from 'execa';
9+
import * as mpath from 'path';
10+
import * as yargs from 'yargs';
11+
import {
12+
walk,
13+
getLernaModules,
14+
changeScopeInFile,
15+
getDistTagsForModuleNames,
16+
updateModuleNames,
17+
setDependencyVersion,
18+
DistTags,
19+
LernaModule,
20+
getNewModuleName,
21+
} from './prepareRelease';
22+
23+
/** The directory to pack the module into */
24+
const scopedPackageDir = 'pack-scoped';
25+
26+
async function changeModuleScope(dir: string, params: { lernaModules: LernaModule[]; scope: string }) {
27+
console.log(`Changing scope of module at ${dir} to ${params.scope}`);
28+
walk(dir).forEach((file) => {
29+
changeScopeInFile(
30+
file,
31+
params.lernaModules.map((m) => m.name),
32+
params.scope
33+
);
34+
});
35+
}
36+
37+
async function changeModuleVersions(
38+
dir: string,
39+
params: {
40+
moduleNames: string[];
41+
scope: string;
42+
distTagsByModuleName?: Map<string, DistTags>;
43+
}
44+
) {
45+
const newModuleNames = params.moduleNames.map((m) => updateModuleNames(m, params.moduleNames, params.scope));
46+
const { distTagsByModuleName = await getDistTagsForModuleNames(newModuleNames) } = params;
47+
const packageJsonPath = mpath.join(dir, 'package.json');
48+
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf-8'));
49+
newModuleNames.forEach((m) => {
50+
const newVersion = distTagsByModuleName.get(m)?.beta;
51+
if (newVersion) {
52+
setDependencyVersion(packageJson, m, newVersion);
53+
}
54+
});
55+
await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
56+
}
57+
58+
async function getDistTagsForModuleNamesCached(
59+
dir: string,
60+
moduleNames: string[],
61+
params: {
62+
scope: string;
63+
cache?: string;
64+
}
65+
): Promise<Map<string, DistTags>> {
66+
if (params.cache) {
67+
try {
68+
console.log(`Loading cached dist tags from ${params.cache}`);
69+
return new Map<string, DistTags>(JSON.parse(await fs.promises.readFile(params.cache, 'utf-8')));
70+
} catch (e) {
71+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
72+
console.log(`No cached dist tags found at ${params.cache}`);
73+
// ignore
74+
} else {
75+
throw e;
76+
}
77+
}
78+
}
79+
80+
const newModuleNames = moduleNames.map((m) => updateModuleNames(m, moduleNames, params.scope));
81+
const distTagsByModuleName = await getDistTagsForModuleNames(newModuleNames);
82+
if (params.cache) {
83+
console.log(`Caching dist tags to ${params.cache}`);
84+
await fs.promises.writeFile(params.cache, JSON.stringify([...distTagsByModuleName.entries()], null, 2) + '\n');
85+
}
86+
return distTagsByModuleName;
87+
}
88+
89+
/** Change the scope of a module and update its dependencies */
90+
async function runChangeScope(
91+
dir: string,
92+
params: { lernaModules?: LernaModule[]; scope: string; cacheDistTags?: string }
93+
) {
94+
const { lernaModules = await getLernaModules() } = params;
95+
const moduleNames = lernaModules.map((m) => m.name);
96+
await changeModuleScope(dir, { ...params, lernaModules });
97+
await changeModuleVersions(dir, {
98+
...params,
99+
moduleNames,
100+
distTagsByModuleName: await getDistTagsForModuleNamesCached(dir, moduleNames, {
101+
scope: params.scope,
102+
cache: params.cacheDistTags,
103+
}),
104+
});
105+
}
106+
107+
function getModuleByDir(lernaModules: LernaModule[], dir: string): LernaModule {
108+
for (const m of lernaModules) {
109+
if (mpath.relative(m.location, dir) === '') {
110+
return m;
111+
}
112+
}
113+
114+
throw new Error(`Could not find module name for directory ${dir}`);
115+
}
116+
117+
function getArchiveName(m: LernaModule) {
118+
// normalize package name: @bitgo-beta/express -> bitgo-beta-express
119+
const packageName = m.name.replace(/^@/, '').replace(/\//g, '-');
120+
return `${packageName}-v${m.version}.tgz`;
121+
}
122+
123+
/** Pack the module and extract it to a directory */
124+
async function packExtract(moduleDir: string, archiveName: string, packDir: string): Promise<void> {
125+
// Create the directory if it doesn't exist
126+
const packDirPath = mpath.join(moduleDir, packDir);
127+
try {
128+
await fs.promises.rm(mpath.join(packDirPath, 'package'), { recursive: true });
129+
} catch (e) {
130+
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
131+
throw e;
132+
}
133+
}
134+
await fs.promises.mkdir(packDirPath, { recursive: true });
135+
136+
await execa('yarn', ['build'], { cwd: moduleDir });
137+
138+
try {
139+
// Pack the module using yarn to temp file
140+
await execa('yarn', ['pack'], {
141+
cwd: moduleDir,
142+
});
143+
144+
// Extract the archive
145+
await execa('tar', ['xzf', archiveName, '-C', packDir], {
146+
cwd: moduleDir,
147+
});
148+
149+
console.log(`Packed and extracted module to ${packDir}`);
150+
} finally {
151+
// Clean up temp file
152+
await fs.promises.unlink(mpath.join(moduleDir, archiveName)).catch((e) => {
153+
console.error(`Failed to clean up file: ${e}`);
154+
});
155+
}
156+
}
157+
158+
/** Pack the extracted package into a new archive */
159+
async function packArchive(moduleDir: string, archiveName: string, packDir: string): Promise<void> {
160+
await execa('tar', ['czf', archiveName, '-C', packDir, 'package'], {
161+
cwd: moduleDir,
162+
});
163+
}
164+
165+
const optScope = {
166+
describe: 'The new scope to set',
167+
type: 'string',
168+
default: '@bitgo-beta',
169+
} as const;
170+
171+
yargs
172+
.command({
173+
command: 'pack-scoped <dir>',
174+
describe: [
175+
'Pack a module with a specific scope. ',
176+
`Creates a package archive with the scope set to the specified value. `,
177+
].join(''),
178+
builder(yargs) {
179+
return yargs
180+
.positional('dir', {
181+
describe: 'Module directory',
182+
type: 'string',
183+
demandOption: true,
184+
})
185+
.options({
186+
scope: optScope,
187+
});
188+
},
189+
async handler({ dir, scope }) {
190+
const lernaModules = await getLernaModules();
191+
const module = getModuleByDir(lernaModules, dir);
192+
const archiveName = getArchiveName(module);
193+
await packExtract(dir, archiveName, scopedPackageDir);
194+
await runChangeScope(mpath.join(dir, scopedPackageDir, 'package'), {
195+
scope,
196+
lernaModules,
197+
cacheDistTags: mpath.join(dir, scopedPackageDir, '.distTags.cache.json'),
198+
});
199+
await packArchive(dir, archiveName, scopedPackageDir);
200+
console.log(`Packed ${getNewModuleName(module.name, scope)} to ${mpath.join(dir, archiveName)}.`);
201+
console.log(`Use 'npm install ${mpath.join(dir, archiveName)} --no-save' to test the package.`);
202+
},
203+
})
204+
.command({
205+
// Low-level command to the scope of a module for a directory without packing it. Useful for testing
206+
command: 'change-scope <dir>',
207+
describe: false,
208+
builder(yargs) {
209+
return yargs
210+
.positional('dir', {
211+
describe: 'Module directory',
212+
type: 'string',
213+
demandOption: true,
214+
})
215+
.option({
216+
scope: optScope,
217+
});
218+
},
219+
async handler({ dir, scope }) {
220+
await runChangeScope(dir, { scope });
221+
},
222+
})
223+
.help()
224+
.strict().argv;

scripts/prepareRelease/changeScopeInFile.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { readFileSync, writeFileSync } from 'fs';
22

3-
export function updateModuleNames(input: string, lernaModules: string[], targetScope: string): string {
4-
lernaModules.forEach((moduleName) => {
5-
const newName = `${moduleName.replace('@bitgo/', `${targetScope}/`)}`;
6-
input = input.replace(new RegExp(moduleName, 'g'), newName);
3+
export function getNewModuleName(moduleName: string, targetScope: string): string {
4+
return moduleName.replace('@bitgo/', `${targetScope}/`);
5+
}
6+
7+
export function updateModuleNames(input: string, moduleNames: string[], targetScope: string): string {
8+
moduleNames.forEach((moduleName) => {
9+
input = input.replace(new RegExp(moduleName, 'g'), getNewModuleName(moduleName, targetScope));
710
});
811
return input;
912
}
1013

11-
export function changeScopeInFile(filePath: string, lernaModules: string[], targetScope: string): number {
14+
export function changeScopeInFile(filePath: string, moduleNames: string[], targetScope: string): number {
1215
const oldContent = readFileSync(filePath, { encoding: 'utf8' });
13-
const newContent = updateModuleNames(oldContent, lernaModules, targetScope);
16+
const newContent = updateModuleNames(oldContent, moduleNames, targetScope);
1417
if (newContent !== oldContent) {
1518
writeFileSync(filePath, newContent, { encoding: 'utf-8' });
1619
return 1;

scripts/prepareRelease/getLernaModules.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import * as execa from 'execa';
22

3+
export type LernaModule = {
4+
name: string;
5+
location: string;
6+
version: string;
7+
};
8+
39
/**
410
* Create a function which can run lerna commands
511
* @param {String} lernaPath - path to lerna binary
@@ -12,7 +18,7 @@ function getLernaRunner(lernaPath: string) {
1218
};
1319
}
1420

15-
export async function getLernaModules(): Promise<Array<{ name: string; location: string }>> {
21+
export async function getLernaModules(): Promise<LernaModule[]> {
1622
const { stdout: lernaBinary } = await execa('yarn', ['bin', 'lerna'], { cwd: process.cwd() });
1723
const lerna = getLernaRunner(lernaBinary);
1824
return JSON.parse(await lerna('list', ['--loglevel', 'silent', '--json', '--all']));

0 commit comments

Comments
 (0)