forgedock 1.26.0 → 1.28.0
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/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +38 -2
- package/bin/engine/telemetry-seam.mjs +41 -6
- package/bin/engine/telemetry-seam.test.mjs +35 -0
- package/bin/engine-cli.mjs +616 -54
- package/bin/engine.mjs +13 -5
- package/bin/forgedock.mjs +169 -39
- package/bin/gc.mjs +580 -0
- package/bin/init-detect.mjs +68 -3
- package/bin/init-enrich.mjs +3 -0
- package/bin/init-repo.mjs +213 -0
- package/bin/journey.mjs +278 -43
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +24 -5
- package/bin/lean/batch.test.mjs +54 -4
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +235 -37
- package/bin/lean/conformance.test.mjs +143 -5
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +178 -16
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +877 -151
- package/bin/lean/lean.test.mjs +1798 -28
- package/bin/lean/ledger.mjs +92 -0
- package/bin/lean/ledger.test.mjs +100 -0
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +321 -19
- package/bin/lean/prompts.mjs +125 -17
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +25 -6
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +124 -0
- package/bin/lean/verify.test.mjs +118 -0
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/adopt.md +3 -0
- package/commands/audit.md +5 -8
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +125 -6
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/review-pr.md +2 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +227 -3
- package/docs/OUTCOME-CODES.md +11 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +321 -0
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +125 -0
- package/scripts/check-devdocs-freshness.test.sh +130 -0
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +16 -5
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +14 -2
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/northstar-docs.test.sh +68 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/review-prepare.sh +11 -9
- package/scripts/review-prepare.test.sh +14 -0
- package/scripts/structure-delta.sh +155 -0
- package/scripts/structure-delta.test.sh +136 -0
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
//
|
|
5
|
+
// symbol-move.mjs: TypeScript twin of scripts/symbol-move.py for refactor:split (forge#4212, #4196). Moves groups of
|
|
6
|
+
// top-level symbols out of one TS module into a sibling directory of modules and leaves a re-export barrel at the old
|
|
7
|
+
// path. ts-morph parses the source and resolves every reference; moved statements are copied byte-for-byte from the
|
|
8
|
+
// source text, never re-printed. No LLM, no network, no ForgeDock dependency: ts-morph is resolved from the --src
|
|
9
|
+
// directory, then the cwd (NODE_PATH is honoured), like `typescript` in scripts/symbol-index.mjs.
|
|
10
|
+
//
|
|
11
|
+
// Usage: node scripts/symbol-move.mjs --src <file.ts|.tsx|.mts|.cts> --plan <plan.json> [--package <name>] [--extend] [--dry-run]
|
|
12
|
+
// node scripts/symbol-move.mjs --probe [--src <file>] (only checks that ts-morph resolves)
|
|
13
|
+
// plan: {"<module>": ["<top-level symbol>", ...], ...}, names as scripts/symbol-index.mjs reports them.
|
|
14
|
+
// --package defaults to <stem>_parts, a directory created next to --src. --extend (a chain step of a large split,
|
|
15
|
+
// forge#4211) adds the plan's modules to an existing package dir (created when absent); a module file that already
|
|
16
|
+
// exists in it refuses. Without --extend an existing package path refuses.
|
|
17
|
+
// Output (stdout, one JSON object):
|
|
18
|
+
// ok: {"status":"ok","package":"<package dir, --src's dir joined with the name>","modules":{module:[symbols]},"shim":"<src path>"}
|
|
19
|
+
// probe: {"status":"ok","tool":"ts-morph"}
|
|
20
|
+
// refused: {"status":"refused","reason":"..."}
|
|
21
|
+
// Result: <package>/<module><src ext> per plan entry, each holding, in order: the source's import declarations its
|
|
22
|
+
// symbols use (pruned to the used names; relative specifiers re-based to the package dir), `import { ... } from
|
|
23
|
+
// "./<sibling>"` for symbols moved to sibling modules (`type` on interface/type-alias names), then the moved
|
|
24
|
+
// statements verbatim (leading comments and `export` keywords included) in source order. --src keeps every unmoved
|
|
25
|
+
// statement verbatim and gains, at the position of the first moved statement, `export { ... } from "./<pkg>/<module>"`
|
|
26
|
+
// for every moved name that was exported and `import { ... } from ...` for moved names kept code still uses. New
|
|
27
|
+
// relative specifiers mirror the source's relative-import extension style (.ts, .js or none; the source's own
|
|
28
|
+
// extension when it has no relative import). Every changed line is a moved block or an import/`export ... from`
|
|
29
|
+
// line, so scripts/refactor-verify.sh can prove the move.
|
|
30
|
+
// Refuses (no file touched) when ts-morph does not resolve, the source does not parse, or the plan is invalid:
|
|
31
|
+
// unknown or non-top-level symbol, a symbol listed twice, a statement split across modules, a bad or colliding
|
|
32
|
+
// module name, an existing package path (without --extend) or module file, a moved symbol that needs a name declared
|
|
33
|
+
// in --src that stays, a moved non-exported symbol used by kept code or another new module (exporting it would be a
|
|
34
|
+
// logic change), `export default` among the moved statements, module-identity code in moved statements
|
|
35
|
+
// (import.meta, __dirname, __filename, a relative require()/import()), or an import cycle between the new modules.
|
|
36
|
+
// Writes are atomic: every file is built in memory, written to a temp file and renamed in, the barrel last.
|
|
37
|
+
// Exit: 0 ok | 2 usage or missing file | 3 refused.
|
|
38
|
+
|
|
39
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
40
|
+
import { createRequire } from "node:module";
|
|
41
|
+
import path from "node:path";
|
|
42
|
+
|
|
43
|
+
const TS_EXT = [".ts", ".tsx", ".mts", ".cts"];
|
|
44
|
+
const JS_OF = { ".ts": ".js", ".tsx": ".js", ".mts": ".mjs", ".cts": ".cjs" };
|
|
45
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
46
|
+
|
|
47
|
+
class Refused extends Error {}
|
|
48
|
+
const emit = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
49
|
+
const usage = () => {
|
|
50
|
+
process.stderr.write("usage: symbol-move.mjs --src <file.ts> --plan <plan.json> [--package <name>] [--extend] [--dry-run]\n"
|
|
51
|
+
+ " symbol-move.mjs --probe [--src <file>]\n");
|
|
52
|
+
return 2;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const o = { package: null, dryRun: false, extend: false, probe: false };
|
|
57
|
+
for (let i = 0; i < argv.length; i++) {
|
|
58
|
+
const a = argv[i];
|
|
59
|
+
if (a === "--dry-run") o.dryRun = true;
|
|
60
|
+
else if (a === "--extend") o.extend = true;
|
|
61
|
+
else if (a === "--probe") o.probe = true;
|
|
62
|
+
else if (["--src", "--plan", "--package"].includes(a) && i + 1 < argv.length) o[a.slice(2)] = argv[++i];
|
|
63
|
+
else return null;
|
|
64
|
+
}
|
|
65
|
+
return o.probe || (o.src && o.plan) ? o : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** ts-morph from the --src directory, then the cwd; null when neither resolves it. */
|
|
69
|
+
function loadTsMorph(src) {
|
|
70
|
+
const bases = [...(src ? [path.resolve(path.dirname(src))] : []), process.cwd()];
|
|
71
|
+
for (const b of bases) {
|
|
72
|
+
try { const m = createRequire(path.join(b, "noop.js"))("ts-morph"); if (m?.Project && m?.ts) return m; } catch { /* try next */ }
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadPlan(file) {
|
|
78
|
+
let plan;
|
|
79
|
+
try { plan = JSON.parse(readFileSync(file, "utf8")); } catch (e) { throw new Refused(`plan is not readable JSON: ${e.message}`); }
|
|
80
|
+
if (!plan || typeof plan !== "object" || Array.isArray(plan) || !Object.keys(plan).length) throw new Refused("plan must be a non-empty object {module: [symbols]}");
|
|
81
|
+
const lower = new Map();
|
|
82
|
+
for (const [mod, names] of Object.entries(plan)) {
|
|
83
|
+
if (!Array.isArray(names) || !names.length || !names.every((n) => typeof n === "string" && n)) throw new Refused(`plan entry ${JSON.stringify(mod)} must be a non-empty list of symbol names`);
|
|
84
|
+
if (!IDENT.test(mod) || mod === "index") throw new Refused(`module name ${JSON.stringify(mod)} is not a valid identifier`);
|
|
85
|
+
const k = mod.toLowerCase();
|
|
86
|
+
if (lower.has(k)) throw new Refused(`module names ${JSON.stringify(lower.get(k))} and ${JSON.stringify(mod)} collide on a case-insensitive filesystem`);
|
|
87
|
+
lower.set(k, mod);
|
|
88
|
+
}
|
|
89
|
+
return plan;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const posix = (p) => p.split(path.sep).join("/");
|
|
93
|
+
const dotted = (p) => (p.startsWith(".") ? p : `./${p}`);
|
|
94
|
+
|
|
95
|
+
/** Builds every output file in memory. Returns {files: [[absPath, text]], barrel: [absPath, text], pkgDir}. */
|
|
96
|
+
function build(tm, src, plan, pkgName, extend) {
|
|
97
|
+
const { ts } = tm;
|
|
98
|
+
const text = readFileSync(src, "utf8");
|
|
99
|
+
const ext = path.extname(src).toLowerCase();
|
|
100
|
+
const project = new tm.Project({
|
|
101
|
+
useInMemoryFileSystem: true,
|
|
102
|
+
compilerOptions: { noLib: true, noResolve: true, allowJs: false, target: ts.ScriptTarget.Latest, jsx: ts.JsxEmit.Preserve },
|
|
103
|
+
});
|
|
104
|
+
const sf = project.createSourceFile(`/src/${path.basename(src)}`, text).compilerNode;
|
|
105
|
+
if (sf.parseDiagnostics?.length) {
|
|
106
|
+
const d = sf.parseDiagnostics[0];
|
|
107
|
+
throw new Refused(`source does not parse: line ${sf.getLineAndCharacterOfPosition(d.start).line + 1}: ${ts.flattenDiagnosticMessageText(d.messageText, " ")}`);
|
|
108
|
+
}
|
|
109
|
+
const checker = project.getTypeChecker().compilerObject;
|
|
110
|
+
const stmts = [...sf.statements];
|
|
111
|
+
const K = ts.SyntaxKind;
|
|
112
|
+
const mods = (n) => (ts.canHaveModifiers?.(n) ? ts.getModifiers(n) : n.modifiers) ?? [];
|
|
113
|
+
const hasMod = (n, k) => mods(n).some((m) => m.kind === k);
|
|
114
|
+
|
|
115
|
+
// ---- top-level declarations: statement -> names it binds, name -> statements ----
|
|
116
|
+
const bound = new Map(), byName = new Map();
|
|
117
|
+
const bindingNames = (b, out) => {
|
|
118
|
+
if (ts.isIdentifier(b)) out.push(b.text);
|
|
119
|
+
else for (const el of b.elements ?? []) if (!ts.isOmittedExpression(el)) bindingNames(el.name, out);
|
|
120
|
+
return out;
|
|
121
|
+
};
|
|
122
|
+
for (const st of stmts) {
|
|
123
|
+
let names = null;
|
|
124
|
+
if ((ts.isFunctionDeclaration(st) || ts.isClassDeclaration(st)) && st.name) names = [st.name.text];
|
|
125
|
+
else if (ts.isInterfaceDeclaration(st) || ts.isTypeAliasDeclaration(st) || ts.isEnumDeclaration(st)) names = [st.name.text];
|
|
126
|
+
else if (ts.isModuleDeclaration(st) && ts.isIdentifier(st.name)) names = [st.name.text];
|
|
127
|
+
else if (ts.isVariableStatement(st)) names = st.declarationList.declarations.flatMap((d) => bindingNames(d.name, []));
|
|
128
|
+
if (!names) continue;
|
|
129
|
+
bound.set(st, names);
|
|
130
|
+
for (const n of names) { if (!byName.has(n)) byName.set(n, []); byName.get(n).push(st); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- plan -> module per name and per statement ----
|
|
134
|
+
const modOf = new Map();
|
|
135
|
+
for (const [mod, names] of Object.entries(plan)) {
|
|
136
|
+
for (const n of names) {
|
|
137
|
+
if (modOf.has(n)) throw new Refused(`symbol ${JSON.stringify(n)} is listed twice (${modOf.get(n)}, ${mod})`);
|
|
138
|
+
if (n.includes(".")) throw new Refused(`${JSON.stringify(n)} is not a top-level symbol; only top-level symbols move`);
|
|
139
|
+
if (!byName.has(n)) throw new Refused(`unknown symbol ${JSON.stringify(n)}: not a top-level declaration in ${src}`);
|
|
140
|
+
modOf.set(n, mod);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const stmtMod = new Map();
|
|
144
|
+
for (const [st, names] of bound) {
|
|
145
|
+
const where = new Set(names.map((n) => modOf.get(n) ?? null));
|
|
146
|
+
if (where.size > 1) {
|
|
147
|
+
const other = names.find((n) => (modOf.get(n) ?? null) !== (modOf.get(names[0]) ?? null));
|
|
148
|
+
throw new Refused(`statement at line ${sf.getLineAndCharacterOfPosition(st.getStart(sf)).line + 1} binds ${JSON.stringify(names[0])} and ${JSON.stringify(other)}, which the plan does not move together`);
|
|
149
|
+
}
|
|
150
|
+
const m = [...where][0];
|
|
151
|
+
if (m !== null) stmtMod.set(st, m);
|
|
152
|
+
}
|
|
153
|
+
for (const [n, sts] of byName) {
|
|
154
|
+
const where = new Set(sts.map((s) => stmtMod.get(s) ?? `${path.basename(src)} (kept)`));
|
|
155
|
+
if (where.size > 1) throw new Refused(`name ${JSON.stringify(n)} is bound by statements going to different places (${[...where].join(", ")})`);
|
|
156
|
+
}
|
|
157
|
+
const moved = stmts.filter((s) => stmtMod.has(s));
|
|
158
|
+
for (const st of moved) {
|
|
159
|
+
if (hasMod(st, K.DefaultKeyword)) throw new Refused(`moved symbol ${bound.get(st)[0]} is an \`export default\`; re-exporting it would change the barrel's shape`);
|
|
160
|
+
}
|
|
161
|
+
const exported = (n) => byName.get(n).some((s) => hasMod(s, K.ExportKeyword) && !hasMod(s, K.DefaultKeyword));
|
|
162
|
+
const typeOnly = (n) => byName.get(n).every((s) => ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s));
|
|
163
|
+
const line = (n) => sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1;
|
|
164
|
+
|
|
165
|
+
// ---- reference resolution ----
|
|
166
|
+
const topOf = (n) => { while (n && n.parent && n.parent !== sf) n = n.parent; return n && n.parent === sf ? n : null; };
|
|
167
|
+
const symOf = (id) => {
|
|
168
|
+
const p = id.parent;
|
|
169
|
+
if (p && ts.isShorthandPropertyAssignment(p) && p.name === id) return checker.getShorthandAssignmentValueSymbol(p);
|
|
170
|
+
if (p && ts.isExportSpecifier(p)) {
|
|
171
|
+
if (p.parent?.parent?.moduleSpecifier || id !== (p.propertyName ?? p.name)) return null;
|
|
172
|
+
return checker.getExportSpecifierLocalTargetSymbol(p);
|
|
173
|
+
}
|
|
174
|
+
return checker.getSymbolAtLocation(id);
|
|
175
|
+
};
|
|
176
|
+
/** Every reference in `root` to a top-level name or an import binding: {name} | {imp: decl node, top}. */
|
|
177
|
+
const refs = (root) => {
|
|
178
|
+
const out = [];
|
|
179
|
+
const visit = (n) => {
|
|
180
|
+
if (ts.isIdentifier(n)) {
|
|
181
|
+
const sym = symOf(n);
|
|
182
|
+
for (const d of sym?.declarations ?? []) {
|
|
183
|
+
const top = topOf(d);
|
|
184
|
+
if (!top) continue;
|
|
185
|
+
if (ts.isImportDeclaration(top) || ts.isImportEqualsDeclaration(top)) { out.push({ imp: d, top, node: n }); break; }
|
|
186
|
+
const isDecl = d === top || (ts.isVariableDeclaration(d) && d.parent?.parent === top) || (ts.isBindingElement(d) && topOf(d) === top && ts.isVariableStatement(top));
|
|
187
|
+
if (isDecl && bound.has(top) && bound.get(top).includes(sym.name)) { out.push({ name: sym.name, node: n }); break; }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
ts.forEachChild(n, visit);
|
|
191
|
+
};
|
|
192
|
+
visit(root);
|
|
193
|
+
return out;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// ---- moved code: module identity, needs, sibling edges ----
|
|
197
|
+
const isRelSpec = (a) => a && ts.isStringLiteralLike(a) && a.text.startsWith(".");
|
|
198
|
+
const needImports = new Map(Object.keys(plan).map((m) => [m, new Map()])); // mod -> import top -> Set(decl)
|
|
199
|
+
const sibling = new Map(Object.keys(plan).map((m) => [m, new Map()])); // mod -> other mod -> Set(name)
|
|
200
|
+
for (const st of moved) {
|
|
201
|
+
const mod = stmtMod.get(st), who = bound.get(st)[0];
|
|
202
|
+
const identity = (n) => {
|
|
203
|
+
if (ts.isMetaProperty(n) && n.keywordToken === K.ImportKeyword) throw new Refused(`moved symbol ${who} uses import.meta, which names the module`);
|
|
204
|
+
if (ts.isIdentifier(n) && (n.text === "__dirname" || n.text === "__filename") && !(n.parent && ts.isPropertyAccessExpression(n.parent) && n.parent.name === n)) throw new Refused(`moved symbol ${who} uses module-identity global ${n.text}`);
|
|
205
|
+
if (ts.isCallExpression(n) && (n.expression.kind === K.ImportKeyword || (ts.isIdentifier(n.expression) && n.expression.text === "require")) && isRelSpec(n.arguments[0])) {
|
|
206
|
+
throw new Refused(`moved symbol ${who} loads ${JSON.stringify(n.arguments[0].text)} relative to the module at line ${line(n)}`);
|
|
207
|
+
}
|
|
208
|
+
ts.forEachChild(n, identity);
|
|
209
|
+
};
|
|
210
|
+
identity(st);
|
|
211
|
+
for (const r of refs(st)) {
|
|
212
|
+
if (r.imp) {
|
|
213
|
+
const m = needImports.get(mod);
|
|
214
|
+
if (!m.has(r.top)) m.set(r.top, new Set());
|
|
215
|
+
m.get(r.top).add(r.imp);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const other = modOf.get(r.name);
|
|
219
|
+
if (other === mod) continue;
|
|
220
|
+
if (other === undefined) throw new Refused(`moved symbol ${who} (-> ${mod}) needs ${JSON.stringify(r.name)}, which stays in ${src} (line ${line(r.node)})`);
|
|
221
|
+
if (!exported(r.name)) throw new Refused(`moved symbol ${who} (-> ${mod}) uses ${JSON.stringify(r.name)} (-> ${other}), which is not exported; exporting it would change its declaration`);
|
|
222
|
+
const s = sibling.get(mod);
|
|
223
|
+
if (!s.has(other)) s.set(other, new Set());
|
|
224
|
+
s.get(other).add(r.name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- kept code referencing moved names: the barrel imports them ----
|
|
229
|
+
const keptNeeds = new Map(); // mod -> Set(name)
|
|
230
|
+
for (const st of stmts) {
|
|
231
|
+
if (stmtMod.has(st) || ts.isImportDeclaration(st) || ts.isImportEqualsDeclaration(st) || (ts.isExportDeclaration(st) && st.moduleSpecifier)) continue;
|
|
232
|
+
for (const r of refs(st)) {
|
|
233
|
+
if (!r.name || !modOf.has(r.name)) continue;
|
|
234
|
+
if (!exported(r.name)) throw new Refused(`${JSON.stringify(r.name)} (-> ${modOf.get(r.name)}) is not exported but kept code uses it at line ${line(r.node)}; exporting it would change its declaration`);
|
|
235
|
+
const m = modOf.get(r.name);
|
|
236
|
+
if (!keptNeeds.has(m)) keptNeeds.set(m, new Set());
|
|
237
|
+
keptNeeds.get(m).add(r.name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---- no cycle between the new modules ----
|
|
242
|
+
const state = new Map();
|
|
243
|
+
const dfs = (m, trail) => {
|
|
244
|
+
if (state.get(m) === 2) return;
|
|
245
|
+
if (state.get(m) === 1) throw new Refused(`plan creates an import cycle: ${[...trail.slice(trail.indexOf(m)), m].join(" -> ")}`);
|
|
246
|
+
state.set(m, 1);
|
|
247
|
+
for (const o of [...sibling.get(m).keys()].sort()) dfs(o, [...trail, m]);
|
|
248
|
+
state.set(m, 2);
|
|
249
|
+
};
|
|
250
|
+
for (const m of Object.keys(plan)) dfs(m, []);
|
|
251
|
+
|
|
252
|
+
// ---- package location ----
|
|
253
|
+
const srcDir = path.dirname(src);
|
|
254
|
+
const pkgDir = path.join(srcDir, pkgName);
|
|
255
|
+
if (!extend && existsSync(pkgDir)) throw new Refused(`package path already exists: ${pkgDir}`);
|
|
256
|
+
const files = Object.keys(plan).map((m) => [m, path.join(pkgDir, m + ext)]);
|
|
257
|
+
if (extend) {
|
|
258
|
+
const taken = files.filter(([, f]) => existsSync(f)).map(([m]) => m);
|
|
259
|
+
if (taken.length) throw new Refused(`module already exists in package ${pkgDir}: ${taken.join(", ")}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- specifier style: mirror the source's relative imports ----
|
|
263
|
+
const relSpecs = stmts.filter((s) => (ts.isImportDeclaration(s) || ts.isExportDeclaration(s)) && isRelSpec(s.moduleSpecifier)).map((s) => s.moduleSpecifier.text);
|
|
264
|
+
const first = relSpecs[0];
|
|
265
|
+
const style = first === undefined ? "own" : /\.(c|m)?tsx?$/.test(first) ? "own" : /\.(c|m)?jsx?$/.test(first) ? "js" : "none";
|
|
266
|
+
const suffix = style === "own" ? ext : style === "js" ? JS_OF[ext] : "";
|
|
267
|
+
const quote = /^\s*import[^'"]*(['"])/m.exec(stmts.filter((s) => ts.isImportDeclaration(s)).map((s) => s.getText(sf)).join("\n"))?.[1] ?? "\"";
|
|
268
|
+
const semi = stmts.some((s) => ts.isImportDeclaration(s)) ? stmts.filter((s) => ts.isImportDeclaration(s)).every((s) => s.getText(sf).trimEnd().endsWith(";")) : true;
|
|
269
|
+
const end = semi ? ";" : "";
|
|
270
|
+
const q = (s) => `${quote}${s}${quote}`;
|
|
271
|
+
const rebase = (spec) => (spec.startsWith(".") ? dotted(posix(path.relative(pkgDir, path.join(srcDir, spec)))) : spec);
|
|
272
|
+
const named = (names) => `{ ${names.map((n) => (typeOnly(n) ? `type ${n}` : n)).join(", ")} }`;
|
|
273
|
+
const srcOrder = (names) => [...names].sort((a, b) => byName.get(a)[0].pos - byName.get(b)[0].pos);
|
|
274
|
+
|
|
275
|
+
/** One source import declaration pruned to the used bindings, with its specifier re-based. */
|
|
276
|
+
const importText = (top, used) => {
|
|
277
|
+
if (ts.isImportEqualsDeclaration(top)) {
|
|
278
|
+
const t = top.getText(sf), ref = top.moduleReference;
|
|
279
|
+
if (ts.isExternalModuleReference(ref) && isRelSpec(ref.expression)) {
|
|
280
|
+
const s = ref.expression;
|
|
281
|
+
return t.slice(0, s.getStart(sf) - top.getStart(sf)) + q(rebase(s.text)) + t.slice(s.getEnd() - top.getStart(sf));
|
|
282
|
+
}
|
|
283
|
+
return t;
|
|
284
|
+
}
|
|
285
|
+
const c = top.importClause, parts = [];
|
|
286
|
+
if (c.name && used.has(c)) parts.push(c.name.text);
|
|
287
|
+
const nb = c.namedBindings;
|
|
288
|
+
if (nb && ts.isNamespaceImport(nb) && used.has(nb)) parts.push(`* as ${nb.name.text}`);
|
|
289
|
+
if (nb && ts.isNamedImports(nb)) {
|
|
290
|
+
const els = nb.elements.filter((e) => used.has(e)).map((e) => e.getText(sf));
|
|
291
|
+
if (els.length) parts.push(`{ ${els.join(", ")} }`);
|
|
292
|
+
}
|
|
293
|
+
const attrs = top.attributes ? ` ${top.attributes.getText(sf)}` : "";
|
|
294
|
+
return `import ${c.isTypeOnly ? "type " : ""}${parts.join(", ")} from ${q(rebase(top.moduleSpecifier.text))}${attrs}${end}`;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ---- statement spans: leading comments move with the statement; the file header stays ----
|
|
298
|
+
const span = (st) => {
|
|
299
|
+
let from = st.getFullStart();
|
|
300
|
+
if (st === stmts[0]) {
|
|
301
|
+
const trivia = text.slice(from, st.getStart(sf)), cut = trivia.lastIndexOf("\n\n");
|
|
302
|
+
if (cut >= 0) from += cut + 2;
|
|
303
|
+
}
|
|
304
|
+
return [from, st.getEnd()];
|
|
305
|
+
};
|
|
306
|
+
const body = (st) => text.slice(...span(st)).replace(/^(?:[ \t]*\r?\n)+/, "");
|
|
307
|
+
|
|
308
|
+
const out = [];
|
|
309
|
+
for (const [mod, file] of files) {
|
|
310
|
+
const lines = [];
|
|
311
|
+
for (const st of stmts) if (needImports.get(mod).has(st)) lines.push(importText(st, needImports.get(mod).get(st)));
|
|
312
|
+
for (const [other, names] of [...sibling.get(mod)].sort((a, b) => Object.keys(plan).indexOf(a[0]) - Object.keys(plan).indexOf(b[0]))) {
|
|
313
|
+
lines.push(`import ${named(srcOrder(names))} from ${q(`./${other}${suffix}`)}${end}`);
|
|
314
|
+
}
|
|
315
|
+
const blocks = moved.filter((s) => stmtMod.get(s) === mod).map(body);
|
|
316
|
+
out.push([file, `${lines.length ? `${lines.join("\n")}\n\n` : ""}${blocks.join("\n\n")}\n`]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---- the barrel: --src with the moved spans cut and import/re-export lines at the first one ----
|
|
320
|
+
const barrel = [];
|
|
321
|
+
for (const mod of Object.keys(plan)) {
|
|
322
|
+
const spec = q(`./${pkgName}/${mod}${suffix}`);
|
|
323
|
+
const kept = keptNeeds.get(mod);
|
|
324
|
+
if (kept?.size) barrel.push(`import ${named(srcOrder(kept))} from ${spec}${end}`);
|
|
325
|
+
const pub = srcOrder(plan[mod].filter(exported));
|
|
326
|
+
if (pub.length) barrel.push(`export ${named(pub)} from ${spec}${end}`);
|
|
327
|
+
}
|
|
328
|
+
const spans = moved.map(span);
|
|
329
|
+
let shim = "", at = 0;
|
|
330
|
+
spans.forEach(([a, b], i) => {
|
|
331
|
+
shim += text.slice(at, a);
|
|
332
|
+
if (i === 0 && barrel.length) shim += `${a > 0 && text[a - 1] !== "\n" ? "\n" : ""}${barrel.join("\n")}${text[b] === "\n" || b >= text.length ? "" : "\n"}`;
|
|
333
|
+
at = b;
|
|
334
|
+
});
|
|
335
|
+
shim += text.slice(at);
|
|
336
|
+
return { files: out, barrel: [src, shim], pkgDir };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function write({ files, barrel, pkgDir }) {
|
|
340
|
+
const createdDir = !existsSync(pkgDir);
|
|
341
|
+
const temps = [];
|
|
342
|
+
try {
|
|
343
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
344
|
+
const all = [...files, barrel];
|
|
345
|
+
for (const [f, t] of all) { const tmp = `${f}.symbol-move-${process.pid}.tmp`; writeFileSync(tmp, t); temps.push([tmp, f]); }
|
|
346
|
+
for (const [tmp, f] of temps) renameSync(tmp, f); // barrel last
|
|
347
|
+
} catch (e) {
|
|
348
|
+
for (const [tmp] of temps) rmSync(tmp, { force: true });
|
|
349
|
+
if (createdDir) rmSync(pkgDir, { recursive: true, force: true });
|
|
350
|
+
throw e;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function main(argv) {
|
|
355
|
+
const o = parseArgs(argv);
|
|
356
|
+
if (!o) return usage();
|
|
357
|
+
if (o.src && !existsSync(o.src)) { process.stderr.write(`symbol-move: no such file: ${o.src}\n`); return 2; }
|
|
358
|
+
const tm = loadTsMorph(o.src);
|
|
359
|
+
if (!tm) return emit({ status: "refused", reason: `no codemod tool for ts: ts-morph is not resolvable from ${o.src ? path.dirname(o.src) : "."} or the cwd (install it in the target repo or put it on NODE_PATH)` }), 3;
|
|
360
|
+
if (o.probe) return emit({ status: "ok", tool: "ts-morph" }), 0;
|
|
361
|
+
if (!existsSync(o.plan)) { process.stderr.write(`symbol-move: no such file: ${o.plan}\n`); return 2; }
|
|
362
|
+
try {
|
|
363
|
+
const ext = path.extname(o.src).toLowerCase();
|
|
364
|
+
if (!TS_EXT.includes(ext)) throw new Refused(`source is not a TypeScript file (${TS_EXT.join(", ")}): ${o.src}`);
|
|
365
|
+
const stem = path.basename(o.src, path.extname(o.src));
|
|
366
|
+
const pkg = o.package ?? `${stem}_parts`;
|
|
367
|
+
if (!IDENT.test(pkg)) throw new Refused(`package name ${JSON.stringify(pkg)} is not a valid identifier`);
|
|
368
|
+
const plan = loadPlan(o.plan);
|
|
369
|
+
const built = build(tm, o.src, plan, pkg, o.extend);
|
|
370
|
+
if (!o.dryRun) write(built);
|
|
371
|
+
emit({ status: "ok", package: posix(path.join(path.dirname(o.src), pkg)), modules: plan, shim: o.src });
|
|
372
|
+
return 0;
|
|
373
|
+
} catch (e) {
|
|
374
|
+
if (e instanceof Refused) return emit({ status: "refused", reason: e.message }), 3;
|
|
375
|
+
throw e;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
process.exitCode = main(process.argv.slice(2));
|