kodu 2.0.2 → 2.1.1
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 +36 -0
- package/dist/package.json +1 -1
- package/dist/src/commands/pack/pack.command.d.ts +10 -2
- package/dist/src/commands/pack/pack.command.js +92 -11
- package/dist/src/commands/pack/pack.command.js.map +1 -1
- package/dist/src/commands/pack/pack.module.js +2 -1
- package/dist/src/commands/pack/pack.module.js.map +1 -1
- package/dist/src/shared/deps/deps.module.d.ts +2 -0
- package/dist/src/shared/deps/deps.module.js +21 -0
- package/dist/src/shared/deps/deps.module.js.map +1 -0
- package/dist/src/shared/deps/deps.service.d.ts +15 -0
- package/dist/src/shared/deps/deps.service.js +114 -0
- package/dist/src/shared/deps/deps.service.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/commands/pack/pack.command.ts +87 -9
- package/src/commands/pack/pack.module.ts +2 -1
- package/src/shared/deps/deps.module.ts +8 -0
- package/src/shared/deps/deps.service.ts +175 -0
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { PromptService } from '../../core/config/prompt.service';
|
|
|
7
7
|
import { FsService } from '../../core/file-system/fs.service';
|
|
8
8
|
import { UiService } from '../../core/ui/ui.service';
|
|
9
9
|
import { CleanerService } from '../../shared/cleaner/cleaner.service';
|
|
10
|
+
import { DepsService } from '../../shared/deps/deps.service';
|
|
10
11
|
import { TokenizerService } from '../../shared/tokenizer/tokenizer.service';
|
|
11
12
|
|
|
12
13
|
type OutputFormat = 'xml' | 'text';
|
|
@@ -20,6 +21,9 @@ type PackOptions = {
|
|
|
20
21
|
list?: boolean;
|
|
21
22
|
format?: OutputFormat;
|
|
22
23
|
clean?: boolean;
|
|
24
|
+
deps?: boolean;
|
|
25
|
+
depsDepth?: number;
|
|
26
|
+
explain?: boolean;
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
type TemplateContext = {
|
|
@@ -41,6 +45,7 @@ export class PackCommand extends CommandRunner {
|
|
|
41
45
|
private readonly fsService: FsService,
|
|
42
46
|
private readonly tokenizer: TokenizerService,
|
|
43
47
|
private readonly cleaner: CleanerService,
|
|
48
|
+
private readonly depsService: DepsService,
|
|
44
49
|
) {
|
|
45
50
|
super();
|
|
46
51
|
}
|
|
@@ -98,6 +103,36 @@ export class PackCommand extends CommandRunner {
|
|
|
98
103
|
return true;
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
@Option({
|
|
107
|
+
flags: '--deps',
|
|
108
|
+
description:
|
|
109
|
+
'Trace imports from entry point(s) and include their dependencies',
|
|
110
|
+
})
|
|
111
|
+
parseDeps(): boolean {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Option({
|
|
116
|
+
flags: '--deps-depth <n>',
|
|
117
|
+
description: 'Max import depth when using --deps (default: unlimited)',
|
|
118
|
+
})
|
|
119
|
+
parseDepsDepth(value: string): number {
|
|
120
|
+
const n = Number.parseInt(value, 10);
|
|
121
|
+
if (Number.isNaN(n) || n < 1) {
|
|
122
|
+
this.ui.log.warn(`Invalid --deps-depth "${value}", ignoring`);
|
|
123
|
+
return Infinity;
|
|
124
|
+
}
|
|
125
|
+
return n;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Option({
|
|
129
|
+
flags: '--explain',
|
|
130
|
+
description: 'Print why each file was included (requires --deps)',
|
|
131
|
+
})
|
|
132
|
+
parseExplain(): boolean {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
@Option({
|
|
102
137
|
flags: '-f, --format <format>',
|
|
103
138
|
description: 'Output format: xml (default) or text',
|
|
@@ -110,7 +145,7 @@ export class PackCommand extends CommandRunner {
|
|
|
110
145
|
return value;
|
|
111
146
|
}
|
|
112
147
|
|
|
113
|
-
async run(
|
|
148
|
+
async run(inputs: string[], options: PackOptions): Promise<void> {
|
|
114
149
|
const spinner = this.ui
|
|
115
150
|
.createSpinner({ text: 'Collecting files...' })
|
|
116
151
|
.start();
|
|
@@ -118,13 +153,39 @@ export class PackCommand extends CommandRunner {
|
|
|
118
153
|
try {
|
|
119
154
|
const { packer } = this.configService.getConfig();
|
|
120
155
|
const extraExcludes = options.exclude ?? [];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
|
|
157
|
+
let files: string[];
|
|
158
|
+
let explainMap: Map<string, string> | undefined;
|
|
159
|
+
|
|
160
|
+
if (options.deps) {
|
|
161
|
+
if (inputs.length === 0) {
|
|
162
|
+
spinner.error('--deps requires at least one entry file as argument');
|
|
163
|
+
this.ui.log.error('Usage: kodu pack <entry.ts> [more.ts...] --deps');
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
spinner.text = 'Resolving dependency graph...';
|
|
169
|
+
const result = this.depsService.collectDependencies(
|
|
170
|
+
inputs,
|
|
171
|
+
process.cwd(),
|
|
172
|
+
{
|
|
173
|
+
maxDepth: options.depsDepth,
|
|
174
|
+
includeTypes: true,
|
|
175
|
+
includeDynamic: false,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
files = result.files;
|
|
179
|
+
explainMap = result.explain;
|
|
180
|
+
} else {
|
|
181
|
+
files = await this.fsService.findProjectFiles({
|
|
182
|
+
excludeBinary: true,
|
|
183
|
+
useGitignore: packer.useGitignore,
|
|
184
|
+
ignore: [...packer.ignore, ...extraExcludes],
|
|
185
|
+
contentBasedBinaryDetection: packer.contentBasedBinaryDetection,
|
|
186
|
+
rootPaths: inputs.length > 0 ? inputs : options.path,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
128
189
|
|
|
129
190
|
if (files.length === 0) {
|
|
130
191
|
spinner.stop('No files to pack.');
|
|
@@ -135,11 +196,28 @@ export class PackCommand extends CommandRunner {
|
|
|
135
196
|
if (options.list) {
|
|
136
197
|
spinner.success(`Found ${files.length} files`);
|
|
137
198
|
for (const file of files) {
|
|
138
|
-
|
|
199
|
+
if (options.explain && explainMap) {
|
|
200
|
+
const absFile = path.resolve(process.cwd(), file);
|
|
201
|
+
const reason =
|
|
202
|
+
explainMap.get(absFile) ?? explainMap.get(file) ?? '';
|
|
203
|
+
this.ui.log.info(`${file} ← ${reason}`);
|
|
204
|
+
} else {
|
|
205
|
+
this.ui.log.info(file);
|
|
206
|
+
}
|
|
139
207
|
}
|
|
140
208
|
return;
|
|
141
209
|
}
|
|
142
210
|
|
|
211
|
+
if (options.explain && explainMap) {
|
|
212
|
+
spinner.success(`Found ${files.length} files`);
|
|
213
|
+
this.ui.log.info('Dependency explanation:');
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
const absFile = path.resolve(process.cwd(), file);
|
|
216
|
+
const reason = explainMap.get(absFile) ?? explainMap.get(file) ?? '';
|
|
217
|
+
this.ui.log.info(` ${file} ← ${reason}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
143
221
|
const format: OutputFormat = options.format ?? 'xml';
|
|
144
222
|
const context = await this.buildContext(files, format, options.clean);
|
|
145
223
|
const fileList = files.join('\n');
|
|
@@ -3,11 +3,12 @@ import { ConfigModule } from '../../core/config/config.module';
|
|
|
3
3
|
import { FsModule } from '../../core/file-system/fs.module';
|
|
4
4
|
import { UiModule } from '../../core/ui/ui.module';
|
|
5
5
|
import { CleanerService } from '../../shared/cleaner/cleaner.service';
|
|
6
|
+
import { DepsModule } from '../../shared/deps/deps.module';
|
|
6
7
|
import { TokenizerModule } from '../../shared/tokenizer/tokenizer.module';
|
|
7
8
|
import { PackCommand } from './pack.command';
|
|
8
9
|
|
|
9
10
|
@Module({
|
|
10
|
-
imports: [ConfigModule, UiModule, FsModule, TokenizerModule],
|
|
11
|
+
imports: [ConfigModule, UiModule, FsModule, TokenizerModule, DepsModule],
|
|
11
12
|
providers: [PackCommand, CleanerService],
|
|
12
13
|
})
|
|
13
14
|
export class PackModule {}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import { Project } from 'ts-morph';
|
|
4
|
+
|
|
5
|
+
export type DepsResult = {
|
|
6
|
+
files: string[];
|
|
7
|
+
explain: Map<string, string>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type CollectOptions = {
|
|
11
|
+
maxDepth?: number;
|
|
12
|
+
includeTypes?: boolean;
|
|
13
|
+
includeDynamic?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class DepsService {
|
|
18
|
+
collectDependencies(
|
|
19
|
+
entryFiles: string[],
|
|
20
|
+
projectRoot: string,
|
|
21
|
+
options: CollectOptions = {},
|
|
22
|
+
): DepsResult {
|
|
23
|
+
const {
|
|
24
|
+
maxDepth = Infinity,
|
|
25
|
+
includeTypes = true,
|
|
26
|
+
includeDynamic = false,
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
const tsConfigPath = this.findTsConfig(projectRoot);
|
|
30
|
+
const project = tsConfigPath
|
|
31
|
+
? new Project({
|
|
32
|
+
tsConfigFilePath: tsConfigPath,
|
|
33
|
+
skipAddingFilesFromTsConfig: true,
|
|
34
|
+
})
|
|
35
|
+
: new Project({
|
|
36
|
+
compilerOptions: {
|
|
37
|
+
allowJs: true,
|
|
38
|
+
resolveJsonModule: true,
|
|
39
|
+
moduleResolution: 2, // NodeJs
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const visited = new Set<string>();
|
|
44
|
+
const explain = new Map<string, string>();
|
|
45
|
+
|
|
46
|
+
const absEntries = entryFiles.map((f) =>
|
|
47
|
+
path.isAbsolute(f) ? f : path.resolve(projectRoot, f),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
for (const entry of absEntries) {
|
|
51
|
+
explain.set(entry, 'entry point');
|
|
52
|
+
this.collect(
|
|
53
|
+
project,
|
|
54
|
+
entry,
|
|
55
|
+
projectRoot,
|
|
56
|
+
visited,
|
|
57
|
+
explain,
|
|
58
|
+
0,
|
|
59
|
+
maxDepth,
|
|
60
|
+
includeTypes,
|
|
61
|
+
includeDynamic,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const files = [...visited].map((abs) =>
|
|
66
|
+
path.relative(projectRoot, abs).split(path.sep).join(path.posix.sep),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return { files, explain };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private collect(
|
|
73
|
+
project: Project,
|
|
74
|
+
absFile: string,
|
|
75
|
+
projectRoot: string,
|
|
76
|
+
visited: Set<string>,
|
|
77
|
+
explain: Map<string, string>,
|
|
78
|
+
depth: number,
|
|
79
|
+
maxDepth: number,
|
|
80
|
+
includeTypes: boolean,
|
|
81
|
+
includeDynamic: boolean,
|
|
82
|
+
): void {
|
|
83
|
+
if (visited.has(absFile)) return;
|
|
84
|
+
visited.add(absFile);
|
|
85
|
+
|
|
86
|
+
if (depth >= maxDepth) return;
|
|
87
|
+
|
|
88
|
+
let sourceFile = project.getSourceFile(absFile);
|
|
89
|
+
if (!sourceFile) {
|
|
90
|
+
try {
|
|
91
|
+
sourceFile = project.addSourceFileAtPath(absFile);
|
|
92
|
+
} catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const relFrom = path
|
|
98
|
+
.relative(projectRoot, absFile)
|
|
99
|
+
.split(path.sep)
|
|
100
|
+
.join(path.posix.sep);
|
|
101
|
+
|
|
102
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
103
|
+
if (!includeTypes && importDecl.isTypeOnly()) continue;
|
|
104
|
+
|
|
105
|
+
const resolved = importDecl.getModuleSpecifierSourceFile();
|
|
106
|
+
if (!resolved) continue;
|
|
107
|
+
|
|
108
|
+
const absResolved = resolved.getFilePath();
|
|
109
|
+
if (absResolved.includes('node_modules')) continue;
|
|
110
|
+
|
|
111
|
+
if (!explain.has(absResolved)) {
|
|
112
|
+
const what = importDecl.isTypeOnly() ? 'type import' : 'import';
|
|
113
|
+
explain.set(absResolved, `${what} from ${relFrom}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.collect(
|
|
117
|
+
project,
|
|
118
|
+
absResolved,
|
|
119
|
+
projectRoot,
|
|
120
|
+
visited,
|
|
121
|
+
explain,
|
|
122
|
+
depth + 1,
|
|
123
|
+
maxDepth,
|
|
124
|
+
includeTypes,
|
|
125
|
+
includeDynamic,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
void includeDynamic;
|
|
130
|
+
|
|
131
|
+
for (const exportDecl of sourceFile.getExportDeclarations()) {
|
|
132
|
+
const resolved = exportDecl.getModuleSpecifierSourceFile();
|
|
133
|
+
if (!resolved) continue;
|
|
134
|
+
|
|
135
|
+
const absResolved = resolved.getFilePath();
|
|
136
|
+
if (absResolved.includes('node_modules')) continue;
|
|
137
|
+
|
|
138
|
+
if (!explain.has(absResolved)) {
|
|
139
|
+
const relFrom2 = path
|
|
140
|
+
.relative(projectRoot, absFile)
|
|
141
|
+
.split(path.sep)
|
|
142
|
+
.join(path.posix.sep);
|
|
143
|
+
explain.set(absResolved, `re-export from ${relFrom2}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.collect(
|
|
147
|
+
project,
|
|
148
|
+
absResolved,
|
|
149
|
+
projectRoot,
|
|
150
|
+
visited,
|
|
151
|
+
explain,
|
|
152
|
+
depth + 1,
|
|
153
|
+
maxDepth,
|
|
154
|
+
includeTypes,
|
|
155
|
+
includeDynamic,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private findTsConfig(projectRoot: string): string | undefined {
|
|
161
|
+
const candidates = [
|
|
162
|
+
path.join(projectRoot, 'tsconfig.json'),
|
|
163
|
+
path.join(projectRoot, 'tsconfig.base.json'),
|
|
164
|
+
];
|
|
165
|
+
for (const c of candidates) {
|
|
166
|
+
try {
|
|
167
|
+
require('node:fs').accessSync(c);
|
|
168
|
+
return c;
|
|
169
|
+
} catch {
|
|
170
|
+
// not found
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|