di-bag-codemod 0.1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/cli.mjs +51 -0
- package/lib/codemod.mjs +77 -0
- package/lib/glob.mjs +51 -0
- package/lib/library.mjs +71 -0
- package/lib/load-typescript.mjs +33 -0
- package/lib/rename-map.mjs +292 -0
- package/lib/rewrite.mjs +946 -0
- package/lib/transforms/build-and-start.mjs +58 -0
- package/lib/transforms/collection-read.mjs +14 -0
- package/lib/transforms/collection-reference.mjs +10 -0
- package/lib/transforms/collection-token.mjs +20 -0
- package/lib/transforms/collection-tokens.mjs +98 -0
- package/lib/transforms/container-derivation.mjs +61 -0
- package/lib/transforms/index.mjs +19 -0
- package/lib/transforms/provider-facades.mjs +74 -0
- package/lib/transforms/provider-methods.mjs +100 -0
- package/lib/transforms/provider-sources.mjs +174 -0
- package/package.json +18 -0
- package/rename-map.json +160 -0
- package/rename-map.schema.json +208 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Danylo Fedorov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# di-bag-codemod
|
|
2
|
+
|
|
3
|
+
Moves code written for [DI Bag](https://github.com/dany-fedorov/di-bag) 0.4 to
|
|
4
|
+
the 0.5 API. It uses the TypeScript checker to authenticate calls, properties,
|
|
5
|
+
contextually typed object keys, and exported type references as DI Bag uses.
|
|
6
|
+
Argument values are rewritten only within those authenticated uses. Runtime
|
|
7
|
+
`DI_BAG_*` strings and module specifiers follow explicit exact or suffix rules
|
|
8
|
+
in the rename map; they do not depend on declaration resolution. Ordinary uses
|
|
9
|
+
such as `text.replace(...)`, `Promise.all(...)`, and your own `register` method
|
|
10
|
+
are left alone.
|
|
11
|
+
|
|
12
|
+
Run it **before** you upgrade, while `di-bag` 0.4 is still installed: every
|
|
13
|
+
type-based decision reads the old declarations. Runs are dry by default;
|
|
14
|
+
`--write` applies the changes, and `--report <file>` records every item left
|
|
15
|
+
for a person. Scoped remains the default lifetime in 0.5, so no blanket
|
|
16
|
+
lifetime pinning is needed. Proven child-replacement lifetime adjustments are
|
|
17
|
+
automatic; ambiguous cases stay unchanged as manual items. See the
|
|
18
|
+
[0.5 migration guide](https://github.com/dany-fedorov/di-bag/blob/main/docs/guides/migrating-to-0.5.md)
|
|
19
|
+
for the complete upgrade sequence.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx di-bag-codemod # dry run over ./tsconfig.json
|
|
25
|
+
npx di-bag-codemod --write # apply
|
|
26
|
+
npx di-bag-codemod --project tsconfig.app.json --write --report codemod-report.json
|
|
27
|
+
npx di-bag-codemod src/app.ts src/worker.ts # explicit files instead of a project
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Pass either `--project <tsconfig>` or positional source files. Passing both is
|
|
31
|
+
a usage error; use repeatable `--extra-files` to add files excluded by a
|
|
32
|
+
tsconfig. Without either input form, `./tsconfig.json` is used. A run prints one
|
|
33
|
+
line per changed file, one entry per manual item, and a summary:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
would rewrite src/app.ts: 3 rewrites
|
|
37
|
+
manual src/app.ts:41:52 startupOrder is not a literal; use maxConcurrentServiceKeys: omit it for 'parallel', 1 for 'sequential', or the number
|
|
38
|
+
startupOrder: order
|
|
39
|
+
1 files, 3 rewrites, 1 manual items (dry run; pass --write to apply) (TypeScript 6.0.3, project)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Option | Meaning |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `--write` | Write the rewritten files. Without it nothing changes on disk. |
|
|
45
|
+
| `--report <file>` | Write the changed files and complete manual items as JSON. |
|
|
46
|
+
| `--extra-files <glob>` | Add files the tsconfig excludes. Repeatable. `*`, `**`, and `?` are supported. |
|
|
47
|
+
| `--library-root <dir>` | Treat declarations under this directory as DI Bag. Repeatable. Without this option, installed `node_modules/di-bag` declarations are recognized automatically; when supplied, only the listed roots count. |
|
|
48
|
+
| `--map <file>` | Use a rename map other than the one in this package. |
|
|
49
|
+
| `--help`, `-h` | Print command usage. |
|
|
50
|
+
|
|
51
|
+
Exit codes: `0` means the run finished, with or without manual items; `2` means
|
|
52
|
+
a usage, tsconfig, or rename-map error.
|
|
53
|
+
|
|
54
|
+
## Manual items
|
|
55
|
+
|
|
56
|
+
The codemod never guesses. What it cannot decide from the original program it
|
|
57
|
+
leaves unchanged and reports with file, line, column, reason, and source text:
|
|
58
|
+
|
|
59
|
+
- options that are not an object literal, or that contain a spread;
|
|
60
|
+
- a string value that is not a literal;
|
|
61
|
+
- a method referenced or destructured without being called when its arguments
|
|
62
|
+
change shape;
|
|
63
|
+
- a call with a spread argument;
|
|
64
|
+
- a receiver of type `any`;
|
|
65
|
+
- a runtime code split into several replacements, and an old code inside a
|
|
66
|
+
regular expression, template, or comment;
|
|
67
|
+
- a re-export of a renamed name, which keeps its old public name.
|
|
68
|
+
|
|
69
|
+
Fix these by hand, then run the compiler.
|
|
70
|
+
|
|
71
|
+
Qualified members in `typeof` follow the same uncalled-reference rule: a compatible
|
|
72
|
+
plain rename changes the terminal member, while reshaped or ambiguous methods stay
|
|
73
|
+
unchanged and are reported for manual migration.
|
|
74
|
+
|
|
75
|
+
## The rename map
|
|
76
|
+
|
|
77
|
+
`rename-map.json` describes the distance from 0.4.0 to the current API.
|
|
78
|
+
`rename-map.schema.json` documents every field.
|
|
79
|
+
Method and property targets must be bare ASCII identifiers. Type targets have
|
|
80
|
+
the same form and cannot be TypeScript keywords or primitive type names. Keys
|
|
81
|
+
emitted inside options bags may contain other characters and are quoted safely.
|
|
82
|
+
|
|
83
|
+
| Section | Rewrites |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `methods` | a method name, its arguments into one options bag or an array, or a custom transform |
|
|
86
|
+
| `options` | a key of an object-literal argument, by argument position and path |
|
|
87
|
+
| `values` | a string at an argument path, or a string compared with or assigned to a library property |
|
|
88
|
+
| `properties` | a property of a library type: access, destructuring, and keys of contextually typed object literals |
|
|
89
|
+
| `types` | an exported class, interface, type alias, or error class in imports and references |
|
|
90
|
+
| `codes` | an exact `DI_BAG_*` runtime code in a string literal |
|
|
91
|
+
| `imports` | an exact module specifier or a suffix of a relative module specifier |
|
|
92
|
+
|
|
93
|
+
An owner is the original declaration that holds the member: `Builder`, `Bag`,
|
|
94
|
+
`DiBagApi`, or `StartupOptions`. A type written inline in a signature belongs
|
|
95
|
+
to that function, such as `fromFactory()`, or method, such as
|
|
96
|
+
`Bag.createScope()`. A custom method transform reads emitted method and existing
|
|
97
|
+
property names from the map. Its optional `transformNames` object supplies
|
|
98
|
+
role-based names for fields that had no declaration in 0.4.
|
|
99
|
+
For a same-name method that accepts both its old positional form and its new
|
|
100
|
+
options-bag form, `arguments.alreadyBag: true` makes the codemod ask the
|
|
101
|
+
TypeScript checker which form a one-argument call uses. It preserves a proven
|
|
102
|
+
bag, converts a proven old value, and reports an ambiguous value for manual
|
|
103
|
+
migration instead of guessing.
|
|
104
|
+
|
|
105
|
+
## Limits
|
|
106
|
+
|
|
107
|
+
The tool rewrites TypeScript and TSX files in the selected program. It does not
|
|
108
|
+
read Markdown, generated source held in strings, or JavaScript without types.
|
|
109
|
+
It preserves formatting and does not run a formatter.
|
|
110
|
+
|
|
111
|
+
When the project provides a `typescript` package with compiler API version
|
|
112
|
+
6.0.3 or later, the codemod uses that project compiler. If no project compiler
|
|
113
|
+
can be resolved, it is older, or it lacks the required JavaScript compiler API,
|
|
114
|
+
the codemod uses its bundled TypeScript and identifies the choice in the summary.
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tools/codemod/cli.mjs
|
|
3
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { loadTypeScript, runCodemod } from './lib/codemod.mjs';
|
|
6
|
+
|
|
7
|
+
const usage = 'usage: di-bag-codemod [--project tsconfig.json | file.ts ...] [--library-root dir]... [--extra-files glob]... [--map rename-map.json] [--write] [--report report.json]';
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const files = [];
|
|
10
|
+
const extraFiles = [];
|
|
11
|
+
const libraryRoots = [];
|
|
12
|
+
let project, mapFile, report, write = false;
|
|
13
|
+
const value = index => {
|
|
14
|
+
if (args[index] === undefined || args[index].startsWith('-')) fail(`${args[index - 1]} needs a value`);
|
|
15
|
+
return args[index];
|
|
16
|
+
};
|
|
17
|
+
for (let index = 0; index < args.length; index++) {
|
|
18
|
+
const argument = args[index];
|
|
19
|
+
if (argument === '--project') project = value(++index);
|
|
20
|
+
else if (argument === '--library-root') libraryRoots.push(value(++index));
|
|
21
|
+
else if (argument === '--extra-files') extraFiles.push(value(++index));
|
|
22
|
+
else if (argument === '--map') mapFile = value(++index);
|
|
23
|
+
else if (argument === '--report') report = value(++index);
|
|
24
|
+
else if (argument === '--write') write = true;
|
|
25
|
+
else if (argument === '--help' || argument === '-h') { console.log(usage); process.exit(0); }
|
|
26
|
+
else if (argument.startsWith('-')) fail(`unknown option ${argument}`);
|
|
27
|
+
else files.push(argument);
|
|
28
|
+
}
|
|
29
|
+
if (project && files.length > 0) fail('--project cannot be used with positional files');
|
|
30
|
+
if (!project && files.length === 0) {
|
|
31
|
+
if (!existsSync('tsconfig.json')) fail('no tsconfig.json in the current directory; pass --project or files');
|
|
32
|
+
project = 'tsconfig.json';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fail(message) {
|
|
36
|
+
console.error(`di-bag-codemod: ${message}\n${usage}`);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const root = process.cwd();
|
|
41
|
+
const compiler = loadTypeScript(project ? dirname(resolve(root, project)) : root);
|
|
42
|
+
let result;
|
|
43
|
+
try {
|
|
44
|
+
result = runCodemod({ typescript: compiler.ts, root, project, files, extraFiles, libraryRoots, write, ...(mapFile ? { mapFile: resolve(root, mapFile) } : {}) });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
fail(error.message);
|
|
47
|
+
}
|
|
48
|
+
if (report) writeFileSync(report, JSON.stringify({ version: 1, written: write, files: result.files.map(({ file, rewrites }) => ({ file, rewrites })), manual: result.manual }, null, 2) + '\n');
|
|
49
|
+
for (const file of result.files) console.log(`${write ? 'rewrote' : 'would rewrite'} ${file.file}: ${file.rewrites} rewrites`);
|
|
50
|
+
for (const item of result.manual) console.log(`manual ${item.file}:${item.line}:${item.column} ${item.reason}${item.text ? `\n ${item.text}` : ''}`);
|
|
51
|
+
console.log(`${result.files.length} files, ${result.rewrites} rewrites, ${result.manual.length} manual items${write ? '' : ' (dry run; pass --write to apply)'} (TypeScript ${compiler.version}, ${compiler.source})`);
|
package/lib/codemod.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// tools/codemod/lib/codemod.mjs
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { expandGlob } from './glob.mjs';
|
|
6
|
+
import { createLibrary } from './library.mjs';
|
|
7
|
+
import { indexRenameMap, loadRenameMap, validateRenameMap } from './rename-map.mjs';
|
|
8
|
+
import { rewriteSourceFile } from './rewrite.mjs';
|
|
9
|
+
import { transforms } from './transforms/index.mjs';
|
|
10
|
+
|
|
11
|
+
export { loadTypeScript } from './load-typescript.mjs';
|
|
12
|
+
export { validateRenameMap } from './rename-map.mjs';
|
|
13
|
+
export { transforms };
|
|
14
|
+
|
|
15
|
+
/** The map shipped with this package: the distance from di-bag 0.4.0 to the current API. */
|
|
16
|
+
export const defaultMapFile = resolve(dirname(fileURLToPath(import.meta.url)), '../rename-map.json');
|
|
17
|
+
|
|
18
|
+
function loadProgram(ts, { project, files, root, extraFiles }) {
|
|
19
|
+
const extra = extraFiles.flatMap(pattern => expandGlob(pattern, root));
|
|
20
|
+
if (project) {
|
|
21
|
+
const config = ts.getParsedCommandLineOfConfigFile(resolve(root, project), {}, {
|
|
22
|
+
...ts.sys, onUnRecoverableConfigFileDiagnostic: diagnostic => { throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')); },
|
|
23
|
+
});
|
|
24
|
+
return ts.createProgram([...new Set([...config.fileNames, ...extra])], { ...config.options, noEmit: true });
|
|
25
|
+
}
|
|
26
|
+
return ts.createProgram([...new Set([...files.map(file => resolve(root, file)), ...extra])], {
|
|
27
|
+
strict: true, noEmit: true, skipLibCheck: true, types: [],
|
|
28
|
+
target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.NodeNext, moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Rewrite every source file of a program that is neither a declaration file, nor under
|
|
34
|
+
* `node_modules`, nor part of the library itself.
|
|
35
|
+
* @param {{ typescript: typeof import('typescript'), root: string, project?: string, files?: string[], extraFiles?: string[],
|
|
36
|
+
* libraryRoots?: string[], map?: object, mapFile?: string, write?: boolean, only?: string[], program?: import('typescript').Program }} options
|
|
37
|
+
* @returns {{ files: { file: string, rewrites: number, text: string }[], manual: { file: string, line: number, column: number, reason: string, text: string }[], rewrites: number }}
|
|
38
|
+
*/
|
|
39
|
+
export function runCodemod({ typescript: ts, root, project, files = [], extraFiles = [], libraryRoots = [], map, mapFile = defaultMapFile, write = false, only, program }) {
|
|
40
|
+
const renameMap = map ?? loadRenameMap(mapFile, Object.keys(transforms));
|
|
41
|
+
const problems = validateRenameMap(renameMap, Object.keys(transforms));
|
|
42
|
+
if (problems.length) throw new Error(`invalid rename map:\n${problems.join('\n')}`);
|
|
43
|
+
const index = indexRenameMap(renameMap);
|
|
44
|
+
const built = program ?? loadProgram(ts, { project, files, root, extraFiles });
|
|
45
|
+
const checker = built.getTypeChecker();
|
|
46
|
+
const library = createLibrary({ ts, checker, root, libraryRoots });
|
|
47
|
+
const selected = only === undefined ? undefined : new Set(only.map(file => resolve(root, file)));
|
|
48
|
+
const writableSourceFiles = new Set(built.getSourceFiles()
|
|
49
|
+
.map(sourceFile => resolve(sourceFile.fileName))
|
|
50
|
+
.filter(fileName => !fileName.includes('/node_modules/') && !library.isLibraryFile(fileName) && (selected === undefined || selected.has(fileName))));
|
|
51
|
+
const changed = [];
|
|
52
|
+
const manual = [];
|
|
53
|
+
let rewrites = 0;
|
|
54
|
+
for (const sourceFile of built.getSourceFiles()) {
|
|
55
|
+
const fileName = resolve(sourceFile.fileName);
|
|
56
|
+
if (sourceFile.isDeclarationFile || fileName.includes('/node_modules/') || library.isLibraryFile(fileName)) continue;
|
|
57
|
+
if (selected && !selected.has(fileName)) continue;
|
|
58
|
+
const fileLabel = relative(root, fileName).replaceAll('\\', '/');
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = rewriteSourceFile({
|
|
62
|
+
ts, checker, program: built, sourceFile, library, index, transforms, writableSourceFiles,
|
|
63
|
+
manualItems: manual,
|
|
64
|
+
fileLabel,
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
manual.push({ file: fileLabel, line: 1, column: 1, reason: `this file was left untouched: ${error.message}`, text: '' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (result.text === sourceFile.text) continue;
|
|
71
|
+
changed.push({ file: fileLabel, rewrites: result.rewrites, text: result.text });
|
|
72
|
+
rewrites += result.rewrites;
|
|
73
|
+
if (write) writeFileSync(fileName, result.text);
|
|
74
|
+
}
|
|
75
|
+
manual.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.column - right.column);
|
|
76
|
+
return { files: changed, manual, rewrites };
|
|
77
|
+
}
|
package/lib/glob.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// tools/codemod/lib/glob.mjs
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const WILDCARD = /[*?]/;
|
|
6
|
+
|
|
7
|
+
function toRegExp(pattern) {
|
|
8
|
+
let source = '';
|
|
9
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
10
|
+
const character = pattern[index];
|
|
11
|
+
if (character === '*' && pattern[index + 1] === '*') {
|
|
12
|
+
// `**/` matches any number of directories, including none.
|
|
13
|
+
if (pattern[index + 2] === '/') { source += '(?:.*/)?'; index += 2; } else { source += '.*'; index += 1; }
|
|
14
|
+
} else if (character === '*') source += '[^/]*';
|
|
15
|
+
else if (character === '?') source += '[^/]';
|
|
16
|
+
else source += character.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
}
|
|
18
|
+
return new RegExp(`^${source}$`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Expand one glob (`*`, `**`, `?`) relative to `root` into sorted absolute file paths.
|
|
23
|
+
* A pattern without wildcards names one file. `node_modules` directories are never entered.
|
|
24
|
+
* @param {string} pattern
|
|
25
|
+
* @param {string} root
|
|
26
|
+
* @returns {string[]}
|
|
27
|
+
*/
|
|
28
|
+
export function expandGlob(pattern, root) {
|
|
29
|
+
const normalized = pattern.replaceAll('\\', '/');
|
|
30
|
+
const segments = normalized.split('/');
|
|
31
|
+
if (segments.includes('node_modules')) return [];
|
|
32
|
+
if (!WILDCARD.test(normalized)) {
|
|
33
|
+
const file = resolve(root, normalized);
|
|
34
|
+
return existsSync(file) && statSync(file).isFile() ? [file] : [];
|
|
35
|
+
}
|
|
36
|
+
const firstWild = segments.findIndex(segment => WILDCARD.test(segment));
|
|
37
|
+
const base = resolve(root, segments.slice(0, firstWild).join('/') || '.');
|
|
38
|
+
if (!existsSync(base)) return [];
|
|
39
|
+
const matcher = toRegExp(segments.slice(firstWild).join('/'));
|
|
40
|
+
const found = [];
|
|
41
|
+
const walk = (directory, prefix) => {
|
|
42
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
43
|
+
if (entry.name === 'node_modules') continue;
|
|
44
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
45
|
+
if (entry.isDirectory()) walk(resolve(directory, entry.name), relative);
|
|
46
|
+
else if (matcher.test(relative)) found.push(resolve(directory, entry.name));
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
walk(base, '');
|
|
50
|
+
return found.sort();
|
|
51
|
+
}
|
package/lib/library.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// tools/codemod/lib/library.mjs
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const normalize = file => file.replaceAll('\\', '/');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Decides which declarations belong to DI Bag and names their owner.
|
|
8
|
+
* With `libraryRoots` the library is those directories (this repository runs with `src` and `dist`);
|
|
9
|
+
* without any, the library is every `node_modules/di-bag/` directory.
|
|
10
|
+
*/
|
|
11
|
+
export function createLibrary({ ts, checker, root, libraryRoots = [] }) {
|
|
12
|
+
const prefixes = libraryRoots.map(directory => `${normalize(resolve(root, directory))}/`);
|
|
13
|
+
const isLibraryFile = fileName => {
|
|
14
|
+
const file = normalize(fileName);
|
|
15
|
+
return prefixes.length === 0 ? file.includes('/node_modules/di-bag/') : prefixes.some(prefix => file.startsWith(prefix));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The symbol a name refers to, with import aliases followed. */
|
|
19
|
+
function symbolAt(node) {
|
|
20
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
21
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
22
|
+
return symbol;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The owner of a member declaration: the nearest enclosing named declaration.
|
|
27
|
+
* A class, interface or type alias gives `Name`; a function gives `name()`;
|
|
28
|
+
* a method gives `Owner.method()`. A type literal written inline in a signature
|
|
29
|
+
* therefore belongs to that function or method.
|
|
30
|
+
*/
|
|
31
|
+
function ownerOf(declaration) {
|
|
32
|
+
for (let node = declaration.parent; node; node = node.parent) {
|
|
33
|
+
if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && node.name) return node.name.text;
|
|
34
|
+
if (ts.isFunctionDeclaration(node) && node.name) return `${node.name.text}()`;
|
|
35
|
+
if ((ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) && ts.isIdentifier(node.name)) {
|
|
36
|
+
const container = node.parent;
|
|
37
|
+
if ((ts.isClassDeclaration(container) || ts.isInterfaceDeclaration(container)) && container.name) return `${container.name.text}.${node.name.text}()`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Library members plus whether every declaration of the symbol belongs to a named library owner. */
|
|
44
|
+
function memberCoverage(symbol) {
|
|
45
|
+
const members = new Map();
|
|
46
|
+
const declarations = symbol?.declarations ?? [];
|
|
47
|
+
let complete = declarations.length > 0;
|
|
48
|
+
for (const declaration of declarations) {
|
|
49
|
+
if (!isLibraryFile(declaration.getSourceFile().fileName)) { complete = false; continue; }
|
|
50
|
+
const owner = ownerOf(declaration);
|
|
51
|
+
if (owner === undefined) { complete = false; continue; }
|
|
52
|
+
members.set(`${owner}.${symbol.name}`, { owner, name: symbol.name });
|
|
53
|
+
}
|
|
54
|
+
return { members: [...members.values()], complete };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Every distinct `{ owner, name }` a member symbol is declared as inside the library. */
|
|
58
|
+
const membersOf = symbol => memberCoverage(symbol).members;
|
|
59
|
+
|
|
60
|
+
/** The exported name when the symbol is a top-level declaration of the library, else undefined. */
|
|
61
|
+
function exportNameOf(symbol) {
|
|
62
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
63
|
+
if (!isLibraryFile(declaration.getSourceFile().fileName)) continue;
|
|
64
|
+
const holder = ts.isVariableDeclaration(declaration) ? declaration.parent?.parent : declaration;
|
|
65
|
+
if (holder?.parent && ts.isSourceFile(holder.parent)) return symbol.name;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { isLibraryFile, symbolAt, membersOf, memberCoverage, exportNameOf };
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// tools/codemod/lib/load-typescript.mjs
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function atLeast(version, minimum) {
|
|
6
|
+
const parts = String(version).split(/[.-]/).map(Number);
|
|
7
|
+
for (let index = 0; index < minimum.length; index++) {
|
|
8
|
+
if ((parts[index] ?? 0) !== minimum[index]) return (parts[index] ?? 0) > minimum[index];
|
|
9
|
+
}
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The project's `typescript` when it exposes the compiler API at 6.0.3 or later, else the bundled copy.
|
|
15
|
+
* A newer project compiler without a compatible JavaScript API is analyzed with the bundled 6.
|
|
16
|
+
* @param {string} from - A directory inside the project.
|
|
17
|
+
* @returns {{ ts: typeof import('typescript'), version: string, source: 'project' | 'bundled' }}
|
|
18
|
+
*/
|
|
19
|
+
export function loadTypeScript(from) {
|
|
20
|
+
const bundledPath = createRequire(import.meta.url).resolve('typescript');
|
|
21
|
+
try {
|
|
22
|
+
const require = createRequire(resolve(from, 'package.json'));
|
|
23
|
+
const path = require.resolve('typescript');
|
|
24
|
+
const candidate = require(path);
|
|
25
|
+
if (path !== bundledPath && typeof candidate.createProgram === 'function' && atLeast(candidate.version, [6, 0, 3])) {
|
|
26
|
+
return { ts: candidate, version: candidate.version, source: 'project' };
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// No resolvable typescript in the project.
|
|
30
|
+
}
|
|
31
|
+
const bundled = createRequire(import.meta.url)(bundledPath);
|
|
32
|
+
return { ts: bundled, version: bundled.version, source: 'bundled' };
|
|
33
|
+
}
|