| 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: [],