@sebgroup/green-core 2.37.1 → 2.37.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/context-cli/index.js +5 -0
- package/bin/context-cli/migrate/engine.d.ts +48 -0
- package/bin/context-cli/migrate/engine.js +111 -0
- package/bin/context-cli/migrate/index.d.ts +15 -0
- package/bin/context-cli/migrate/index.js +232 -0
- package/bin/context-cli/migrate/registry.d.ts +43 -0
- package/bin/context-cli/migrate/registry.js +75 -0
- package/bin/context-cli/migrate/types.d.ts +135 -0
- package/bin/context-cli/migrate/types.js +0 -0
- package/bin/context-cli/migrate/units/0/example.d.ts +52 -0
- package/bin/context-cli/migrate/units/0/example.js +146 -0
- package/components/pagination/pagination.component.js +14 -3
- package/custom-elements.json +969 -707
- package/gds-element.js +1 -1
- package/generated/mcp/components.json +1 -1
- package/generated/mcp/guides/migration.md +5 -0
- package/generated/mcp/icons.json +1 -1
- package/generated/mcp/index.json +1 -1
- package/package.json +1 -1
- package/utils/helpers/custom-element-scoping.js +1 -1
package/bin/context-cli/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
DOCS_FRAMEWORK_CANONICAL,
|
|
15
15
|
normalizeDocsFramework
|
|
16
16
|
} from "./framework.js";
|
|
17
|
+
import { handleMigrate } from "./migrate/index.js";
|
|
17
18
|
import { parseArgs } from "./parse-args.js";
|
|
18
19
|
function getPrimaryTextContent(result) {
|
|
19
20
|
const textBlock = result.content.find(
|
|
@@ -43,6 +44,7 @@ COMMANDS
|
|
|
43
44
|
guides List available guides
|
|
44
45
|
guide <name> Get a specific guide's content
|
|
45
46
|
instructions Get base usage instructions
|
|
47
|
+
migrate <subcommand> Run code migrations
|
|
46
48
|
|
|
47
49
|
GLOBAL OPTIONS
|
|
48
50
|
-h, --help Show this help message
|
|
@@ -337,6 +339,9 @@ async function main() {
|
|
|
337
339
|
case "instructions":
|
|
338
340
|
await runInstructions(args);
|
|
339
341
|
break;
|
|
342
|
+
case "migrate":
|
|
343
|
+
await handleMigrate(args);
|
|
344
|
+
break;
|
|
340
345
|
default:
|
|
341
346
|
process.stderr.write(`Error: Unknown command '${args.command}'.
|
|
342
347
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration engine — file discovery and migration orchestration.
|
|
3
|
+
*
|
|
4
|
+
* The engine walks a directory tree, filters files by extension, and runs
|
|
5
|
+
* migration units in detect-only or detect+apply mode. All file I/O uses
|
|
6
|
+
* Node built-in modules (no external dependencies).
|
|
7
|
+
*
|
|
8
|
+
* Security:
|
|
9
|
+
* - Writes files only when `apply: true` is explicitly set
|
|
10
|
+
* - Skips unreadable files and directories silently
|
|
11
|
+
* - Normalises all output paths to forward slashes
|
|
12
|
+
*
|
|
13
|
+
* @module migrate/engine
|
|
14
|
+
*/
|
|
15
|
+
import type { MigrationResult, MigrationUnit } from './types.js';
|
|
16
|
+
/** Directories that are always skipped during file discovery. */
|
|
17
|
+
export declare const DEFAULT_SKIP_DIRS: Set<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Discover all files under `basePath` whose extension is in `extensions`.
|
|
20
|
+
*
|
|
21
|
+
* @param basePath - Root directory to scan
|
|
22
|
+
* @param extensions - Set of extensions to include (e.g. `new Set(['.ts', '.html'])`)
|
|
23
|
+
* @param skipDirs - Directory names to skip (defaults to {@link DEFAULT_SKIP_DIRS})
|
|
24
|
+
* @returns Sorted array of absolute file paths
|
|
25
|
+
*/
|
|
26
|
+
export declare function discoverFiles(basePath: string, extensions: Set<string>, skipDirs?: Set<string>): Promise<string[]>;
|
|
27
|
+
/** Options for {@link runMigrations}. */
|
|
28
|
+
export interface RunOptions {
|
|
29
|
+
/** Root directory to scan for files */
|
|
30
|
+
basePath: string;
|
|
31
|
+
/** Migration units to execute */
|
|
32
|
+
units: MigrationUnit[];
|
|
33
|
+
/** When true, apply transformations and write files. Default: false (detect only). */
|
|
34
|
+
apply?: boolean;
|
|
35
|
+
/** Additional directory names to skip during file discovery */
|
|
36
|
+
skipDirs?: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run one or more migration units against a directory tree.
|
|
40
|
+
*
|
|
41
|
+
* In detect-only mode (default), files are scanned but never modified.
|
|
42
|
+
* In apply mode (`apply: true`), each unit's `apply()` function is called
|
|
43
|
+
* and the result is written back to disk.
|
|
44
|
+
*
|
|
45
|
+
* @param options - Configuration for the migration run
|
|
46
|
+
* @returns One {@link MigrationResult} per unit, in the same order as `options.units`
|
|
47
|
+
*/
|
|
48
|
+
export declare function runMigrations(options: RunOptions): Promise<MigrationResult[]>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import "../../../chunks/chunk.QU3DSPNU.js";
|
|
2
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { extname, join, relative, resolve } from "node:path";
|
|
4
|
+
const DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5
|
+
"node_modules",
|
|
6
|
+
".git",
|
|
7
|
+
"dist",
|
|
8
|
+
".nx",
|
|
9
|
+
".cache",
|
|
10
|
+
"coverage",
|
|
11
|
+
".angular",
|
|
12
|
+
".next",
|
|
13
|
+
"__pycache__"
|
|
14
|
+
]);
|
|
15
|
+
async function* walkDir(dir, skipDirs) {
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
19
|
+
} catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
if (skipDirs.has(entry.name)) continue;
|
|
25
|
+
yield* walkDir(join(dir, entry.name), skipDirs);
|
|
26
|
+
} else if (entry.isFile()) {
|
|
27
|
+
yield join(dir, entry.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function discoverFiles(basePath, extensions, skipDirs) {
|
|
32
|
+
const skip = skipDirs ?? DEFAULT_SKIP_DIRS;
|
|
33
|
+
const files = [];
|
|
34
|
+
for await (const filePath of walkDir(basePath, skip)) {
|
|
35
|
+
if (extensions.has(extname(filePath))) {
|
|
36
|
+
files.push(filePath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return files.sort();
|
|
40
|
+
}
|
|
41
|
+
async function runMigrations(options) {
|
|
42
|
+
const { basePath, units, apply = false, skipDirs: extraSkip } = options;
|
|
43
|
+
const resolvedBase = resolve(basePath);
|
|
44
|
+
const skipDirs = new Set(DEFAULT_SKIP_DIRS);
|
|
45
|
+
if (extraSkip) {
|
|
46
|
+
for (const dir of extraSkip) skipDirs.add(dir);
|
|
47
|
+
}
|
|
48
|
+
const allExtensions = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const unit of units) {
|
|
50
|
+
for (const ext of unit.fileExtensions) {
|
|
51
|
+
allExtensions.add(ext.startsWith(".") ? ext : `.${ext}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const allFiles = await discoverFiles(resolvedBase, allExtensions, skipDirs);
|
|
55
|
+
const unitExtSets = /* @__PURE__ */ new Map();
|
|
56
|
+
for (const unit of units) {
|
|
57
|
+
unitExtSets.set(
|
|
58
|
+
unit.id,
|
|
59
|
+
new Set(
|
|
60
|
+
unit.fileExtensions.map((e) => e.startsWith(".") ? e : `.${e}`)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const results = [];
|
|
65
|
+
for (const unit of units) {
|
|
66
|
+
const extSet = unitExtSets.get(unit.id);
|
|
67
|
+
const relevantFiles = allFiles.filter((f) => extSet.has(extname(f)));
|
|
68
|
+
const allMatches = [];
|
|
69
|
+
const manualReview = [];
|
|
70
|
+
let filesModified = 0;
|
|
71
|
+
for (const filePath of relevantFiles) {
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
content = await readFile(filePath, "utf-8");
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const matches = unit.detect(filePath, content);
|
|
79
|
+
if (matches.length === 0) continue;
|
|
80
|
+
const relFile = relative(resolvedBase, filePath).replace(/\\/g, "/");
|
|
81
|
+
for (const m of matches) {
|
|
82
|
+
m.file = relFile;
|
|
83
|
+
if (m.manualReview) {
|
|
84
|
+
manualReview.push(m);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
allMatches.push(...matches);
|
|
88
|
+
if (apply) {
|
|
89
|
+
const transformed = unit.apply(filePath, content);
|
|
90
|
+
if (transformed !== null && transformed !== content) {
|
|
91
|
+
await writeFile(filePath, transformed, "utf-8");
|
|
92
|
+
filesModified++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
results.push({
|
|
97
|
+
unitId: unit.id,
|
|
98
|
+
summary: unit.summary,
|
|
99
|
+
filesScanned: relevantFiles.length,
|
|
100
|
+
matches: allMatches,
|
|
101
|
+
filesModified,
|
|
102
|
+
manualReview
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
DEFAULT_SKIP_DIRS,
|
|
109
|
+
discoverFiles,
|
|
110
|
+
runMigrations
|
|
111
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handler for the `migrate` command.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* - `list` — List available migration units
|
|
6
|
+
* - `run` — Detect (and optionally apply) migrations
|
|
7
|
+
*
|
|
8
|
+
* @module migrate
|
|
9
|
+
*/
|
|
10
|
+
import type { ParsedArgs } from '../parse-args.js';
|
|
11
|
+
export declare const MIGRATE_HELP: string;
|
|
12
|
+
/**
|
|
13
|
+
* Route the `migrate` command to the appropriate subcommand.
|
|
14
|
+
*/
|
|
15
|
+
export declare function handleMigrate(args: ParsedArgs): Promise<void>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import "../../../chunks/chunk.QU3DSPNU.js";
|
|
2
|
+
import { runMigrations } from "./engine.js";
|
|
3
|
+
import { getMigrationUnits } from "./registry.js";
|
|
4
|
+
const PROGRAM_NAME = "green-core-context";
|
|
5
|
+
const MIGRATE_HELP = `
|
|
6
|
+
Detect and apply automated code migrations for Green Design System
|
|
7
|
+
major version upgrades.
|
|
8
|
+
|
|
9
|
+
USAGE
|
|
10
|
+
${PROGRAM_NAME} migrate <subcommand> [options]
|
|
11
|
+
|
|
12
|
+
SUBCOMMANDS
|
|
13
|
+
list List available migration units
|
|
14
|
+
run Detect issues (default: detect only)
|
|
15
|
+
|
|
16
|
+
OPTIONS (for 'run')
|
|
17
|
+
--apply Apply changes (default: detect only)
|
|
18
|
+
--unit <id> Run a specific migration unit by ID
|
|
19
|
+
--major <n> Run all migrations for major version n
|
|
20
|
+
--path <dir> Target directory (default: current dir)
|
|
21
|
+
--json Output results as JSON
|
|
22
|
+
-h, --help Show this help message
|
|
23
|
+
|
|
24
|
+
EXAMPLES
|
|
25
|
+
${PROGRAM_NAME} migrate list
|
|
26
|
+
${PROGRAM_NAME} migrate run --path ./src
|
|
27
|
+
${PROGRAM_NAME} migrate run --unit 3.0.0/my-migration --apply
|
|
28
|
+
${PROGRAM_NAME} migrate run --major 3 --json
|
|
29
|
+
`.trim();
|
|
30
|
+
function runList(args) {
|
|
31
|
+
return runListAsync(args);
|
|
32
|
+
}
|
|
33
|
+
async function runListAsync(args) {
|
|
34
|
+
const majorFlag = args.flags["major"];
|
|
35
|
+
const jsonFlag = args.flags["json"];
|
|
36
|
+
const filterMajor = majorFlag !== void 0 && majorFlag !== true ? Number(majorFlag) : void 0;
|
|
37
|
+
if (filterMajor !== void 0 && Number.isNaN(filterMajor)) {
|
|
38
|
+
process.stderr.write("Error: --major must be a number.\n");
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const resolved = await getMigrationUnits({ major: filterMajor });
|
|
43
|
+
if (jsonFlag) {
|
|
44
|
+
const json = resolved.map((r) => ({
|
|
45
|
+
id: r.unit.id,
|
|
46
|
+
major: r.major,
|
|
47
|
+
summary: r.unit.summary,
|
|
48
|
+
safe: r.unit.safe,
|
|
49
|
+
fileExtensions: r.unit.fileExtensions,
|
|
50
|
+
exampleBefore: r.unit.exampleBefore,
|
|
51
|
+
exampleAfter: r.unit.exampleAfter
|
|
52
|
+
}));
|
|
53
|
+
process.stdout.write(JSON.stringify(json, null, 2) + "\n");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (resolved.length === 0) {
|
|
57
|
+
process.stdout.write("No migration units available.\n");
|
|
58
|
+
if (filterMajor !== void 0) {
|
|
59
|
+
process.stdout.write(`(filtered by --major ${filterMajor})
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write("Available migrations:\n\n");
|
|
65
|
+
const idWidth = Math.max(4, ...resolved.map((r) => r.unit.id.length));
|
|
66
|
+
const header = ` ${"ID".padEnd(idWidth)} Major Safe Summary`;
|
|
67
|
+
const separator = ` ${"\u2500".repeat(idWidth)} \u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 ${"\u2500".repeat(40)}`;
|
|
68
|
+
process.stdout.write(header + "\n");
|
|
69
|
+
process.stdout.write(separator + "\n");
|
|
70
|
+
for (const r of resolved) {
|
|
71
|
+
const safe = r.unit.safe ? "yes " : "no ";
|
|
72
|
+
const line = ` ${r.unit.id.padEnd(idWidth)} ${String(r.major).padEnd(5)} ${safe} ${r.unit.summary}`;
|
|
73
|
+
process.stdout.write(line + "\n");
|
|
74
|
+
}
|
|
75
|
+
process.stdout.write(`
|
|
76
|
+
${resolved.length} migration(s) available.
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
async function runRun(args) {
|
|
80
|
+
const applyFlag = args.flags["apply"] === true;
|
|
81
|
+
const jsonFlag = args.flags["json"] === true;
|
|
82
|
+
const pathFlag = args.flags["path"];
|
|
83
|
+
const unitFlag = args.flags["unit"];
|
|
84
|
+
const majorFlag = args.flags["major"];
|
|
85
|
+
const targetPath = typeof pathFlag === "string" ? pathFlag : process.cwd();
|
|
86
|
+
const filterMajor = majorFlag !== void 0 && majorFlag !== true ? Number(majorFlag) : void 0;
|
|
87
|
+
if (filterMajor !== void 0 && Number.isNaN(filterMajor)) {
|
|
88
|
+
process.stderr.write("Error: --major must be a number.\n");
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const filterId = typeof unitFlag === "string" ? unitFlag : void 0;
|
|
93
|
+
const resolved = await getMigrationUnits({ major: filterMajor, id: filterId });
|
|
94
|
+
if (resolved.length === 0) {
|
|
95
|
+
if (filterId) {
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
`Error: No migration unit found with ID '${filterId}'.
|
|
98
|
+
`
|
|
99
|
+
);
|
|
100
|
+
} else if (filterMajor !== void 0) {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
`Error: No migration units found for major version ${filterMajor}.
|
|
103
|
+
`
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
process.stderr.write("No migration units are registered.\n");
|
|
107
|
+
}
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const mode = applyFlag ? "apply" : "detect";
|
|
112
|
+
if (!jsonFlag) {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`Running ${resolved.length} migration(s) in ${mode} mode...
|
|
115
|
+
|
|
116
|
+
`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const results = await runMigrations({
|
|
120
|
+
basePath: targetPath,
|
|
121
|
+
units: resolved.map((r) => r.unit),
|
|
122
|
+
apply: applyFlag
|
|
123
|
+
});
|
|
124
|
+
if (jsonFlag) {
|
|
125
|
+
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
formatTextResults(results, applyFlag);
|
|
129
|
+
}
|
|
130
|
+
function formatTextResults(results, applied) {
|
|
131
|
+
let totalMatches = 0;
|
|
132
|
+
let totalModified = 0;
|
|
133
|
+
let totalManual = 0;
|
|
134
|
+
let totalScanned = 0;
|
|
135
|
+
for (const result of results) {
|
|
136
|
+
process.stdout.write(`\u2500\u2500 ${result.unitId} \u2500\u2500
|
|
137
|
+
`);
|
|
138
|
+
process.stdout.write(`${result.summary}
|
|
139
|
+
|
|
140
|
+
`);
|
|
141
|
+
totalScanned += result.filesScanned;
|
|
142
|
+
totalMatches += result.matches.length;
|
|
143
|
+
totalModified += result.filesModified;
|
|
144
|
+
totalManual += result.manualReview.length;
|
|
145
|
+
if (result.matches.length === 0) {
|
|
146
|
+
process.stdout.write(" No issues found.\n\n");
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (applied) {
|
|
150
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
151
|
+
for (const m of result.matches) {
|
|
152
|
+
byFile.set(m.file, (byFile.get(m.file) ?? 0) + 1);
|
|
153
|
+
}
|
|
154
|
+
for (const [file, count] of byFile) {
|
|
155
|
+
const prefix = result.manualReview.some((m) => m.file === file) ? "!" : "\u2713";
|
|
156
|
+
process.stdout.write(
|
|
157
|
+
` ${prefix} ${file} (${count} change${count > 1 ? "s" : ""})
|
|
158
|
+
`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
for (const m of result.matches) {
|
|
163
|
+
const marker = m.manualReview ? " [manual]" : "";
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
` ${m.file}:${m.line}${marker}
|
|
166
|
+
${m.message}
|
|
167
|
+
|
|
168
|
+
`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
process.stdout.write(
|
|
173
|
+
` ${result.matches.length} issue(s) in ${filesWithMatches(result)} file(s) (scanned ${result.filesScanned})
|
|
174
|
+
|
|
175
|
+
`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
process.stdout.write("\u2500\u2500 Summary \u2500\u2500\n");
|
|
179
|
+
process.stdout.write(`Scanned: ${totalScanned} files
|
|
180
|
+
`);
|
|
181
|
+
process.stdout.write(`Found: ${totalMatches} issue(s)
|
|
182
|
+
`);
|
|
183
|
+
if (applied) {
|
|
184
|
+
process.stdout.write(`Applied: ${totalModified} file(s) modified
|
|
185
|
+
`);
|
|
186
|
+
} else if (totalMatches > 0) {
|
|
187
|
+
process.stdout.write(`
|
|
188
|
+
Run with --apply to fix automatically.
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
if (totalManual > 0) {
|
|
192
|
+
process.stdout.write(
|
|
193
|
+
`Manual: ${totalManual} issue(s) require manual review
|
|
194
|
+
`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function filesWithMatches(result) {
|
|
199
|
+
return new Set(result.matches.map((m) => m.file)).size;
|
|
200
|
+
}
|
|
201
|
+
async function handleMigrate(args) {
|
|
202
|
+
if (args.flags["h"] || args.flags["help"]) {
|
|
203
|
+
process.stdout.write(MIGRATE_HELP + "\n");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const subcommand = args.positional[0];
|
|
207
|
+
switch (subcommand) {
|
|
208
|
+
case "list":
|
|
209
|
+
await runList(args);
|
|
210
|
+
break;
|
|
211
|
+
case "run":
|
|
212
|
+
await runRun(args);
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
if (subcommand) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`Error: Unknown migrate subcommand '${subcommand}'.
|
|
218
|
+
|
|
219
|
+
`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write(MIGRATE_HELP + "\n");
|
|
223
|
+
if (!subcommand) {
|
|
224
|
+
} else {
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export {
|
|
230
|
+
MIGRATE_HELP,
|
|
231
|
+
handleMigrate
|
|
232
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration unit registry — auto-discovery from the file system.
|
|
3
|
+
*
|
|
4
|
+
* Units are discovered by scanning the `units/` directory for numbered
|
|
5
|
+
* subdirectories (each representing a major version). Every `.ts` / `.js`
|
|
6
|
+
* file inside a major directory that exports a `unit` constant is
|
|
7
|
+
* registered automatically.
|
|
8
|
+
*
|
|
9
|
+
* units/
|
|
10
|
+
* 0/ ← major 0 (examples / disabled units)
|
|
11
|
+
* example.ts
|
|
12
|
+
* 3/ ← major 3
|
|
13
|
+
* remove-dropdown-value-generic.ts
|
|
14
|
+
*
|
|
15
|
+
* @module migrate/registry
|
|
16
|
+
*/
|
|
17
|
+
import type { ResolvedMigrationUnit } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Discover migration units from the `units/<major>/` directory structure.
|
|
20
|
+
*
|
|
21
|
+
* @param basePath - Root `units/` directory to scan. Defaults to the
|
|
22
|
+
* co-located `./units` directory next to this module.
|
|
23
|
+
* @returns All discovered units, sorted by major then ID.
|
|
24
|
+
*/
|
|
25
|
+
export declare function discoverMigrationUnits(basePath?: string): Promise<ResolvedMigrationUnit[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Get migration units matching optional filter criteria.
|
|
28
|
+
*
|
|
29
|
+
* By default only enabled units are returned. Set `includeDisabled: true`
|
|
30
|
+
* to include disabled units (useful for tooling / debugging).
|
|
31
|
+
*
|
|
32
|
+
* @param options.major - Filter by target major version
|
|
33
|
+
* @param options.id - Filter by exact unit ID
|
|
34
|
+
* @param options.includeDisabled - Include units with `enabled: false`
|
|
35
|
+
* @param options.basePath - Override the units directory (for tests)
|
|
36
|
+
* @returns Filtered array of resolved migration units
|
|
37
|
+
*/
|
|
38
|
+
export declare function getMigrationUnits(options?: {
|
|
39
|
+
major?: number;
|
|
40
|
+
id?: string;
|
|
41
|
+
includeDisabled?: boolean;
|
|
42
|
+
basePath?: string;
|
|
43
|
+
}): Promise<ResolvedMigrationUnit[]>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import "../../../chunks/chunk.QU3DSPNU.js";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DEFAULT_UNITS_DIR = join(__dirname, "units");
|
|
7
|
+
function isUnitFile(name) {
|
|
8
|
+
if (name.endsWith(".d.ts")) return false;
|
|
9
|
+
if (name.endsWith(".test.ts") || name.endsWith(".test.js")) return false;
|
|
10
|
+
if (name.endsWith(".spec.ts") || name.endsWith(".spec.js")) return false;
|
|
11
|
+
return name.endsWith(".ts") || name.endsWith(".js");
|
|
12
|
+
}
|
|
13
|
+
async function discoverMigrationUnits(basePath) {
|
|
14
|
+
const unitsDir = basePath ?? DEFAULT_UNITS_DIR;
|
|
15
|
+
const results = [];
|
|
16
|
+
let topEntries;
|
|
17
|
+
try {
|
|
18
|
+
topEntries = await readdir(unitsDir, { withFileTypes: true });
|
|
19
|
+
} catch {
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of topEntries) {
|
|
23
|
+
if (!entry.isDirectory()) continue;
|
|
24
|
+
const major = Number(entry.name);
|
|
25
|
+
if (Number.isNaN(major)) continue;
|
|
26
|
+
const majorDir = join(unitsDir, entry.name);
|
|
27
|
+
let files;
|
|
28
|
+
try {
|
|
29
|
+
files = await readdir(majorDir, { withFileTypes: true });
|
|
30
|
+
} catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (!file.isFile() || !isUnitFile(file.name)) continue;
|
|
35
|
+
try {
|
|
36
|
+
const moduleUrl = pathToFileURL(join(majorDir, file.name)).href;
|
|
37
|
+
const mod = await import(moduleUrl);
|
|
38
|
+
if (mod.unit && typeof mod.unit === "object" && "id" in mod.unit) {
|
|
39
|
+
results.push({ major, unit: mod.unit });
|
|
40
|
+
} else {
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
`Warning: ${entry.name}/${file.name} does not export a 'unit' constant \u2014 skipped.
|
|
43
|
+
`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
`Warning: Failed to import ${entry.name}/${file.name}: ${err instanceof Error ? err.message : String(err)}
|
|
49
|
+
`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
results.sort(
|
|
55
|
+
(a, b) => a.major - b.major || a.unit.id.localeCompare(b.unit.id)
|
|
56
|
+
);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
async function getMigrationUnits(options) {
|
|
60
|
+
let units = await discoverMigrationUnits(options?.basePath);
|
|
61
|
+
if (!options?.includeDisabled) {
|
|
62
|
+
units = units.filter((r) => r.unit.enabled);
|
|
63
|
+
}
|
|
64
|
+
if (options?.major !== void 0) {
|
|
65
|
+
units = units.filter((r) => r.major === options.major);
|
|
66
|
+
}
|
|
67
|
+
if (options?.id !== void 0) {
|
|
68
|
+
units = units.filter((r) => r.unit.id === options.id);
|
|
69
|
+
}
|
|
70
|
+
return units;
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
discoverMigrationUnits,
|
|
74
|
+
getMigrationUnits
|
|
75
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Green Design System migration engine.
|
|
3
|
+
*
|
|
4
|
+
* Migration units are versioned, self-contained modules that detect and
|
|
5
|
+
* optionally fix breaking changes in consumer codebases. Each unit targets
|
|
6
|
+
* a specific major version upgrade and encapsulates:
|
|
7
|
+
* - Detection logic (regex-based, zero external dependencies)
|
|
8
|
+
* - Transformation logic (string replacement or manual-review flagging)
|
|
9
|
+
*
|
|
10
|
+
* ## Directory layout
|
|
11
|
+
*
|
|
12
|
+
* Units are organized by major version in the file system:
|
|
13
|
+
*
|
|
14
|
+
* units/
|
|
15
|
+
* 3/
|
|
16
|
+
* remove-dropdown-value-generic.ts
|
|
17
|
+
* rename-datepicker-slot.ts
|
|
18
|
+
* 4/
|
|
19
|
+
* some-future-migration.ts
|
|
20
|
+
*
|
|
21
|
+
* The major version is derived from the directory name — not from a field
|
|
22
|
+
* on the unit itself. Units that should not appear in listings or be run
|
|
23
|
+
* can set `enabled: false`.
|
|
24
|
+
*
|
|
25
|
+
* @module migrate/types
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* A single match found by a migration unit's detect function.
|
|
29
|
+
*/
|
|
30
|
+
export interface MigrationMatch {
|
|
31
|
+
/** File path (made relative by the engine after detect returns) */
|
|
32
|
+
file: string;
|
|
33
|
+
/** 1-based line number where the match was found */
|
|
34
|
+
line: number;
|
|
35
|
+
/** The matched text snippet (the relevant line fragment) */
|
|
36
|
+
match: string;
|
|
37
|
+
/** Human-readable description of what needs to change */
|
|
38
|
+
message: string;
|
|
39
|
+
/**
|
|
40
|
+
* If true, this match cannot be auto-fixed and requires manual
|
|
41
|
+
* intervention. The engine will report it separately.
|
|
42
|
+
*/
|
|
43
|
+
manualReview?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Aggregated result from running a single migration unit across a project.
|
|
47
|
+
*/
|
|
48
|
+
export interface MigrationResult {
|
|
49
|
+
/** The migration unit ID */
|
|
50
|
+
unitId: string;
|
|
51
|
+
/** Short summary of the migration */
|
|
52
|
+
summary: string;
|
|
53
|
+
/** Number of files scanned */
|
|
54
|
+
filesScanned: number;
|
|
55
|
+
/** All matches found */
|
|
56
|
+
matches: MigrationMatch[];
|
|
57
|
+
/** Number of files that were modified (0 in detect-only mode) */
|
|
58
|
+
filesModified: number;
|
|
59
|
+
/** Matches that require manual review (subset of matches) */
|
|
60
|
+
manualReview: MigrationMatch[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A migration unit describes a single automated migration step.
|
|
64
|
+
*
|
|
65
|
+
* ## Creating a new unit
|
|
66
|
+
*
|
|
67
|
+
* 1. Create a new file under `migrate/units/<major>/`
|
|
68
|
+
* (e.g. `units/3/remove-dropdown-value-generic.ts`)
|
|
69
|
+
* 2. Export a `MigrationUnit` object named `unit`
|
|
70
|
+
* 3. The registry auto-discovers it from the directory structure
|
|
71
|
+
*
|
|
72
|
+
* See `migrate/units/0/example.ts` for a documented template.
|
|
73
|
+
*/
|
|
74
|
+
export interface MigrationUnit {
|
|
75
|
+
/**
|
|
76
|
+
* Unique identifier (kebab-case slug).
|
|
77
|
+
* Example: `remove-dropdown-value-generic`
|
|
78
|
+
*/
|
|
79
|
+
id: string;
|
|
80
|
+
/**
|
|
81
|
+
* Whether this unit should be listed and available for execution.
|
|
82
|
+
* Set to `false` for drafts, examples, or temporarily disabled units.
|
|
83
|
+
*/
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
/** Short human-readable summary (one line) */
|
|
86
|
+
summary: string;
|
|
87
|
+
/**
|
|
88
|
+
* Code example showing the pattern **before** the migration is applied.
|
|
89
|
+
* Primarily serves as context for coding models applying changes manually.
|
|
90
|
+
*/
|
|
91
|
+
exampleBefore: string;
|
|
92
|
+
/**
|
|
93
|
+
* Code example showing the expected result **after** migration.
|
|
94
|
+
* Primarily serves as context for coding models applying changes manually.
|
|
95
|
+
*/
|
|
96
|
+
exampleAfter: string;
|
|
97
|
+
/**
|
|
98
|
+
* File extensions to scan, including the leading dot.
|
|
99
|
+
* Example: `['.ts', '.tsx', '.html']`
|
|
100
|
+
*/
|
|
101
|
+
fileExtensions: string[];
|
|
102
|
+
/**
|
|
103
|
+
* Whether this migration is safe to auto-apply without manual review.
|
|
104
|
+
* When `true`, `apply()` is guaranteed to produce correct output for
|
|
105
|
+
* every match that `detect()` finds. When `false`, some matches may
|
|
106
|
+
* require manual intervention.
|
|
107
|
+
*/
|
|
108
|
+
safe: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Detect occurrences of the deprecated pattern in a file.
|
|
111
|
+
*
|
|
112
|
+
* @param filePath - Absolute path to the file being scanned
|
|
113
|
+
* @param content - The file's full text content (UTF-8)
|
|
114
|
+
* @returns Array of matches found in the file (may be empty)
|
|
115
|
+
*/
|
|
116
|
+
detect(filePath: string, content: string): MigrationMatch[];
|
|
117
|
+
/**
|
|
118
|
+
* Apply the migration transformation to a file's content.
|
|
119
|
+
*
|
|
120
|
+
* @param filePath - Absolute path to the file being transformed
|
|
121
|
+
* @param content - The file's full text content (UTF-8)
|
|
122
|
+
* @returns The transformed content, or `null` if no changes were made
|
|
123
|
+
*/
|
|
124
|
+
apply(filePath: string, content: string): string | null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A migration unit enriched with metadata derived from the directory
|
|
128
|
+
* structure during auto-discovery. Returned by the registry.
|
|
129
|
+
*/
|
|
130
|
+
export interface ResolvedMigrationUnit {
|
|
131
|
+
/** Major version, derived from the `units/<major>/` directory name */
|
|
132
|
+
major: number;
|
|
133
|
+
/** The migration unit definition */
|
|
134
|
+
unit: MigrationUnit;
|
|
135
|
+
}
|
|
File without changes
|