|
| 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; |
0 commit comments