@nage-api/compat 1.0.0-beta.2

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.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +215 -0
  3. package/dist/cli.d.ts +9 -0
  4. package/dist/cli.js +29 -0
  5. package/dist/codemods/cli-main.d.ts +32 -0
  6. package/dist/codemods/cli-main.js +128 -0
  7. package/dist/codemods/config-inventory.d.ts +20 -0
  8. package/dist/codemods/config-inventory.js +118 -0
  9. package/dist/codemods/env-rename.d.ts +36 -0
  10. package/dist/codemods/env-rename.js +104 -0
  11. package/dist/codemods/import-paths.d.ts +45 -0
  12. package/dist/codemods/import-paths.js +247 -0
  13. package/dist/codemods/job-generics.d.ts +17 -0
  14. package/dist/codemods/job-generics.js +74 -0
  15. package/dist/codemods/node-fs.d.ts +11 -0
  16. package/dist/codemods/node-fs.js +35 -0
  17. package/dist/codemods/registry.d.ts +26 -0
  18. package/dist/codemods/registry.js +42 -0
  19. package/dist/codemods/res-envelope.d.ts +29 -0
  20. package/dist/codemods/res-envelope.js +111 -0
  21. package/dist/codemods/runner.d.ts +36 -0
  22. package/dist/codemods/runner.js +44 -0
  23. package/dist/codemods/types.d.ts +69 -0
  24. package/dist/codemods/types.js +40 -0
  25. package/dist/index.d.ts +31 -0
  26. package/dist/index.js +63 -0
  27. package/dist/legacy/compat.module.d.ts +14 -0
  28. package/dist/legacy/compat.module.js +39 -0
  29. package/dist/legacy/envelope.d.ts +62 -0
  30. package/dist/legacy/envelope.js +86 -0
  31. package/dist/legacy/legacy-envelope.decorator.d.ts +17 -0
  32. package/dist/legacy/legacy-envelope.decorator.js +23 -0
  33. package/dist/legacy/legacy-envelope.interceptor.d.ts +25 -0
  34. package/dist/legacy/legacy-envelope.interceptor.js +67 -0
  35. package/dist/legacy/options.d.ts +14 -0
  36. package/dist/legacy/options.js +7 -0
  37. package/dist/legacy/responses.d.ts +48 -0
  38. package/dist/legacy/responses.js +52 -0
  39. package/package.json +61 -0
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * Config blob → `defineConfig` (PLAN.md §23.4) — as a report.
4
+ *
5
+ * `docs/migration.md` already reached this conclusion in prose: "mapping your
6
+ * old configuration onto this is the least mechanical part of the whole
7
+ * migration, and it is worth doing by hand rather than transliterating". The
8
+ * reason is that the legacy shape carries less information than the new one
9
+ * needs. `env.get<string>('DATABASE_URL')` is a cast, not a check, so the source
10
+ * says nothing about whether the value is required, what it must look like, or
11
+ * what happens when it is absent — and those three answers *are* the zod schema.
12
+ *
13
+ * What a machine can do is the tedious half: find every variable the application
14
+ * reads, wherever it reads it from, so the schema is written against a list
15
+ * rather than against memory. That is what this produces — one note per distinct
16
+ * variable, at its first use, plus a note at each site where a default is
17
+ * hiding a misconfiguration.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.configInventoryCodemod = void 0;
21
+ const types_js_1 = require("./types.js");
22
+ /** `process.env.NAME` and `process.env['NAME']`. */
23
+ const PROCESS_ENV = /process\s*\.\s*env\s*(?:\.\s*([A-Za-z_][\w]*)|\[\s*['"]([^'"]+)['"]\s*\])/g;
24
+ /** The `EnvironmentManager` singleton: `env.get<string>('NAME')` (§4.4). */
25
+ const ENV_MANAGER = /\benv(?:Manager)?\s*\.\s*get\s*(?:<[^>]*>)?\s*\(\s*['"]([^'"]+)['"]/g;
26
+ /** `registerAs('jwt', …)` — the namespaces that become `defineConfig` keys. */
27
+ const REGISTER_AS = /\bregisterAs\s*\(\s*['"]([^'"]+)['"]/g;
28
+ /** `configService.get('jwt.secret')` — a string path with no type behind it. */
29
+ const CONFIG_GET = /\bconfig(?:Service)?\s*\.\s*get\s*(?:<[^>]*>)?\s*\(\s*['"]([^'"]+)['"]/g;
30
+ /** `parseInt(process.env.PORT) || 3000` and `process.env.X || 'default'`. */
31
+ const DEFAULTED = /process\s*\.\s*env\s*(?:\.\s*[A-Za-z_][\w]*|\[[^\]]*\])[^\n]*?(?:\|\||\?\?)/;
32
+ const NUMERIC_COERCION = /\b(?:parseInt|parseFloat|Number)\s*\(\s*process\s*\.\s*env/;
33
+ exports.configInventoryCodemod = {
34
+ name: 'config-inventory',
35
+ kind: 'report',
36
+ section: '§23.4, §11',
37
+ summary: 'Inventories every environment variable and config namespace the application reads — the input to `env.schema.ts` and `defineConfig`.',
38
+ run(files) {
39
+ const notes = [];
40
+ const seenVariables = new Set();
41
+ const seenNamespaces = new Set();
42
+ for (const file of files) {
43
+ if (!(0, types_js_1.isSourceFile)(file.path))
44
+ continue;
45
+ file.content.split('\n').forEach((line, index) => {
46
+ if ((0, types_js_1.isCommented)(line))
47
+ return;
48
+ const at = { path: file.path, line: index + 1 };
49
+ for (const [, dotted, bracketed] of line.matchAll(PROCESS_ENV)) {
50
+ const variable = dotted ?? bracketed;
51
+ if (variable === undefined || seenVariables.has(variable))
52
+ continue;
53
+ seenVariables.add(variable);
54
+ notes.push({
55
+ ...at,
56
+ rule: 'env-variable',
57
+ message: `\`${variable}\` is read from the environment.`,
58
+ action: `Add it to \`EnvSchema\`. Required with no default unless absence is genuinely valid; \`secretSchema\` if it is key material, so a short or missing value fails at boot rather than at first use.`,
59
+ });
60
+ }
61
+ for (const [, variable] of line.matchAll(ENV_MANAGER)) {
62
+ if (variable === undefined || seenVariables.has(variable))
63
+ continue;
64
+ seenVariables.add(variable);
65
+ notes.push({
66
+ ...at,
67
+ rule: 'env-variable',
68
+ message: `\`${variable}\` is read through the \`EnvironmentManager\` singleton, whose \`get<T>()\` casts without checking (§4.4).`,
69
+ action: `Add it to \`EnvSchema\`. If it came from AWS Secrets Manager, keep it there with \`secrets: { provider: 'aws' }\` — the SDK stays an optional peer.`,
70
+ });
71
+ }
72
+ for (const [, namespace] of line.matchAll(REGISTER_AS)) {
73
+ if (namespace === undefined || seenNamespaces.has(namespace))
74
+ continue;
75
+ seenNamespaces.add(namespace);
76
+ notes.push({
77
+ ...at,
78
+ rule: 'config-namespace',
79
+ message: `\`registerAs('${namespace}', …)\` declares a configuration namespace.`,
80
+ action: `It becomes a key of the object \`defineConfig\` returns, typed rather than a string path. Its factory is where the variables above are read; fold those reads into \`EnvSchema\` and the factory disappears.`,
81
+ });
82
+ }
83
+ for (const [, path] of line.matchAll(CONFIG_GET)) {
84
+ if (path === undefined)
85
+ continue;
86
+ notes.push({
87
+ ...at,
88
+ rule: 'config-path',
89
+ message: `\`get('${path}')\` reads configuration by string path, so a typo is \`undefined\` at run time.`,
90
+ action: 'Inject the typed config object instead; the path becomes a property access the compiler checks.',
91
+ });
92
+ }
93
+ if (NUMERIC_COERCION.test(line)) {
94
+ notes.push({
95
+ ...at,
96
+ rule: 'silent-coercion',
97
+ message: "`parseInt` on an environment variable cannot fail: `parseInt('80o80')` is `80`, so a typo in a deployment variable is indistinguishable from a correct one (§5.1).",
98
+ action: '`z.coerce.number()` in the schema rejects it at boot with the variable named.',
99
+ });
100
+ // `parseInt(process.env.PORT) || 3000` matches both rules and is one
101
+ // defect, not two. The coercion is the more specific finding, so it
102
+ // wins and the line is not reported twice.
103
+ return;
104
+ }
105
+ if (DEFAULTED.test(line)) {
106
+ notes.push({
107
+ ...at,
108
+ rule: 'silent-default',
109
+ message: 'A default behind `||` or `??` makes an unset variable indistinguishable from a set one — which is why the legacy JWT secret was never replaced (§5.2).',
110
+ action: 'Decide deliberately: a default in the schema for something genuinely optional, or no default at all so `loadEnvOrExit` names it and exits non-zero.',
111
+ });
112
+ }
113
+ });
114
+ }
115
+ return { changes: [], notes };
116
+ },
117
+ };
118
+ //# sourceMappingURL=config-inventory.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The environment-variable rename map (PLAN.md §23.4).
3
+ *
4
+ * One entry, because one rename is documented rather than inferred:
5
+ * `RECAPTCHA_SECRET_KET` was a typo in `libs/recaptcha` (§2.4), so the variable
6
+ * was never set, the verification request carried `secret=undefined`, and code
7
+ * that checked only `success` treated every call as a pass. Renaming it is safe
8
+ * in the strongest sense available — the old name resolves to nothing anywhere,
9
+ * so no behaviour can depend on it.
10
+ *
11
+ * The map is deliberately not padded out. Two renames that look obvious are
12
+ * notes instead:
13
+ *
14
+ * - `JWT_SECRET` → `JWT_PRIVATE_KEY` is not a rename. HS256 signs with a
15
+ * shared secret and RS256 signs with a private key (§28.3); moving the old
16
+ * value under the new name produces an application that fails to sign, or
17
+ * worse, one that verifies with a key it also hands out.
18
+ * - `APP_ENGINE` has no successor at all. One engine per workspace is a
19
+ * structural decision (§28.4), not a variable.
20
+ *
21
+ * Files: source *and* `.env`, because the variable is named in both and renaming
22
+ * only one of them is how a migration ends up with a working build and an
23
+ * unconfigured deployment.
24
+ */
25
+ import { type Codemod } from './types.js';
26
+ /** Legacy name → current name. Every entry needs evidence in PLAN.md. */
27
+ export declare const ENV_RENAMES: Readonly<Record<string, string>>;
28
+ interface EnvNote {
29
+ readonly variable: string;
30
+ readonly message: string;
31
+ readonly action: string;
32
+ }
33
+ export declare const ENV_NOTES: readonly EnvNote[];
34
+ export declare const envRenameCodemod: Codemod;
35
+ export {};
36
+ //# sourceMappingURL=env-rename.d.ts.map
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * The environment-variable rename map (PLAN.md §23.4).
4
+ *
5
+ * One entry, because one rename is documented rather than inferred:
6
+ * `RECAPTCHA_SECRET_KET` was a typo in `libs/recaptcha` (§2.4), so the variable
7
+ * was never set, the verification request carried `secret=undefined`, and code
8
+ * that checked only `success` treated every call as a pass. Renaming it is safe
9
+ * in the strongest sense available — the old name resolves to nothing anywhere,
10
+ * so no behaviour can depend on it.
11
+ *
12
+ * The map is deliberately not padded out. Two renames that look obvious are
13
+ * notes instead:
14
+ *
15
+ * - `JWT_SECRET` → `JWT_PRIVATE_KEY` is not a rename. HS256 signs with a
16
+ * shared secret and RS256 signs with a private key (§28.3); moving the old
17
+ * value under the new name produces an application that fails to sign, or
18
+ * worse, one that verifies with a key it also hands out.
19
+ * - `APP_ENGINE` has no successor at all. One engine per workspace is a
20
+ * structural decision (§28.4), not a variable.
21
+ *
22
+ * Files: source *and* `.env`, because the variable is named in both and renaming
23
+ * only one of them is how a migration ends up with a working build and an
24
+ * unconfigured deployment.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.envRenameCodemod = exports.ENV_NOTES = exports.ENV_RENAMES = void 0;
28
+ const types_js_1 = require("./types.js");
29
+ /** Legacy name → current name. Every entry needs evidence in PLAN.md. */
30
+ exports.ENV_RENAMES = {
31
+ RECAPTCHA_SECRET_KET: 'RECAPTCHA_SECRET_KEY',
32
+ };
33
+ exports.ENV_NOTES = [
34
+ {
35
+ variable: 'RECAPTCHA_SECRET_KET',
36
+ message: 'The secret behind this variable travelled in a GET query string, so it is in every proxy and access log that saw a verification.',
37
+ action: 'Rotate it. Renaming the variable does not un-log the old value.',
38
+ },
39
+ {
40
+ variable: 'JWT_SECRET',
41
+ message: 'HS256 signs with a shared secret; @nage-api/auth signs with an RS256 private key (§28.3). These are different kinds of material, not the same value under a new name.',
42
+ action: 'Generate a key pair, set `JWT_PRIVATE_KEY`, and run the dual-verify window in §23.3 phase 4 until the refresh tokens issued under the old secret have cycled out.',
43
+ },
44
+ {
45
+ variable: 'APP_ENGINE',
46
+ message: 'Engine selection at run time is gone: a workspace installs one driver (§28.4), so both engines are no longer in the build.',
47
+ action: 'Delete the variable and pick the engine. If the answer is Mongo, note that @nage-api/data-mongo is not written yet.',
48
+ },
49
+ ];
50
+ function isEnvFile(path) {
51
+ const name = path.split(/[/\\]/).pop() ?? '';
52
+ return name === '.env' || name.startsWith('.env.');
53
+ }
54
+ /** Whole-word: `RECAPTCHA_SECRET_KET` must not match inside a longer name. */
55
+ function wordPattern(name) {
56
+ return new RegExp(`(?<![\\w$])${name}(?![\\w$])`, 'g');
57
+ }
58
+ exports.envRenameCodemod = {
59
+ name: 'env-rename',
60
+ kind: 'rewrite',
61
+ section: '§23.4',
62
+ summary: 'Renames the one documented legacy environment variable, and reports the two that look renameable and are not.',
63
+ run(files) {
64
+ const changes = [];
65
+ const notes = [];
66
+ for (const file of files) {
67
+ if (!(0, types_js_1.isSourceFile)(file.path) && !isEnvFile(file.path))
68
+ continue;
69
+ const lines = file.content.split('\n');
70
+ const edits = [];
71
+ const rewritten = lines.map((line, index) => {
72
+ if ((0, types_js_1.isCommented)(line))
73
+ return line;
74
+ let next = line;
75
+ for (const [from, to] of Object.entries(exports.ENV_RENAMES)) {
76
+ next = next.replace(wordPattern(from), to);
77
+ }
78
+ if (next !== line)
79
+ edits.push({ line: index + 1, before: line.trim(), after: next.trim() });
80
+ return next;
81
+ });
82
+ if (edits.length > 0) {
83
+ changes.push({ path: file.path, content: rewritten.join('\n'), edits });
84
+ }
85
+ lines.forEach((line, index) => {
86
+ if ((0, types_js_1.isCommented)(line))
87
+ return;
88
+ for (const note of exports.ENV_NOTES) {
89
+ if (!wordPattern(note.variable).test(line))
90
+ continue;
91
+ notes.push({
92
+ path: file.path,
93
+ line: index + 1,
94
+ rule: note.variable,
95
+ message: note.message,
96
+ action: note.action,
97
+ });
98
+ }
99
+ });
100
+ }
101
+ return { changes, notes };
102
+ },
103
+ };
104
+ //# sourceMappingURL=env-rename.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Import-path rewrites (PLAN.md §23.4, and the table in `docs/migration.md`).
3
+ *
4
+ * Two specifiers are rewritten, and only when every name on the import is one
5
+ * this repository can vouch for:
6
+ *
7
+ * - `…/core.responses` → `@nage-api/compat`, for `Result`/`Created`/`ErrorResponse`.
8
+ * Certain because `@nage-api/compat` is what supplies the replacement: the names,
9
+ * the arity and the body are this package's own.
10
+ * - `…/core.decorators` → `@nage-api/core`, for the four version decorators.
11
+ * Certain because §16.2 retained them deliberately, under the same names and
12
+ * the same call shapes.
13
+ *
14
+ * Everything else in the §22 table is a **note**. The reasons differ and each is
15
+ * given at the call site, but they reduce to three:
16
+ *
17
+ * - the legacy module has no single successor (`@core/sql` becomes `@nage-api/data`
18
+ * *and* `@nage-api/data-sql`, split by symbol);
19
+ * - the successor is not written yet (`@nage-api/data-mongo`, the notify channels);
20
+ * - the names survive but the meanings do not. `NotFoundError` and
21
+ * `ValidationError` exist in both frameworks, so repointing that import
22
+ * compiles — and changes the status code and the error code the client sees.
23
+ * A rewrite whose failure mode is a *passing* build is exactly the rewrite
24
+ * not to make.
25
+ */
26
+ import { type Codemod } from './types.js';
27
+ interface RewriteRule {
28
+ readonly rule: string;
29
+ readonly matches: (specifier: string) => boolean;
30
+ readonly to: string;
31
+ /** Only these names may travel; anything else keeps the old import. */
32
+ readonly names: readonly string[];
33
+ readonly heldBack: string;
34
+ }
35
+ interface ReportRule {
36
+ readonly rule: string;
37
+ readonly matches: (specifier: string) => boolean;
38
+ readonly message: string;
39
+ readonly action: string;
40
+ }
41
+ export declare const REWRITE_RULES: readonly RewriteRule[];
42
+ export declare const REPORT_RULES: readonly ReportRule[];
43
+ export declare const importPathsCodemod: Codemod;
44
+ export {};
45
+ //# sourceMappingURL=import-paths.d.ts.map
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ /**
3
+ * Import-path rewrites (PLAN.md §23.4, and the table in `docs/migration.md`).
4
+ *
5
+ * Two specifiers are rewritten, and only when every name on the import is one
6
+ * this repository can vouch for:
7
+ *
8
+ * - `…/core.responses` → `@nage-api/compat`, for `Result`/`Created`/`ErrorResponse`.
9
+ * Certain because `@nage-api/compat` is what supplies the replacement: the names,
10
+ * the arity and the body are this package's own.
11
+ * - `…/core.decorators` → `@nage-api/core`, for the four version decorators.
12
+ * Certain because §16.2 retained them deliberately, under the same names and
13
+ * the same call shapes.
14
+ *
15
+ * Everything else in the §22 table is a **note**. The reasons differ and each is
16
+ * given at the call site, but they reduce to three:
17
+ *
18
+ * - the legacy module has no single successor (`@core/sql` becomes `@nage-api/data`
19
+ * *and* `@nage-api/data-sql`, split by symbol);
20
+ * - the successor is not written yet (`@nage-api/data-mongo`, the notify channels);
21
+ * - the names survive but the meanings do not. `NotFoundError` and
22
+ * `ValidationError` exist in both frameworks, so repointing that import
23
+ * compiles — and changes the status code and the error code the client sees.
24
+ * A rewrite whose failure mode is a *passing* build is exactly the rewrite
25
+ * not to make.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.importPathsCodemod = exports.REPORT_RULES = exports.REWRITE_RULES = void 0;
29
+ const types_js_1 = require("./types.js");
30
+ /** `…/core.responses`, `…/core.responses.js`, `@core/core.responses`, … */
31
+ function moduleNamed(name) {
32
+ const pattern = new RegExp(`(?:^|[/\\\\])${name.replace('.', '\\.')}(?:\\.(?:js|ts))?$`);
33
+ return (specifier) => pattern.test(specifier);
34
+ }
35
+ function exactly(specifier) {
36
+ return (candidate) => candidate === specifier;
37
+ }
38
+ exports.REWRITE_RULES = [
39
+ {
40
+ rule: 'core.responses',
41
+ matches: moduleNamed('core.responses'),
42
+ to: '@nage-api/compat',
43
+ names: ['Result', 'Created', 'ErrorResponse'],
44
+ heldBack: 'the rest of `core.responses.ts` — the Swagger definitions and the upload helpers — has no equivalent in @nage-api/compat',
45
+ },
46
+ {
47
+ rule: 'core.decorators',
48
+ matches: moduleNamed('core.decorators'),
49
+ to: '@nage-api/core',
50
+ names: ['ForVersion', 'FromVersion', 'TillVersion', 'BetweenVersions'],
51
+ heldBack: '`FileUploads`, `MsListener` and `MsEventListener` are not retained: uploads moved to @nage-api/storage and the microservice bus was redesigned as @nage-api/queue (§23 "the job bus and the sockets are redesigns")',
52
+ },
53
+ ];
54
+ exports.REPORT_RULES = [
55
+ {
56
+ rule: 'core.errors',
57
+ matches: moduleNamed('core.errors'),
58
+ message: '`NotFoundError` and `ValidationError` exist in @nage-api/core under the same names, with different constructors and a stable `error.code` the client now sees.',
59
+ action: 'Repoint by hand, one throw site at a time, checking the status and code each one now produces (§17.1). A blanket rewrite would compile and change the API.',
60
+ },
61
+ {
62
+ rule: 'core.job',
63
+ matches: moduleNamed('core.job'),
64
+ message: '`Job` is now `Job<TEntity, TBody, TParams>` from @nage-api/core (§13).',
65
+ action: 'Import it from @nage-api/core and supply the entity. `Job<Order>` compiles against both frameworks and means different things in each, so this is not a rewrite.',
66
+ },
67
+ {
68
+ rule: '@core/sql',
69
+ matches: exactly('@core/sql'),
70
+ message: '`libs/sql` splits by symbol across two packages.',
71
+ action: '`ModelService`, `parseQuery` and `defineQueryPolicy` come from @nage-api/data; the Sequelize binding comes from @nage-api/data-sql.',
72
+ },
73
+ {
74
+ rule: '@core/mongo',
75
+ matches: exactly('@core/mongo'),
76
+ message: '@nage-api/data-mongo is not written. This is blocking for a MongoDB application.',
77
+ action: 'Port everything above the data layer, then stop; or move the application to the SQL driver. See `docs/migration.md` → "What has no equivalent yet".',
78
+ },
79
+ {
80
+ rule: '@core/email',
81
+ matches: exactly('@core/email'),
82
+ message: '@nage-api/notify has the shape of the legacy notification module but ships no email channel.',
83
+ action: 'Supply a `NotificationChannel` wrapping the mailer you already use. The hardcoded From name and `tls.rejectUnauthorized: false` do not come with it.',
84
+ },
85
+ {
86
+ rule: '@core/twilio',
87
+ matches: exactly('@core/twilio'),
88
+ message: '@nage-api/notify ships no SMS channel.',
89
+ action: 'Supply a `NotificationChannel` wrapping the Twilio client you already use.',
90
+ },
91
+ {
92
+ rule: '@core/recaptcha',
93
+ matches: exactly('@core/recaptcha'),
94
+ message: '@nage-api/integrations-recaptcha exists, but the verifier is a different class with a different method, and the environment variable was renamed (see the `env-rename` codemod).',
95
+ action: 'Repoint the import by hand and rotate the secret: the legacy verifier sent it in a GET query string, so it is in every proxy log that saw a verification.',
96
+ },
97
+ {
98
+ rule: '@core/firebase',
99
+ matches: exactly('@core/firebase'),
100
+ message: 'No equivalent package.',
101
+ action: 'Keep using the admin SDK directly, behind a port of your own. Note that the legacy controller dispatched on `this.service[job.action]` with no allow-list (§5.2); do not carry that across.',
102
+ },
103
+ {
104
+ rule: '@core/stripe',
105
+ matches: exactly('@core/stripe'),
106
+ message: 'No equivalent package.',
107
+ action: 'The legacy lib was a bare client holder; construct the client where you need it.',
108
+ },
109
+ {
110
+ rule: '@core/square',
111
+ matches: exactly('@core/square'),
112
+ message: 'No equivalent package.',
113
+ action: 'The legacy lib was a bare client holder; construct the client where you need it.',
114
+ },
115
+ {
116
+ rule: '@core/geocoder',
117
+ matches: exactly('@core/geocoder'),
118
+ message: 'No equivalent package.',
119
+ action: 'Keep `node-geocoder` behind a port of your own.',
120
+ },
121
+ ];
122
+ /**
123
+ * `import`/`export … from '…'`, including multi-line clauses.
124
+ *
125
+ * The clause excludes `;` so a lazy match cannot run past the end of one
126
+ * statement and pair an earlier `import` with a later `from` — which is what a
127
+ * `[\s\S]*?` clause does to a side-effect import (`import 'reflect-metadata';`)
128
+ * followed by a real one.
129
+ */
130
+ const IMPORT_PATTERN = /\b(?:import|export)\b([^;]*?)\bfrom\s*(['"])([^'"]+)\2/g;
131
+ /** The names an import clause binds, or `undefined` when it binds a namespace. */
132
+ function importedNames(clause) {
133
+ const braces = /\{([^}]*)\}/.exec(clause);
134
+ if (braces === null)
135
+ return undefined;
136
+ // A default or namespace import alongside the braces: `import Default, { A }`.
137
+ if (/^\s*(?:type\s+)?[A-Za-z_$][\w$]*\s*,/.test(clause))
138
+ return undefined;
139
+ if (clause.includes('*'))
140
+ return undefined;
141
+ return (braces[1] ?? '')
142
+ .split(',')
143
+ .map((entry) => entry
144
+ .trim()
145
+ .replace(/^type\s+/, '')
146
+ .split(/\s+as\s+/)[0]
147
+ ?.trim() ?? '')
148
+ .filter((name) => name.length > 0);
149
+ }
150
+ function lineOf(content, index) {
151
+ let line = 1;
152
+ for (let position = 0; position < index; position += 1) {
153
+ if (content[position] === '\n')
154
+ line += 1;
155
+ }
156
+ return line;
157
+ }
158
+ function lineText(content, index) {
159
+ const start = content.lastIndexOf('\n', index) + 1;
160
+ const end = content.indexOf('\n', index);
161
+ return content.slice(start, end === -1 ? content.length : end);
162
+ }
163
+ function runOnFile(file) {
164
+ const notes = [];
165
+ const edits = [];
166
+ let content = file.content;
167
+ // Offsets shift as the content is spliced, so matches are collected against
168
+ // the original text first and applied from the end backwards.
169
+ const matches = [...file.content.matchAll(IMPORT_PATTERN)];
170
+ for (const match of matches.reverse()) {
171
+ const index = match.index;
172
+ const clause = match[1] ?? '';
173
+ const specifier = match[3] ?? '';
174
+ const line = lineOf(file.content, index);
175
+ if ((0, types_js_1.isCommented)(lineText(file.content, index)))
176
+ continue;
177
+ const rewrite = exports.REWRITE_RULES.find((candidate) => candidate.matches(specifier));
178
+ if (rewrite !== undefined) {
179
+ const names = importedNames(clause);
180
+ const unsupported = names === undefined ? undefined : names.filter((name) => !rewrite.names.includes(name));
181
+ if (names !== undefined && unsupported?.length === 0) {
182
+ const quote = match[2] ?? "'";
183
+ const specifierStart = file.content.lastIndexOf(`${quote}${specifier}${quote}`, index + match[0].length);
184
+ // The specifier's own line, not the `import` keyword's: a multi-line
185
+ // clause puts them several lines apart, and a diff that points at the
186
+ // wrong one is a diff nobody can check.
187
+ const before = lineText(file.content, specifierStart);
188
+ content =
189
+ content.slice(0, specifierStart) +
190
+ `${quote}${rewrite.to}${quote}` +
191
+ content.slice(specifierStart + specifier.length + 2);
192
+ edits.push({
193
+ line: lineOf(file.content, specifierStart),
194
+ before: before.trim(),
195
+ after: before.replace(specifier, rewrite.to).trim(),
196
+ });
197
+ continue;
198
+ }
199
+ notes.push({
200
+ path: file.path,
201
+ line,
202
+ rule: rewrite.rule,
203
+ message: names === undefined
204
+ ? `\`${specifier}\` is imported as a default or namespace, so which names travel to ${rewrite.to} cannot be read off the import.`
205
+ : `\`${(unsupported ?? []).join('`, `')}\` from \`${specifier}\` cannot move to ${rewrite.to}: ${rewrite.heldBack}.`,
206
+ action: names === undefined
207
+ ? `Split it into a named import; the rewrite then covers ${rewrite.names.join(', ')}.`
208
+ : `Move ${rewrite.names.join(', ')} in a separate import — this codemod will then rewrite it — and port the rest by hand.`,
209
+ });
210
+ continue;
211
+ }
212
+ const report = exports.REPORT_RULES.find((candidate) => candidate.matches(specifier));
213
+ if (report !== undefined) {
214
+ notes.push({
215
+ path: file.path,
216
+ line,
217
+ rule: report.rule,
218
+ message: report.message,
219
+ action: report.action,
220
+ });
221
+ }
222
+ }
223
+ return {
224
+ ...(edits.length === 0 ? {} : { change: { path: file.path, content, edits: edits.reverse() } }),
225
+ notes: notes.reverse(),
226
+ };
227
+ }
228
+ exports.importPathsCodemod = {
229
+ name: 'import-paths',
230
+ kind: 'rewrite',
231
+ section: '§23.4',
232
+ summary: 'Repoints the two legacy modules whose successors are name-for-name identical, and reports the eleven that are not.',
233
+ run(files) {
234
+ const changes = [];
235
+ const notes = [];
236
+ for (const file of files) {
237
+ if (!(0, types_js_1.isSourceFile)(file.path))
238
+ continue;
239
+ const result = runOnFile(file);
240
+ if (result.change !== undefined)
241
+ changes.push(result.change);
242
+ notes.push(...result.notes);
243
+ }
244
+ return { changes, notes };
245
+ },
246
+ };
247
+ //# sourceMappingURL=import-paths.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `Job` and `ModelService` type parameters (PLAN.md §22, §13) — a report.
3
+ *
4
+ * The legacy generics were defeated at the boundary: `body: any`, `records:
5
+ * any[]`, `payload: any` on `core.job.ts`, so `ModelService<M>` knew the model
6
+ * and the job handed it `any` anyway (§5.1). The new `Job<TEntity, TBody,
7
+ * TParams>` types every member against the entity.
8
+ *
9
+ * Supplying those arguments is not a text transform. Which entity a job is about
10
+ * is a fact about the surrounding module — often about which repository is
11
+ * injected three classes away — and getting it wrong produces code that compiles
12
+ * and lies. So this locates the sites and names what each one needs; the 146
13
+ * `any`s §5.1 counted are a list to work through, not a diff to approve.
14
+ */
15
+ import { type Codemod } from './types.js';
16
+ export declare const jobGenericsCodemod: Codemod;
17
+ //# sourceMappingURL=job-generics.d.ts.map
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * `Job` and `ModelService` type parameters (PLAN.md §22, §13) — a report.
4
+ *
5
+ * The legacy generics were defeated at the boundary: `body: any`, `records:
6
+ * any[]`, `payload: any` on `core.job.ts`, so `ModelService<M>` knew the model
7
+ * and the job handed it `any` anyway (§5.1). The new `Job<TEntity, TBody,
8
+ * TParams>` types every member against the entity.
9
+ *
10
+ * Supplying those arguments is not a text transform. Which entity a job is about
11
+ * is a fact about the surrounding module — often about which repository is
12
+ * injected three classes away — and getting it wrong produces code that compiles
13
+ * and lies. So this locates the sites and names what each one needs; the 146
14
+ * `any`s §5.1 counted are a list to work through, not a diff to approve.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.jobGenericsCodemod = void 0;
18
+ const types_js_1 = require("./types.js");
19
+ const PROBES = [
20
+ {
21
+ rule: 'job-any',
22
+ pattern: /\bJob\s*<\s*any\b/,
23
+ message: '`Job<any>` re-erases exactly what the new contract types.',
24
+ action: 'Supply the entity: `Job<Order>`. `TBody` defaults to `DeepPartial<TEntity>` and `TParams` to the route parameters, so most sites need one argument.',
25
+ },
26
+ {
27
+ rule: 'job-untyped',
28
+ pattern: /:\s*Job\s*(?:[,);=]|$)/,
29
+ message: '`Job` with no type argument. In the new contract the entity is required.',
30
+ action: 'Supply the entity the handler operates on.',
31
+ },
32
+ {
33
+ rule: 'service-any',
34
+ pattern: /\b(?:ModelService|SqlService|MongoService)\s*<\s*any\b/,
35
+ message: 'The base service is generic over the model; `any` here removes the type safety the class was written for.',
36
+ action: 'Supply the entity. `ModelService` in @nage-api/data keeps the `doBefore*`/`doAfter*` hook names, so the port is the type argument plus the import.',
37
+ },
38
+ {
39
+ rule: 'payload-any',
40
+ pattern: /\b(?:body|payload|records|owner|result)\s*[?]?\s*:\s*any\b/,
41
+ message: 'One of the four members that defeated the legacy generics (`body`, `payload`, `records`, `owner`).',
42
+ action: 'Type it against the entity or the DTO. `strict: true` will surface whatever the `any` was hiding; that is the migration working, not the migration failing (§23.2).',
43
+ },
44
+ ];
45
+ exports.jobGenericsCodemod = {
46
+ name: 'job-generics',
47
+ kind: 'report',
48
+ section: '§22, §13',
49
+ summary: 'Locates the `any`s that defeated the legacy `Job`/`ModelService` generics.',
50
+ run(files) {
51
+ const notes = [];
52
+ for (const file of files) {
53
+ if (!(0, types_js_1.isSourceFile)(file.path))
54
+ continue;
55
+ file.content.split('\n').forEach((line, index) => {
56
+ if ((0, types_js_1.isCommented)(line))
57
+ return;
58
+ for (const probe of PROBES) {
59
+ if (!probe.pattern.test(line))
60
+ continue;
61
+ notes.push({
62
+ path: file.path,
63
+ line: index + 1,
64
+ rule: probe.rule,
65
+ message: probe.message,
66
+ action: probe.action,
67
+ });
68
+ }
69
+ });
70
+ }
71
+ return { changes: [], notes };
72
+ },
73
+ };
74
+ //# sourceMappingURL=job-generics.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The real filesystem behind `CodemodFileSystem`.
3
+ *
4
+ * Separated from the command layer so every test of the command runs against an
5
+ * in-memory map. The skip list is the same one the CLI's own scanner uses:
6
+ * walking `node_modules` of a legacy monorepo is minutes of work producing notes
7
+ * about somebody else's code.
8
+ */
9
+ import type { CodemodFileSystem } from './cli-main.js';
10
+ export declare function nodeCodemodFs(): CodemodFileSystem;
11
+ //# sourceMappingURL=node-fs.d.ts.map