diff --git a/.changeset/gamut-codemods-scope-rename.md b/.changeset/gamut-codemods-scope-rename.md new file mode 100644 index 0000000000..3a6a13605f --- /dev/null +++ b/.changeset/gamut-codemods-scope-rename.md @@ -0,0 +1,5 @@ +--- +'@skillsoft/gamut-codemods': minor +--- + +`scope-swap` now renames every `@codecademy/gamut*` package and `eslint-plugin-gamut` to `@skillsoft/*`: imports, exports, `require`, `import()`, `jest.mock`/`vi.mock`, `declare module`, import types, and package names in comments, `package.json`, `.mdx` ESM blocks, and `.yarnrc.yml` lists. `@codecademy/gamut-kit` has no `@skillsoft` replacement, so it's replaced with the packages it bundled, in `package.json` and in Module Federation `shared` config. That makes Gamut a real singleton across host and remotes, so the run ends with a reminder to deploy them together. Dependencies on a newer upstream release than `@skillsoft` has caught up to get a warning. diff --git a/packages/gamut-codemods/README.md b/packages/gamut-codemods/README.md index 8f174671a3..6f2a98ccb3 100644 --- a/packages/gamut-codemods/README.md +++ b/packages/gamut-codemods/README.md @@ -54,8 +54,13 @@ Every skillsoft-gamut pull request publishes installable previews through pkg.pr ### What scope-swap changes -| Migration | What it does | -| --------- | ------------ | +| Migration | What it does | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mf-shared` | Replaces `@codecademy/gamut-kit` in Module Federation `shared` config with the packages it bundled. Gamut becomes a real singleton, so test the host and remotes together. | +| `scope-rename` | Every `@codecademy/gamut*` module string becomes `@skillsoft/gamut*`: imports, exports, `require`, `import()`, `jest.mock`/`vi.mock`, `declare module`, and `import('x').T`. Also exact package-name strings elsewhere, like `transpilePackages`, with one warning per file, and package names in comments (`/dist` paths and removed packages are left for the leftovers). | +| `package-json` | Renames dependencies and sets their versions. A new package that's already listed keeps its value (a preview URL, say), and a `*` peer range stays `*`. Replaces `@codecademy/gamut-kit` with the individual packages. | +| `mdx-imports` | Runs the migrations above over `import`/`export` statements in `.mdx` files. Fenced code blocks are left alone. | +| `yarnrc` | `.yarnrc.yml` list items, like `npmPreapprovedPackages`: renames old package names, or drops them if a glob like `'@skillsoft/*'` already covers the new one. | ### Known limits diff --git a/packages/gamut-codemods/src/__tests__/__testfixtures__/end-to-end.input.tsx b/packages/gamut-codemods/src/__tests__/__testfixtures__/end-to-end.input.tsx new file mode 100644 index 0000000000..d3ece33271 --- /dev/null +++ b/packages/gamut-codemods/src/__tests__/__testfixtures__/end-to-end.input.tsx @@ -0,0 +1,5 @@ +import { Box } from '@codecademy/gamut'; +import { theme } from '@codecademy/gamut-styles'; + +export const Thing = () => ; +export const shared = { '@codecademy/gamut-kit': { singleton: true } }; diff --git a/packages/gamut-codemods/src/__tests__/__testfixtures__/end-to-end.output.tsx b/packages/gamut-codemods/src/__tests__/__testfixtures__/end-to-end.output.tsx new file mode 100644 index 0000000000..3c06199eab --- /dev/null +++ b/packages/gamut-codemods/src/__tests__/__testfixtures__/end-to-end.output.tsx @@ -0,0 +1,5 @@ +import { Box } from '@skillsoft/gamut'; +import { theme } from '@skillsoft/gamut-styles'; + +export const Thing = () => ; +export const shared = { '@skillsoft/gamut': { singleton: true }, '@skillsoft/gamut-icons': { singleton: true }, '@skillsoft/gamut-illustrations': { singleton: true }, '@skillsoft/gamut-patterns': { singleton: true }, '@skillsoft/gamut-styles': { singleton: true }, '@skillsoft/variance': { singleton: true } }; diff --git a/packages/gamut-codemods/src/__tests__/cli.test.ts b/packages/gamut-codemods/src/__tests__/cli.test.ts index 5c18886764..d17bc79f16 100644 --- a/packages/gamut-codemods/src/__tests__/cli.test.ts +++ b/packages/gamut-codemods/src/__tests__/cli.test.ts @@ -142,12 +142,16 @@ describe('main: refusing to write', () => { }); describe('main: full run', () => { - it('lists leftovers and prints the checklist', async () => { + it('rewrites sources, manifests, and docs, then prints the checklist', async () => { const dir = project({ 'src/Post.tsx': POST, + 'docs/story.mdx': "import { Box } from '@codecademy/gamut';\n\n# Hi\n", 'package.json': JSON.stringify({ name: 'app', - dependencies: { '@codecademy/gamut': '^73.0.0' }, + dependencies: { + '@codecademy/gamut': '^73.0.0', + '@codecademy/gamut-kit': '^73.0.0', + }, }), 'jest.config.js': "module.exports = { moduleNameMapper: { '^@codecademy/gamut/(.*)$': '/x/$1' } };\n", @@ -155,13 +159,17 @@ describe('main: full run', () => { expect(await main(['scope-swap', dir, '--force'])).toBe(0); + expect(read(dir, 'src/Post.tsx')).toContain("from '@skillsoft/gamut'"); + expect(read(dir, 'docs/story.mdx')).toContain("'@skillsoft/gamut'"); + expect(read(dir, 'package.json')).toContain('"@skillsoft/gamut"'); + expect(read(dir, 'package.json')).not.toContain('gamut-kit'); + const out = output(); expect(out).toMatch(/file\(s\) changed/); expect(out).toMatch(/leftovers \(\d+\)/); - expect(out).toMatch(/Post\.tsx:1/); - expect(out).toMatch(/package\.json:1/); expect(out).toMatch(/jest\.config\.js:1/); expect(out).toMatch(/next steps/); + expect(out).toMatch(/gamut-kit is gone/); expect(out).toMatch(/Reinstall \(yarn\)/); }); @@ -173,6 +181,7 @@ describe('main: full run', () => { expect(await main(['scope-swap', dir, '--dry'])).toBe(0); expect(read(dir, 'src/Post.tsx')).toBe(POST); expect(read(dir, 'package.json')).toBe(pkg); + expect(output()).toMatch(/would update .*package\.json/); expect(output()).toMatch(/skipped in --dry/); expect(output()).not.toMatch(/next steps/); }); @@ -184,6 +193,20 @@ describe('main: full run', () => { expect(output()).not.toMatch(/leftovers skipped/); }); + it('runs only the named migrations with --only', async () => { + const dir = project({ + 'src/Post.tsx': POST, + 'package.json': JSON.stringify({ + dependencies: { '@codecademy/gamut': '^73.0.0' }, + }), + }); + expect( + await main(['scope-swap', dir, '--force', '--only=package-json']) + ).toBe(0); + expect(read(dir, 'src/Post.tsx')).toBe(POST); + expect(read(dir, 'package.json')).toContain('"@skillsoft/gamut"'); + }); + it('returns 1 when the source runner reports errors', async () => { mockRun.mockResolvedValueOnce({ error: 1 }); const dir = project({ 'src/Post.tsx': POST }); diff --git a/packages/gamut-codemods/src/__tests__/lib.test.ts b/packages/gamut-codemods/src/__tests__/lib.test.ts index 5c821d40e1..f4d9cd1b6e 100644 --- a/packages/gamut-codemods/src/__tests__/lib.test.ts +++ b/packages/gamut-codemods/src/__tests__/lib.test.ts @@ -84,12 +84,13 @@ describe('findLeftovers', () => { "moduleNameMapper: { '^@codecademy/gamut(.*)$': 'x' }", "'@skillsoft/eslint-plugin-gamut'", "'@skillsoft/gamut-styles'", + "'@codecademy/gamut-kit'", ].join('\n') ); - expect(findLeftovers([dir], manifest).map((h) => h.line)).toEqual([1]); + expect(findLeftovers([dir], manifest).map((h) => h.line)).toEqual([1, 4]); }); - it('tags every old package the manifest replaces', () => { + it('tags every old package the manifest replaces, including gamut-kit', () => { const dir = tmp(); const old = [ ...Object.keys(manifest.packages), @@ -119,7 +120,7 @@ describe('findLeftovers', () => { it('reads editor config and extensionless scripts', () => { const dir = tmp(); - const line = "require('@codecademy/gamut/package.json');"; + const line = "require('@codecademy/gamut-kit/package.json');"; fs.writeFileSync( path.join(dir, 'sync-deps'), `#!/usr/bin/env node\n${line}` diff --git a/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/story.input.mdx b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/story.input.mdx new file mode 100644 index 0000000000..74c89ceda0 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/story.input.mdx @@ -0,0 +1,16 @@ +import { Canvas, Meta } from '@storybook/blocks'; +import { Box } from '@codecademy/gamut'; +import { theme } from '@codecademy/gamut-styles'; +import * as Stories from './Card.stories'; + + + +# Card + +Built on `@codecademy/gamut`'s Box. + +```tsx +import { Card } from '@codecademy/gamut'; +``` + + diff --git a/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/story.output.mdx b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/story.output.mdx new file mode 100644 index 0000000000..9631c38c47 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/story.output.mdx @@ -0,0 +1,16 @@ +import { Canvas, Meta } from '@storybook/blocks'; +import { Box } from '@skillsoft/gamut'; +import { theme } from '@skillsoft/gamut-styles'; +import * as Stories from './Card.stories'; + + + +# Card + +Built on `@codecademy/gamut`'s Box. + +```tsx +import { Card } from '@codecademy/gamut'; +``` + + diff --git a/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/unterminated.input.mdx b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/unterminated.input.mdx new file mode 100644 index 0000000000..db0db1b105 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/unterminated.input.mdx @@ -0,0 +1,4 @@ +import { + Text, + +Some prose that never closes the import. diff --git a/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/unterminated.output.mdx b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/unterminated.output.mdx new file mode 100644 index 0000000000..db0db1b105 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mdx-imports/__testfixtures__/unterminated.output.mdx @@ -0,0 +1,4 @@ +import { + Text, + +Some prose that never closes the import. diff --git a/packages/gamut-codemods/src/migrations/mdx-imports/index.ts b/packages/gamut-codemods/src/migrations/mdx-imports/index.ts new file mode 100644 index 0000000000..b0e7b34915 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mdx-imports/index.ts @@ -0,0 +1,63 @@ +import type { FileMigration } from '../../lib/types'; + +/* + MDX files (Storybook docs, mostly) import from Gamut like any module, but + jscodeshift can't parse MDX. This pulls out the top-level ESM statements + (`import ... from '...'`, `export ... from '...'`) and runs each one + through the preset's source migrations, so stories get the same scope + rename, Video split, and deep-import rewrites as .tsx files. + + Fenced code blocks are left alone. They're documentation, and a code + sample that still says @codecademy is the leftovers report's job. +*/ + +const FENCE = /^\s*(```|~~~)/; +const STATEMENT_START = /^(import\s|export\s+(\*|\{|type\s+\{))/; +const STATEMENT_END = + /(from\s*(['"])[^'"\n]*\2|^import\s*(['"])[^'"\n]*\3)\s*;?\s*$/; +/* A statement that runs longer than this isn't one we understand. */ +const MAX_STATEMENT_LINES = 50; + +export const mdxImports: FileMigration = { + name: 'mdx-imports', + kind: 'file', + description: + 'Run the source migrations over import/export statements in .mdx files.', + appliesTo: (file) => file.endsWith('.mdx'), + run({ source, transformSource }) { + const lines = source.split('\n'); + const out: string[] = []; + let inFence = false; + let changed = false; + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (FENCE.test(line)) inFence = !inFence; + if (inFence || !STATEMENT_START.test(line)) { + out.push(line); + continue; + } + + let end = i; + while ( + end < lines.length - 1 && + end - i < MAX_STATEMENT_LINES && + !STATEMENT_END.test(lines.slice(i, end + 1).join('\n')) + ) { + end += 1; + } + const statement = lines.slice(i, end + 1).join('\n'); + if (!STATEMENT_END.test(statement)) { + out.push(line); + continue; + } + + const next = transformSource(statement); + if (next !== statement) changed = true; + out.push(next); + i = end; + } + + return changed ? out.join('\n') : null; + }, +}; diff --git a/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/already-shared.input.ts b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/already-shared.input.ts new file mode 100644 index 0000000000..04847f011a --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/already-shared.input.ts @@ -0,0 +1,11 @@ +const shared = { + '@codecademy/gamut': { singleton: true, requiredVersion: '^73.0.0' }, + '@codecademy/gamut-kit': { singleton: true }, +}; +export const config = { shared }; +export const inline = { + shared: { + '@codecademy/gamut-kit': { eager: true }, + '@codecademy/gamut': { singleton: true }, + }, +}; diff --git a/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/already-shared.output.ts b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/already-shared.output.ts new file mode 100644 index 0000000000..e3154ac7d5 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/already-shared.output.ts @@ -0,0 +1,19 @@ +const shared = { + '@codecademy/gamut': { singleton: true, requiredVersion: '^73.0.0' }, + '@codecademy/gamut-icons': { singleton: true }, + '@codecademy/gamut-illustrations': { singleton: true }, + '@codecademy/gamut-patterns': { singleton: true }, + '@codecademy/gamut-styles': { singleton: true }, + '@codecademy/variance': { singleton: true }, +}; +export const config = { shared }; +export const inline = { + shared: { + '@codecademy/gamut-icons': { eager: true }, + '@codecademy/gamut-illustrations': { eager: true }, + '@codecademy/gamut-patterns': { eager: true }, + '@codecademy/gamut-styles': { eager: true }, + '@codecademy/variance': { eager: true }, + '@codecademy/gamut': { singleton: true }, + }, +}; diff --git a/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/computed-key.input.ts b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/computed-key.input.ts new file mode 100644 index 0000000000..0d890b8874 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/computed-key.input.ts @@ -0,0 +1,5 @@ +const key = '@codecademy/gamut'; +export const shared = { + [key]: { singleton: true }, + 123: { singleton: true }, +}; diff --git a/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/computed-key.output.ts b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/computed-key.output.ts new file mode 100644 index 0000000000..0d890b8874 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/computed-key.output.ts @@ -0,0 +1,5 @@ +const key = '@codecademy/gamut'; +export const shared = { + [key]: { singleton: true }, + 123: { singleton: true }, +}; diff --git a/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.input.ts b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.input.ts new file mode 100644 index 0000000000..caf4b09e6d --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.input.ts @@ -0,0 +1,11 @@ +export default { + plugins: [ + new ModuleFederationPlugin({ + shared: { + react: { singleton: true }, + '@codecademy/gamut-kit': { singleton: true }, + '@emotion/react': { singleton: true }, + }, + }), + ], +}; diff --git a/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.output.ts b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.output.ts new file mode 100644 index 0000000000..4c70a53c17 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.output.ts @@ -0,0 +1,16 @@ +export default { + plugins: [ + new ModuleFederationPlugin({ + shared: { + react: { singleton: true }, + '@codecademy/gamut': { singleton: true }, + '@codecademy/gamut-icons': { singleton: true }, + '@codecademy/gamut-illustrations': { singleton: true }, + '@codecademy/gamut-patterns': { singleton: true }, + '@codecademy/gamut-styles': { singleton: true }, + '@codecademy/variance': { singleton: true }, + '@emotion/react': { singleton: true }, + }, + }), + ], +}; diff --git a/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.warnings.json b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.warnings.json new file mode 100644 index 0000000000..cc4b2bc076 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/__testfixtures__/kit.warnings.json @@ -0,0 +1 @@ +["replaced '@codecademy/gamut-kit' in Module Federation shared config"] diff --git a/packages/gamut-codemods/src/migrations/mf-shared/index.ts b/packages/gamut-codemods/src/migrations/mf-shared/index.ts new file mode 100644 index 0000000000..9d3e677821 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/mf-shared/index.ts @@ -0,0 +1,112 @@ +import { isStringLiteral } from '../../lib/module-strings'; +import type { Edit, SpliceMigration } from '../../lib/types'; + +/* + Module Federation `shared` config that names a removed package: + + shared: { '@codecademy/gamut-kit': { singleton: true } } + becomes + shared: { + '@codecademy/gamut': { singleton: true }, + '@codecademy/gamut-styles': { singleton: true }, + ... + } + + Handles `const shared = { ... }` the same way. + + MF matches shared entries by import name, and code imports the individual + packages, not the kit. So the old entry never deduped anything, and after + this Gamut really is a singleton across host and remotes. That's a runtime + change, hence the warning and checklist item. scope-rename runs after this + and swaps the scope on the new keys. +*/ + +const keyName = (key: any): string | null => { + if (!key) return null; + if (key.type === 'Identifier') return key.name; + if (isStringLiteral(key)) return key.value; + return null; +}; + +/* + The object in `shared: { ... }` (inline plugin options) or + `const shared = { ... }` (declared, then passed in). +*/ +const sharedObject = (node: any) => { + if ( + (node.type === 'ObjectProperty' || node.type === 'Property') && + keyName(node.key) === 'shared' && + node.value?.type === 'ObjectExpression' + ) { + return node.value; + } + if ( + node.type === 'VariableDeclarator' && + keyName(node.id) === 'shared' && + node.init?.type === 'ObjectExpression' + ) { + return node.init; + } + return null; +}; + +export const mfShared: SpliceMigration = { + name: 'mf-shared', + kind: 'splice', + description: + 'Replace removed packages in Module Federation `shared` config with the packages they bundled.', + edits({ j, root, source, manifest, warn, note }) { + const edits: Edit[] = []; + + root + .find(j.Node) + .filter((p) => sharedObject(p.node) !== null) + .forEach((p) => { + const shared = sharedObject(p.node); + const existing = new Set( + shared.properties.map((prop: any) => keyName(prop.key)) + ); + + for (const prop of shared.properties) { + const removed = manifest.removedPackages[keyName(prop.key) ?? '']; + if (!removed) continue; + + const lineStart = source.lastIndexOf('\n', prop.start) + 1; + const before = source.slice(lineStart, prop.start); + /* One entry per line if the old one had its own line, else inline. */ + const ownLine = /^\s*$/.test(before); + const separator = ownLine ? `,\n${before}` : ', '; + const value = source.slice(prop.value.start, prop.value.end); + const additions = removed.mfSharedAs.filter( + (pkg) => !existing.has(pkg) + ); + const text = additions + .map((pkg) => `'${pkg}': ${value}`) + .join(separator); + + /* Nothing to add: drop the entry and the comma that follows it. */ + const end = + text || source[prop.end] !== ',' + ? prop.end + : prop.end + + 1 + + (source.slice(prop.end + 1).match(/^\s*/)?.[0].length ?? 0); + edits.push({ + start: text || !ownLine ? prop.start : lineStart, + end, + text, + }); + + warn( + prop, + `replaced '${keyName( + prop.key + )}' in Module Federation shared config. Gamut is now actually shared as a singleton; test the host and remotes together.` + ); + note('mf-shared'); + } + }); + + return edits; + }, +}; diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/already-renamed.input.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/already-renamed.input.json new file mode 100644 index 0000000000..a332e52190 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/already-renamed.input.json @@ -0,0 +1,9 @@ +{ + "name": "app", + "dependencies": { + "@codecademy/gamut-kit": "3.0.25", + "@skillsoft/gamut": "https://pkg.pr.new/@skillsoft/gamut@28", + "@skillsoft/gamut-icons": "https://pkg.pr.new/@skillsoft/gamut-icons@28", + "@codecademy/gamut-styles": "21.2.0" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/already-renamed.output.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/already-renamed.output.json new file mode 100644 index 0000000000..e51edd4454 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/already-renamed.output.json @@ -0,0 +1,12 @@ +{ + "name": "app", + "dependencies": { + "@skillsoft/gamut": "https://pkg.pr.new/@skillsoft/gamut@28", + "@skillsoft/gamut-icons": "https://pkg.pr.new/@skillsoft/gamut-icons@28", + "@skillsoft/gamut-illustrations": "^{{version}}", + "@skillsoft/gamut-patterns": "^{{version}}", + "@skillsoft/gamut-styles": "^{{version}}", + "@skillsoft/gamut-tests": "^{{version}}", + "@skillsoft/variance": "^{{version}}" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.input.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.input.json new file mode 100644 index 0000000000..1ad0d5bf74 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.input.json @@ -0,0 +1,11 @@ +{ + "name": "app", + "dependencies": { + "@codecademy/gamut": "73.6.1", + "@codecademy/gamut-kit": "3.0.24", + "react": "^19.0.0" + }, + "devDependencies": { + "eslint-plugin-gamut": "3.1.0" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.output.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.output.json new file mode 100644 index 0000000000..779450d73c --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.output.json @@ -0,0 +1,16 @@ +{ + "name": "app", + "dependencies": { + "@skillsoft/gamut": "^{{version}}", + "@skillsoft/gamut-icons": "^{{version}}", + "@skillsoft/gamut-illustrations": "^{{version}}", + "@skillsoft/gamut-patterns": "^{{version}}", + "@skillsoft/gamut-styles": "^{{version}}", + "@skillsoft/gamut-tests": "^{{version}}", + "@skillsoft/variance": "^{{version}}", + "react": "^19.0.0" + }, + "devDependencies": { + "@skillsoft/eslint-plugin-gamut": "^0.0.1" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.warnings.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.warnings.json new file mode 100644 index 0000000000..47a5eebf34 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/kit.warnings.json @@ -0,0 +1 @@ +["replaced '@codecademy/gamut-kit'"] diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.input.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.input.json new file mode 100644 index 0000000000..23e11d6c43 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.input.json @@ -0,0 +1,7 @@ +{ + "name": "app", + "dependencies": { + "@codecademy/gamut": "74.0.0", + "@codecademy/gamut-styles": "^21.2.0" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.output.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.output.json new file mode 100644 index 0000000000..780d3a617b --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.output.json @@ -0,0 +1,7 @@ +{ + "name": "app", + "dependencies": { + "@skillsoft/gamut": "^{{version}}", + "@skillsoft/gamut-styles": "^{{version}}" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.warnings.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.warnings.json new file mode 100644 index 0000000000..9fe9ebdcc7 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/newer-than-source.warnings.json @@ -0,0 +1 @@ +["'@codecademy/gamut@74.0.0' is newer than 73.6.1"] diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/peers.input.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/peers.input.json new file mode 100644 index 0000000000..27dbc10c94 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/peers.input.json @@ -0,0 +1,7 @@ +{ + "name": "lib", + "peerDependencies": { + "@codecademy/gamut": "*", + "@codecademy/gamut-styles": "^21.0.0" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/peers.output.json b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/peers.output.json new file mode 100644 index 0000000000..e4ecfdfaf1 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/__testfixtures__/peers.output.json @@ -0,0 +1,7 @@ +{ + "name": "lib", + "peerDependencies": { + "@skillsoft/gamut": "*", + "@skillsoft/gamut-styles": "^{{version}}" + } +} diff --git a/packages/gamut-codemods/src/migrations/package-json/index.ts b/packages/gamut-codemods/src/migrations/package-json/index.ts new file mode 100644 index 0000000000..de42ad63cf --- /dev/null +++ b/packages/gamut-codemods/src/migrations/package-json/index.ts @@ -0,0 +1,94 @@ +import { isNewerThan } from '../../lib/manifest'; +import type { FileMigration } from '../../lib/types'; + +/* + Renames dependencies in package.json and pins them to + manifest.targetVersions. Removed packages are replaced by the packages + they bundled. Only touched fields are re-sorted; indentation and the + trailing newline are kept. + + A new package that's already listed keeps its value. People add the new + names by hand before running this, often pointed at a preview tarball, + and overwriting that with a range that isn't published yet breaks + install. A `*` peer range stays `*`: it means "whatever the host has", + and narrowing it is a decision for the package owner. + + A range newer than manifest.sourceVersions gets a warning: the code may + already use APIs that only exist upstream, which no rename can fix. +*/ + +const FIELDS = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', +] as const; + +const sortKeys = (obj: Record) => + Object.fromEntries( + Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)) + ); + +const detectIndent = (text: string) => + (text.match(/^[ \t]+(?=")/m) ?? [' '])[0]; + +export const packageJson: FileMigration = { + name: 'package-json', + kind: 'file', + description: 'Rename dependencies in package.json.', + appliesTo: (file) => /(^|\/)package\.json$/.test(file), + run({ source, manifest, warn, note }) { + const pkg = JSON.parse(source) as Record; + let changed = false; + + for (const field of FIELDS) { + const deps = pkg[field] as Record | undefined; + if (!deps) continue; + const next = { ...deps }; + let touched = false; + + for (const [removed, { expandTo, note: why }] of Object.entries( + manifest.removedPackages + )) { + if (!(removed in next)) continue; + delete next[removed]; + for (const old of expandTo) { + const renamed = manifest.packages[old]?.to; + if (!(old in next) && !(renamed && renamed in next)) next[old] = '*'; + } + warn( + `${field}: replaced '${removed}' with its individual packages. ${why}` + ); + note(`removed:${removed}`); + touched = true; + } + + for (const [from, { to }] of Object.entries(manifest.packages)) { + if (!(from in next)) continue; + const range = next[from]; + const covered = manifest.sourceVersions[from]; + if (covered && isNewerThan(range, covered)) { + warn( + `${field}: '${from}@${range}' is newer than ${covered}, the last release ${to} matches. Code written against the newer release may not type-check until ${to} catches up.` + ); + } + delete next[from]; + touched = true; + if (to in next) continue; + next[to] = + field === 'peerDependencies' && range === '*' + ? '*' + : manifest.targetVersions[to]; + } + + if (touched) { + pkg[field] = sortKeys(next); + changed = true; + } + } + + if (!changed) return null; + const trailing = source.endsWith('\n') ? '\n' : ''; + return JSON.stringify(pkg, null, detectIndent(source)) + trailing; + }, +}; diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/comments.input.tsx b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/comments.input.tsx new file mode 100644 index 0000000000..bc44b7f3c0 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/comments.input.tsx @@ -0,0 +1,12 @@ +/** + * @deprecated + * @see `Disclosure` and `List` from `@codecademy/gamut` + * @example + * import { FillButton } from '@codecademy/gamut'; + * import { theme } from '@codecademy/gamut-styles'; + */ +export const Old = () => null; + +// Built on @codecademy/gamut-kit and @codecademy/gamut/dist/Box/props, see eslint-plugin-gamut. +// Already moved: @skillsoft/gamut and @skillsoft/eslint-plugin-gamut. +export const el =
{/* from @codecademy/variance */}
; diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/comments.output.tsx b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/comments.output.tsx new file mode 100644 index 0000000000..d1f4e01a73 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/comments.output.tsx @@ -0,0 +1,12 @@ +/** + * @deprecated + * @see `Disclosure` and `List` from `@skillsoft/gamut` + * @example + * import { FillButton } from '@skillsoft/gamut'; + * import { theme } from '@skillsoft/gamut-styles'; + */ +export const Old = () => null; + +// Built on @codecademy/gamut-kit and @codecademy/gamut/dist/Box/props, see @skillsoft/eslint-plugin-gamut. +// Already moved: @skillsoft/gamut and @skillsoft/eslint-plugin-gamut. +export const el =
{/* from @skillsoft/variance */}
; diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.input.tsx b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.input.tsx new file mode 100644 index 0000000000..db77a7d96b --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.input.tsx @@ -0,0 +1,20 @@ +import { Box } from '@codecademy/gamut'; +import { theme } from '@codecademy/gamut-styles'; +import { Video } from '@codecademy/gamut/Video'; +import { setupRtl } from '@codecademy/gamut-tests'; +import type { CoreTheme } from '@codecademy/gamut-styles'; +import { Kit } from '@codecademy/gamut-kit'; +import { Other } from '@codecademy/gamut-extra-thing'; + +export * from '@codecademy/gamut-icons'; + +declare module '@codecademy/gamut-styles' { + interface Theme extends CoreTheme {} +} + +jest.mock('@codecademy/variance'); +const lazy = () => import('@codecademy/gamut-patterns'); +const req = require('@codecademy/gamut-illustrations'); +type T = import('@codecademy/gamut').BoxProps; + +export const transpilePackages = ['@codecademy/gamut']; diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.output.tsx b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.output.tsx new file mode 100644 index 0000000000..dca4d84c5d --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.output.tsx @@ -0,0 +1,20 @@ +import { Box } from '@skillsoft/gamut'; +import { theme } from '@skillsoft/gamut-styles'; +import { Video } from '@skillsoft/gamut/Video'; +import { setupRtl } from '@skillsoft/gamut-tests'; +import type { CoreTheme } from '@skillsoft/gamut-styles'; +import { Kit } from '@codecademy/gamut-kit'; +import { Other } from '@codecademy/gamut-extra-thing'; + +export * from '@skillsoft/gamut-icons'; + +declare module '@skillsoft/gamut-styles' { + interface Theme extends CoreTheme {} +} + +jest.mock('@skillsoft/variance'); +const lazy = () => import('@skillsoft/gamut-patterns'); +const req = require('@skillsoft/gamut-illustrations'); +type T = import('@skillsoft/gamut').BoxProps; + +export const transpilePackages = ['@skillsoft/gamut']; diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.warnings.json b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.warnings.json new file mode 100644 index 0000000000..158b86d60b --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/everywhere.warnings.json @@ -0,0 +1 @@ +["'@codecademy/gamut-kit' has no replacement package", "renamed 1 package-name string(s) outside imports (@codecademy/gamut)"] diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/no-semicolons.input.tsx b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/no-semicolons.input.tsx new file mode 100644 index 0000000000..5570545ab3 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/no-semicolons.input.tsx @@ -0,0 +1,13 @@ +import { Box } from '@codecademy/gamut' + +jest.mock('./Card', () => { + return ({ title, description }: any) => ( +
+ {title} - {description} +
+ ) +}) + +jest.mock("@codecademy/gamut-icons", () => ({ + AiEditSparkIcon: () =>
AI Icon
, +})) diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/no-semicolons.output.tsx b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/no-semicolons.output.tsx new file mode 100644 index 0000000000..fac3c38590 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/no-semicolons.output.tsx @@ -0,0 +1,13 @@ +import { Box } from '@skillsoft/gamut' + +jest.mock('./Card', () => { + return ({ title, description }: any) => ( +
+ {title} - {description} +
+ ) +}) + +jest.mock("@skillsoft/gamut-icons", () => ({ + AiEditSparkIcon: () =>
AI Icon
, +})) diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/require-forms.input.ts b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/require-forms.input.ts new file mode 100644 index 0000000000..e3383b9181 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/require-forms.input.ts @@ -0,0 +1,3 @@ +import gamut = require('@codecademy/gamut'); +export const resolved = require.resolve('@codecademy/gamut/Video'); +export { gamut }; diff --git a/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/require-forms.output.ts b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/require-forms.output.ts new file mode 100644 index 0000000000..885aecb532 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/__testfixtures__/require-forms.output.ts @@ -0,0 +1,3 @@ +import gamut = require('@skillsoft/gamut'); +export const resolved = require.resolve('@skillsoft/gamut/Video'); +export { gamut }; diff --git a/packages/gamut-codemods/src/migrations/scope-rename/index.ts b/packages/gamut-codemods/src/migrations/scope-rename/index.ts new file mode 100644 index 0000000000..0c29583d10 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/scope-rename/index.ts @@ -0,0 +1,109 @@ +import { removedPackageFor, renameSpecifier } from '../../lib/manifest'; +import type { StringNode } from '../../lib/module-strings'; +import { findModuleStrings, isStringLiteral } from '../../lib/module-strings'; +import type { Edit, SpliceMigration } from '../../lib/types'; + +/* + Swaps old package names for new ones in every module position: imports, + exports, require, mocks, `declare module`, import types. Runs last, since + every other migration is written against the old names. + + Any other string that exactly names an old package (Next's + transpilePackages, Module Federation shared keys, lint ignore lists) is + rewritten too. Across mono, platform, front, and talent-intelligence every + one of those was a correct rename, so they get one warning per file + asking for a look at the diff, not one per string. + + This is a splice migration: it edits the text inside the quotes and never + asks recast to print. Mutating even one literal's value made recast + reprint neighbouring statements in semicolon-less files, including JSX + text whitespace. + + Package names in comments (JSDoc `@see`, usage examples) are renamed as + well. `/dist` paths and removed packages are left for the leftovers + report, since there's no name that's right for them. +*/ + +interface CommentNode { + value: string; + start: number; + end: number; +} + +const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); + +/* Old names, not part of a longer name and not followed by `/dist`. */ +const commentPattern = (from: string[]) => + new RegExp( + `(? b.length - a.length) + .map(escape) + .join('|')})(?![\\w-])(?!/dist(?![\\w-]))`, + 'g' + ); + +export const scopeRename: SpliceMigration = { + name: 'scope-rename', + kind: 'splice', + description: 'Swap old package specifiers for their new names.', + edits({ j, root, manifest, warn }) { + const edits: Edit[] = []; + const moduleStrings = new Set(findModuleStrings(j, root)); + const outsideImports: string[] = []; + + const visit = (literal: StringNode, inModulePosition: boolean) => { + const removed = removedPackageFor(manifest, literal.value); + if (removed) { + warn( + literal, + `'${removed}' has no replacement package. ${manifest.removedPackages[removed].note}` + ); + return; + } + const next = renameSpecifier(manifest, literal.value); + if (!next) return; + if (!inModulePosition) outsideImports.push(literal.value); + /* Inside the quotes only, so the file's quote style survives. */ + edits.push({ + start: literal.start + 1, + end: literal.end - 1, + text: next, + }); + }; + + for (const literal of moduleStrings) visit(literal, true); + + root + .find(j.Node) + .filter((p) => isStringLiteral(p.node) && !moduleStrings.has(p.node)) + .forEach((p) => visit(p.node as StringNode, false)); + + const pattern = commentPattern(Object.keys(manifest.packages)); + const seen = new Set(); + /* j.Comment paths point at the owning node, so walk node.comments. */ + root.find(j.Node).forEach((p) => { + const comments = (p.node as { comments?: CommentNode[] }).comments ?? []; + for (const comment of comments) { + if (seen.has(comment.start)) continue; + seen.add(comment.start); + for (const match of comment.value.matchAll(pattern)) { + const next = manifest.packages[match[0]]?.to; + if (!next) continue; + /* +2 skips the `//` or `/*` that `value` leaves out. */ + const start = comment.start + 2 + (match.index ?? 0); + edits.push({ start, end: start + match[0].length, text: next }); + } + } + }); + + if (outsideImports.length > 0) { + const names = [...new Set(outsideImports)].join(', '); + warn( + null, + `renamed ${outsideImports.length} package-name string(s) outside imports (${names}). Check the diff.` + ); + } + + return edits; + }, +}; diff --git a/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/no-glob.input.yml b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/no-glob.input.yml new file mode 100644 index 0000000000..6452a33ad1 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/no-glob.input.yml @@ -0,0 +1,2 @@ +npmPreapprovedPackages: + - eslint-plugin-gamut # lint rules diff --git a/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/no-glob.output.yml b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/no-glob.output.yml new file mode 100644 index 0000000000..7a6d8c80bf --- /dev/null +++ b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/no-glob.output.yml @@ -0,0 +1,2 @@ +npmPreapprovedPackages: + - '@skillsoft/eslint-plugin-gamut' # lint rules diff --git a/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/preapproved.input.yml b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/preapproved.input.yml new file mode 100644 index 0000000000..8c251e0841 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/preapproved.input.yml @@ -0,0 +1,10 @@ +npmPreapprovedPackages: + - '@skillsoft/*' + - 'eslint-plugin-gamut' + - "@codecademy/gamut-kit" + - react + +packageExtensions: + '@codecademy/gamut@*': + peerDependencies: + react: '*' diff --git a/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/preapproved.output.yml b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/preapproved.output.yml new file mode 100644 index 0000000000..0d09a98e03 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/yarnrc/__testfixtures__/preapproved.output.yml @@ -0,0 +1,9 @@ +npmPreapprovedPackages: + - '@skillsoft/*' + - "@codecademy/gamut-kit" + - react + +packageExtensions: + '@codecademy/gamut@*': + peerDependencies: + react: '*' diff --git a/packages/gamut-codemods/src/migrations/yarnrc/index.ts b/packages/gamut-codemods/src/migrations/yarnrc/index.ts new file mode 100644 index 0000000000..d3c6931788 --- /dev/null +++ b/packages/gamut-codemods/src/migrations/yarnrc/index.ts @@ -0,0 +1,45 @@ +import { renameSpecifier } from '../../lib/manifest'; +import type { FileMigration } from '../../lib/types'; + +/* + Package names listed in .yarnrc.yml, like npmPreapprovedPackages: + + npmPreapprovedPackages: + - '@skillsoft/*' + - 'eslint-plugin-gamut' + + An old name is renamed, or dropped if a scope glob in the same file + (`'@skillsoft/*'`) already covers the new one. Only exact list items are + touched; anything more involved shows up in the leftovers report. +*/ + +const LIST_ITEM = /^(\s*-\s*)(['"]?)([^'"\s#]+)\2(\s*(#.*)?)$/; + +export const yarnrc: FileMigration = { + name: 'yarnrc', + kind: 'file', + description: 'Rename or drop old package names in .yarnrc.yml lists.', + appliesTo: (file) => /(^|\/)\.yarnrc\.yml$/.test(file), + run({ source, manifest }) { + const lines = source.split('\n'); + const globs = lines + .map((line) => LIST_ITEM.exec(line)?.[3]) + .filter((value): value is string => !!value?.endsWith('/*')) + .map((glob) => glob.slice(0, -1)); + + let changed = false; + const out = lines.flatMap((line) => { + const match = LIST_ITEM.exec(line); + const next = match && renameSpecifier(manifest, match[3]); + if (!match || !next) return [line]; + changed = true; + if (globs.some((prefix) => next.startsWith(prefix))) return []; + const [, lead, quote, , trail] = match; + /* `@` is reserved in YAML, so a plain scalar can't start with it. */ + const q = quote || (next.startsWith('@') ? "'" : ''); + return [`${lead}${q}${next}${q}${trail}`]; + }); + + return changed ? out.join('\n') : null; + }, +}; diff --git a/packages/gamut-codemods/src/presets/scope-swap/index.ts b/packages/gamut-codemods/src/presets/scope-swap/index.ts index 69f1682399..f4db213fa3 100644 --- a/packages/gamut-codemods/src/presets/scope-swap/index.ts +++ b/packages/gamut-codemods/src/presets/scope-swap/index.ts @@ -1,15 +1,32 @@ import type { Preset } from '../../lib/types'; +import { mdxImports } from '../../migrations/mdx-imports'; +import { mfShared } from '../../migrations/mf-shared'; +import { packageJson } from '../../migrations/package-json'; +import { scopeRename } from '../../migrations/scope-rename'; +import { yarnrc } from '../../migrations/yarnrc'; import { manifest } from './manifest'; export const scopeSwap: Preset = { name: 'scope-swap', description: 'Move from @codecademy/gamut* to @skillsoft/gamut*.', manifest, - migrations: [], + migrations: [ + mfShared, + /* Last: everything above matches on @codecademy names. */ + scopeRename, + packageJson, + mdxImports, + yarnrc, + ], checklist: [ 'Run your formatter and lint autofix (prettier --write, then eslint --fix). Split imports come out in recast style, `@skillsoft` sorts differently from `@codecademy` for import-order rules like simple-import-sort, and the shorter scope lets some wrapped imports fit on one line.', 'Reinstall (yarn), then type-check and run your tests.', 'Work through the warnings above and the leftovers list.', ], - conditionalChecklist: {}, + conditionalChecklist: { + 'removed:@codecademy/gamut-kit': + "gamut-kit is gone. Delete anything that reads it for versions, such as a sync-gamut-deps script. @skillsoft/gamut-styles has @emotion/cache and stylis peer dependencies, so add them if yarn warns that they aren't provided.", + 'mf-shared': + "Module Federation shared config changed. Gamut is now a real singleton: run the host and every remote together, and update the remotes' shared config to match.", + }, }; diff --git a/packages/gamut-codemods/src/presets/scope-swap/manifest.ts b/packages/gamut-codemods/src/presets/scope-swap/manifest.ts index 6802bda554..6605d88983 100644 --- a/packages/gamut-codemods/src/presets/scope-swap/manifest.ts +++ b/packages/gamut-codemods/src/presets/scope-swap/manifest.ts @@ -1,3 +1,6 @@ +import fs from 'node:fs'; +import path from 'node:path'; + import type { Manifest } from '../../lib/manifest'; /* @@ -9,6 +12,16 @@ import type { Manifest } from '../../lib/manifest'; built packages, so a row can't reference an export that doesn't exist. */ +/* + This package is in the changesets `fixed` group with the packages it + migrates to, so its own version is theirs. +*/ +const { version } = JSON.parse( + fs.readFileSync(path.join(__dirname, '../../../package.json'), 'utf8') +) as { version: string }; + +const GROUP_RANGE = `^${version}`; + export const manifest: Manifest = { packages: { '@codecademy/gamut': { to: '@skillsoft/gamut' }, @@ -21,11 +34,60 @@ export const manifest: Manifest = { 'eslint-plugin-gamut': { to: '@skillsoft/eslint-plugin-gamut' }, }, - removedPackages: {}, + removedPackages: { + '@codecademy/gamut-kit': { + expandTo: [ + '@codecademy/gamut', + '@codecademy/gamut-icons', + '@codecademy/gamut-illustrations', + '@codecademy/gamut-patterns', + '@codecademy/gamut-styles', + '@codecademy/gamut-tests', + '@codecademy/variance', + ], + /* gamut-tests is test-only, so it has no business in a runtime share. */ + mfSharedAs: [ + '@codecademy/gamut', + '@codecademy/gamut-icons', + '@codecademy/gamut-illustrations', + '@codecademy/gamut-patterns', + '@codecademy/gamut-styles', + '@codecademy/variance', + ], + note: 'gamut-kit is not published under @skillsoft. Depend on the individual packages instead.', + }, + }, - targetVersions: {}, + targetVersions: { + '@skillsoft/gamut': GROUP_RANGE, + '@skillsoft/gamut-styles': GROUP_RANGE, + '@skillsoft/gamut-icons': GROUP_RANGE, + '@skillsoft/gamut-illustrations': GROUP_RANGE, + '@skillsoft/gamut-patterns': GROUP_RANGE, + '@skillsoft/gamut-tests': GROUP_RANGE, + '@skillsoft/variance': GROUP_RANGE, + /* + Versioned outside the fixed group. manifest.test.ts fails if this + falls behind packages/eslint-plugin-gamut/package.json. + */ + '@skillsoft/eslint-plugin-gamut': '^0.0.1', + }, - sourceVersions: {}, + /* + The upstream @codecademy release each package was forked from (the top + of its CHANGELOG.md before any @skillsoft entries). Bump these when + upstream changes are synced in. + */ + sourceVersions: { + '@codecademy/gamut': '73.6.1', + '@codecademy/gamut-styles': '21.2.0', + '@codecademy/gamut-icons': '10.2.0', + '@codecademy/gamut-illustrations': '1.1.0', + '@codecademy/gamut-patterns': '1.1.0', + '@codecademy/gamut-tests': '7.1.0', + '@codecademy/variance': '1.1.0', + 'eslint-plugin-gamut': '3.1.0', + }, movedExports: [],