@reliverse/dler 1.2.3 → 1.2.5
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 +14 -30
- package/bin/app/cmds.js +6 -0
- package/bin/app/inject/README.md +20 -20
- package/bin/app/inject/cmd.js +7 -3
- package/bin/app/inject/expect/cmd.js +43 -0
- package/bin/app/inject/expect/impl.js +162 -0
- package/bin/app/relifso/cmd.js +36 -5
- package/bin/app/{rempts/cmd.txt → relifso/init/cmd.js} +93 -121
- package/bin/app/relifso/init/impl/mod.js +248 -0
- package/bin/app/rempts/{impl/index.js → init/cmd/cmd.js} +1 -1
- package/bin/app/rempts/init/cmds/cmd.js +80 -0
- package/bin/app/rempts/migrate/cmd.js +38 -0
- package/bin/app/rempts/migrate/impl/commander.js +265 -0
- package/bin/init.js +4 -0
- package/bin/libs/cfg/cfg-default.js +3 -1
- package/package.json +3 -2
- package/bin/app/auth/cmd.js +0 -0
- package/bin/app/inject/impl/arg-ts-expect-error.txt +0 -49
- package/bin/app/inject/impl/inject-mod.txt +0 -28
- package/bin/app/inject/impl/reinject.config.js +0 -4
- package/bin/app/inject/impl/ts-expect-error.txt +0 -277
- package/bin/app/relifso/impl/reinit-mod.txt +0 -395
- /package/bin/app/relifso/{impl → init/impl}/const.js +0 -0
- /package/bin/app/relifso/{impl → init/impl}/templates/t-gitignore.js +0 -0
- /package/bin/app/relifso/{impl → init/impl}/templates/t-license.js +0 -0
- /package/bin/app/relifso/{impl → init/impl}/templates/t-readme.js +0 -0
- /package/bin/app/relifso/{impl → init/impl}/types.js +0 -0
- /package/bin/app/relifso/{impl → init/impl}/utils.js +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
import {
|
|
3
|
+
Project,
|
|
4
|
+
Node,
|
|
5
|
+
ScriptTarget,
|
|
6
|
+
ModuleKind,
|
|
7
|
+
SyntaxKind
|
|
8
|
+
} from "ts-morph";
|
|
9
|
+
function parseCommanderFlags(flags) {
|
|
10
|
+
const parts = flags.split(/[\s,]+/).filter(Boolean);
|
|
11
|
+
let longName = "";
|
|
12
|
+
let shortName;
|
|
13
|
+
let takesValue = false;
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
if (part.startsWith("--")) {
|
|
16
|
+
longName = part.substring(2);
|
|
17
|
+
} else if (part.startsWith("-") && part.length === 2) {
|
|
18
|
+
shortName = part.substring(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (flags.includes("<") || flags.includes("[")) {
|
|
22
|
+
takesValue = true;
|
|
23
|
+
longName = longName.replace(/[<[].*/, "").trim();
|
|
24
|
+
}
|
|
25
|
+
if (!longName && shortName) {
|
|
26
|
+
if (flags.includes(`<${shortName}>`)) takesValue = true;
|
|
27
|
+
longName = shortName;
|
|
28
|
+
}
|
|
29
|
+
if (!longName) {
|
|
30
|
+
throw new Error(`Could not parse flags: ${flags}`);
|
|
31
|
+
}
|
|
32
|
+
return { longName, shortName, takesValue };
|
|
33
|
+
}
|
|
34
|
+
function getDefaultValueText(node) {
|
|
35
|
+
if (!node) return void 0;
|
|
36
|
+
if (Node.isLiteralExpression(node)) {
|
|
37
|
+
return node.getText();
|
|
38
|
+
}
|
|
39
|
+
if (Node.isIdentifier(node) || Node.isPropertyAccessExpression(node)) {
|
|
40
|
+
return node.getText();
|
|
41
|
+
}
|
|
42
|
+
if (Node.isCallExpression(node)) {
|
|
43
|
+
return node.getText();
|
|
44
|
+
}
|
|
45
|
+
if (Node.isPrefixUnaryExpression(node)) {
|
|
46
|
+
return node.getText();
|
|
47
|
+
}
|
|
48
|
+
console.warn(
|
|
49
|
+
`Unhandled default value type: ${node.getKindName()}, text: ${node.getText()}`
|
|
50
|
+
);
|
|
51
|
+
return node.getText();
|
|
52
|
+
}
|
|
53
|
+
async function transformCommand(varDecl, info, sourceFile) {
|
|
54
|
+
const commandName = info.commandName;
|
|
55
|
+
if (!commandName) {
|
|
56
|
+
console.warn("No command name found");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let actionBodyText = "";
|
|
60
|
+
if (info.actionFunction) {
|
|
61
|
+
if (Node.isArrowFunction(info.actionFunction) || Node.isFunctionExpression(info.actionFunction) || Node.isFunctionDeclaration(info.actionFunction)) {
|
|
62
|
+
const body = info.actionFunction.getBody();
|
|
63
|
+
if (body) {
|
|
64
|
+
actionBodyText = body.getText();
|
|
65
|
+
if (Node.isBlock(body)) {
|
|
66
|
+
actionBodyText = actionBodyText.slice(1, -1).trim();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
console.warn(
|
|
71
|
+
`Unhandled action function type: ${info.actionFunction.getKindName()}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
info.options.reverse();
|
|
76
|
+
if (info.actionFunctionParam) {
|
|
77
|
+
const paramRegex = new RegExp(`\\b${info.actionFunctionParam}\\b`, "g");
|
|
78
|
+
actionBodyText = actionBodyText.replace(paramRegex, "args");
|
|
79
|
+
}
|
|
80
|
+
const argsProperties = info.options.map((opt) => {
|
|
81
|
+
const defaultValueStr = opt.defaultValue ? `,
|
|
82
|
+
default: ${String(opt.defaultValue)}` : "";
|
|
83
|
+
const requiredStr = opt.required ? ",\n required: true" : "";
|
|
84
|
+
return ` ${opt.name}: {
|
|
85
|
+
type: "${opt.type}",
|
|
86
|
+
description: "${opt.description}"${defaultValueStr}${requiredStr}
|
|
87
|
+
}`;
|
|
88
|
+
}).join(",\n");
|
|
89
|
+
const defineCommandText = `
|
|
90
|
+
export const ${varDecl.getName()} = defineCommand({
|
|
91
|
+
meta: {
|
|
92
|
+
name: "${commandName}",
|
|
93
|
+
version: "${info.version || "1.0.0"}",
|
|
94
|
+
description: "${info.description || "Migrated from Commander"}",
|
|
95
|
+
},
|
|
96
|
+
args: {
|
|
97
|
+
${argsProperties}
|
|
98
|
+
},
|
|
99
|
+
async run({ args }) {
|
|
100
|
+
${actionBodyText}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Add runMain at the end of the file
|
|
105
|
+
await runMain(${varDecl.getName()});
|
|
106
|
+
`;
|
|
107
|
+
varDecl.replaceWithText(defineCommandText);
|
|
108
|
+
if (info.actionFunction && Node.isIdentifier(info.actionFunction)) {
|
|
109
|
+
const funcName = info.actionFunction.getText();
|
|
110
|
+
const funcDecl = sourceFile.getFunction(funcName);
|
|
111
|
+
if (funcDecl) {
|
|
112
|
+
funcDecl.remove();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
sourceFile.fixMissingImports();
|
|
116
|
+
await sourceFile.save();
|
|
117
|
+
console.log(`Transformed command in: ${sourceFile.getFilePath()}`);
|
|
118
|
+
}
|
|
119
|
+
function extractCommandInfo(node, sourceFile) {
|
|
120
|
+
const info = {
|
|
121
|
+
options: []
|
|
122
|
+
};
|
|
123
|
+
if (Node.isNewExpression(node)) {
|
|
124
|
+
const expr = node.getExpression();
|
|
125
|
+
if (expr && Node.isIdentifier(expr) && expr.getText() === "Command") {
|
|
126
|
+
const commandArg = node.getArguments()[0];
|
|
127
|
+
if (commandArg && Node.isStringLiteral(commandArg)) {
|
|
128
|
+
info.commandName = commandArg.getLiteralText();
|
|
129
|
+
}
|
|
130
|
+
return info;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (Node.isCallExpression(node)) {
|
|
134
|
+
let tempExpr = node;
|
|
135
|
+
while (tempExpr && Node.isCallExpression(tempExpr)) {
|
|
136
|
+
const expression = tempExpr.getExpression();
|
|
137
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
138
|
+
const methodName = expression.getName();
|
|
139
|
+
switch (methodName) {
|
|
140
|
+
case "action":
|
|
141
|
+
handleActionMethod(tempExpr, info, sourceFile);
|
|
142
|
+
break;
|
|
143
|
+
case "option":
|
|
144
|
+
handleOptionMethod(tempExpr, info);
|
|
145
|
+
break;
|
|
146
|
+
case "description":
|
|
147
|
+
handleDescriptionMethod(tempExpr, info);
|
|
148
|
+
break;
|
|
149
|
+
case "version":
|
|
150
|
+
handleVersionMethod(tempExpr, info);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const parent = tempExpr.getParent();
|
|
155
|
+
tempExpr = Node.isCallExpression(parent) ? parent : void 0;
|
|
156
|
+
}
|
|
157
|
+
return info;
|
|
158
|
+
}
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
function handleActionMethod(node, info, sourceFile) {
|
|
162
|
+
const [actionArg] = node.getArguments();
|
|
163
|
+
if (actionArg && Node.isIdentifier(actionArg)) {
|
|
164
|
+
const funcDef = sourceFile.getFunction(actionArg.getText()) || sourceFile.getVariableDeclaration(actionArg.getText())?.getInitializer();
|
|
165
|
+
if (funcDef && (Node.isArrowFunction(funcDef) || Node.isFunctionDeclaration(funcDef) || Node.isFunctionExpression(funcDef))) {
|
|
166
|
+
info.actionFunction = funcDef;
|
|
167
|
+
const firstParam = funcDef.getParameters()[0];
|
|
168
|
+
if (firstParam) {
|
|
169
|
+
info.actionFunctionParam = firstParam.getName();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function handleOptionMethod(node, info) {
|
|
175
|
+
const [flagsArg, descArg, defaultValueArg] = node.getArguments();
|
|
176
|
+
if (flagsArg && Node.isStringLiteral(flagsArg)) {
|
|
177
|
+
const { longName, shortName, takesValue } = parseCommanderFlags(
|
|
178
|
+
flagsArg.getLiteralText()
|
|
179
|
+
);
|
|
180
|
+
info.options.push({
|
|
181
|
+
name: longName,
|
|
182
|
+
shortFlag: shortName,
|
|
183
|
+
type: takesValue ? "string" : "boolean",
|
|
184
|
+
description: Node.isStringLiteral(descArg) ? descArg.getLiteralText() : "TODO: Add description",
|
|
185
|
+
defaultValue: defaultValueArg ? getDefaultValueText(defaultValueArg) : void 0
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function handleDescriptionMethod(node, info) {
|
|
190
|
+
const [descArg] = node.getArguments();
|
|
191
|
+
if (descArg && Node.isStringLiteral(descArg)) {
|
|
192
|
+
info.description = descArg.getLiteralText();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function handleVersionMethod(node, info) {
|
|
196
|
+
const [versionArg] = node.getArguments();
|
|
197
|
+
if (versionArg && Node.isStringLiteral(versionArg)) {
|
|
198
|
+
info.version = versionArg.getLiteralText();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export async function commanderToRempts(targetDirectory) {
|
|
202
|
+
if (!targetDirectory) {
|
|
203
|
+
throw new Error("Target directory is required");
|
|
204
|
+
}
|
|
205
|
+
const project = new Project({
|
|
206
|
+
compilerOptions: {
|
|
207
|
+
target: ScriptTarget.ESNext,
|
|
208
|
+
module: ModuleKind.ESNext,
|
|
209
|
+
esModuleInterop: true,
|
|
210
|
+
skipLibCheck: true
|
|
211
|
+
},
|
|
212
|
+
skipAddingFilesFromTsConfig: true
|
|
213
|
+
});
|
|
214
|
+
const filePaths = await glob(`${targetDirectory}/**/*.ts`, {
|
|
215
|
+
ignore: ["**/node_modules/**", "**/*.d.ts"],
|
|
216
|
+
absolute: true
|
|
217
|
+
});
|
|
218
|
+
for (const filePath of filePaths) {
|
|
219
|
+
console.log(`Processing: ${filePath}`);
|
|
220
|
+
project.addSourceFileAtPath(filePath);
|
|
221
|
+
try {
|
|
222
|
+
await transformFile(filePath, project);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`Error transforming file ${filePath}:`, error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
console.log("Migration completed successfully.");
|
|
228
|
+
}
|
|
229
|
+
async function transformFile(filePath, project) {
|
|
230
|
+
const sourceFile = project.getSourceFile(filePath);
|
|
231
|
+
if (!sourceFile) {
|
|
232
|
+
console.error(`Could not find source file: ${filePath}`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const commanderImport = sourceFile.getImportDeclaration("commander");
|
|
236
|
+
if (commanderImport) {
|
|
237
|
+
commanderImport.remove();
|
|
238
|
+
}
|
|
239
|
+
const remptsImport = sourceFile.getImportDeclaration("@reliverse/rempts");
|
|
240
|
+
const neededRemptsImports = ["defineCommand", "runMain"];
|
|
241
|
+
if (remptsImport) {
|
|
242
|
+
const existingNamedImports = remptsImport.getNamedImports().map((ni) => ni.getName());
|
|
243
|
+
for (const neededImport of neededRemptsImports) {
|
|
244
|
+
if (!existingNamedImports.includes(neededImport)) {
|
|
245
|
+
remptsImport.addNamedImport(neededImport);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
sourceFile.addImportDeclaration({
|
|
250
|
+
moduleSpecifier: "@reliverse/rempts",
|
|
251
|
+
namedImports: neededRemptsImports
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(
|
|
255
|
+
SyntaxKind.VariableDeclaration
|
|
256
|
+
);
|
|
257
|
+
for (const varDecl of variableDeclarations) {
|
|
258
|
+
const initializer = varDecl.getInitializer();
|
|
259
|
+
if (!initializer) continue;
|
|
260
|
+
const commandInfo = extractCommandInfo(initializer, sourceFile);
|
|
261
|
+
if (commandInfo) {
|
|
262
|
+
await transformCommand(varDecl, commandInfo, sourceFile);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
package/bin/init.js
CHANGED
|
@@ -164,6 +164,10 @@ export default defineConfig({
|
|
|
164
164
|
transpileStub: ${DEFAULT_CONFIG.transpileStub},
|
|
165
165
|
transpileTarget: "${DEFAULT_CONFIG.transpileTarget}",
|
|
166
166
|
transpileWatch: ${DEFAULT_CONFIG.transpileWatch},
|
|
167
|
+
|
|
168
|
+
// Additionals
|
|
169
|
+
injectComment: "${DEFAULT_CONFIG.injectComment}",
|
|
170
|
+
tscCommand: "${DEFAULT_CONFIG.tscCommand}",
|
|
167
171
|
});
|
|
168
172
|
`;
|
|
169
173
|
return configTemplate;
|
|
@@ -47,5 +47,7 @@ export const DEFAULT_CONFIG = {
|
|
|
47
47
|
transpileSplitting: false,
|
|
48
48
|
transpileStub: false,
|
|
49
49
|
transpileTarget: "node",
|
|
50
|
-
transpileWatch: false
|
|
50
|
+
transpileWatch: false,
|
|
51
|
+
injectComment: "// @ts-expect-error TODO: fix ts",
|
|
52
|
+
tscCommand: "tsc --project ./tsconfig.json --noEmit"
|
|
51
53
|
};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@reliverse/relico": "^1.1.2",
|
|
4
4
|
"@reliverse/relinka": "^1.4.5",
|
|
5
|
-
"@reliverse/rempts": "^1.7.
|
|
5
|
+
"@reliverse/rempts": "^1.7.10",
|
|
6
6
|
"@rollup/plugin-alias": "^5.1.1",
|
|
7
7
|
"@rollup/plugin-commonjs": "^28.0.3",
|
|
8
8
|
"@rollup/plugin-json": "^6.1.0",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"scule": "^1.3.0",
|
|
34
34
|
"semver": "^7.7.2",
|
|
35
35
|
"tinyglobby": "^0.2.13",
|
|
36
|
+
"ts-morph": "^25.0.1",
|
|
36
37
|
"untyped": "^2.0.0"
|
|
37
38
|
},
|
|
38
39
|
"description": "dler (prev. relidler) is a flexible, unified, and fully automated bundler for TypeScript and JavaScript projects, as well as an NPM and JSR publishing tool.",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"license": "MIT",
|
|
41
42
|
"name": "@reliverse/dler",
|
|
42
43
|
"type": "module",
|
|
43
|
-
"version": "1.2.
|
|
44
|
+
"version": "1.2.5",
|
|
44
45
|
"keywords": [
|
|
45
46
|
"reliverse",
|
|
46
47
|
"cli",
|
package/bin/app/auth/cmd.js
DELETED
|
File without changes
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { relinka } from "@reliverse/relinka";
|
|
2
|
-
import { defineCommand } from "@reliverse/rempts";
|
|
3
|
-
|
|
4
|
-
import { useTsExpectError } from "./ts-expect-error.js";
|
|
5
|
-
|
|
6
|
-
export default defineCommand({
|
|
7
|
-
meta: {
|
|
8
|
-
name: "tee",
|
|
9
|
-
description: "Inject `@ts-expect-error` above lines where TS errors occur",
|
|
10
|
-
},
|
|
11
|
-
args: {
|
|
12
|
-
dev: {
|
|
13
|
-
type: "boolean",
|
|
14
|
-
description: "Run the CLI in dev mode",
|
|
15
|
-
},
|
|
16
|
-
files: {
|
|
17
|
-
type: "positional",
|
|
18
|
-
// array: true, // TODO: implement in dler
|
|
19
|
-
required: true,
|
|
20
|
-
description: `'auto' or path(s) to line references file(s)`,
|
|
21
|
-
},
|
|
22
|
-
comment: {
|
|
23
|
-
type: "string",
|
|
24
|
-
required: false,
|
|
25
|
-
description:
|
|
26
|
-
"Override the comment line to insert. Default is `// @ts-expect-error TODO: fix ts`",
|
|
27
|
-
},
|
|
28
|
-
tscPaths: {
|
|
29
|
-
type: "string",
|
|
30
|
-
// array: true,
|
|
31
|
-
required: false,
|
|
32
|
-
description:
|
|
33
|
-
"Optional: specify path(s) to restrict TSC processing (only effective when using 'auto')",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
run: async ({ args }) => {
|
|
37
|
-
if (args.dev) {
|
|
38
|
-
relinka("verbose", "Using dev mode");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await useTsExpectError({
|
|
42
|
-
files: [args.files],
|
|
43
|
-
comment: args.comment,
|
|
44
|
-
tscPaths: [args.tscPaths],
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
process.exit(0);
|
|
48
|
-
},
|
|
49
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// todo: implement
|
|
2
|
-
import { defineCommand, errorHandler, runMain } from "@reliverse/rempts";
|
|
3
|
-
|
|
4
|
-
const main = defineCommand({
|
|
5
|
-
meta: {
|
|
6
|
-
name: "reinject",
|
|
7
|
-
version: "1.0.0",
|
|
8
|
-
description: "@reliverse/reinject-cli",
|
|
9
|
-
},
|
|
10
|
-
args: {
|
|
11
|
-
dev: {
|
|
12
|
-
type: "boolean",
|
|
13
|
-
description: "Runs the CLI in dev mode",
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
subCommands: {
|
|
17
|
-
cli: () => import("./cli/cli-mod.js").then((r) => r.default),
|
|
18
|
-
tee: () =>
|
|
19
|
-
import("./cli/args/arg-ts-expect-error.js").then((r) => r.default),
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
await runMain(main).catch((error: unknown) => {
|
|
24
|
-
errorHandler(
|
|
25
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
26
|
-
"An unhandled error occurred, please report it at https://github.com/reliverse/reinject",
|
|
27
|
-
);
|
|
28
|
-
});
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import { relinka } from "@reliverse/relinka";
|
|
2
|
-
import { loadConfig } from "c12";
|
|
3
|
-
import { execa } from "execa";
|
|
4
|
-
import fs from "fs-extra";
|
|
5
|
-
import path from "pathe";
|
|
6
|
-
|
|
7
|
-
//-------------------------------------
|
|
8
|
-
// 1) c12 config interface
|
|
9
|
-
//-------------------------------------
|
|
10
|
-
type ReinjectUserConfig = {
|
|
11
|
-
// The comment to inject above each error line
|
|
12
|
-
injectComment?: string;
|
|
13
|
-
// The command used to spawn tsc (e.g. "tsc --noEmit --project tsconfig.json")
|
|
14
|
-
tscCommand?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
//-------------------------------------
|
|
18
|
-
// 2) Load c12 configs
|
|
19
|
-
//-------------------------------------
|
|
20
|
-
async function loadReinjectConfig(): Promise<ReinjectUserConfig> {
|
|
21
|
-
const { config } = await loadConfig<ReinjectUserConfig>({
|
|
22
|
-
name: "reinject", // tries reinject.config.*, .reinjectrc, etc.
|
|
23
|
-
defaults: {},
|
|
24
|
-
overrides: {},
|
|
25
|
-
dotenv: false,
|
|
26
|
-
packageJson: false,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
injectComment: config.injectComment ?? "// @ts-expect-error TODO: fix ts",
|
|
31
|
-
tscCommand: config.tscCommand ?? "tsc --noEmit",
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
//-------------------------------------
|
|
36
|
-
// Helper: Parse command string into command and arguments
|
|
37
|
-
//-------------------------------------
|
|
38
|
-
function parseCommand(command: string): { cmd: string; args: string[] } {
|
|
39
|
-
// This parser splits the command by whitespace while handling double or single quotes.
|
|
40
|
-
const regex = /"([^"]+)"|'([^']+)'|(\S+)/g;
|
|
41
|
-
const args: string[] = [];
|
|
42
|
-
let match: RegExpExecArray | null;
|
|
43
|
-
while ((match = regex.exec(command)) !== null) {
|
|
44
|
-
args.push(match[1] || match[2] || match[3]);
|
|
45
|
-
}
|
|
46
|
-
const cmd = args.shift() || "";
|
|
47
|
-
return { cmd, args };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
//-------------------------------------
|
|
51
|
-
// 3) parseLineRefs from a lines file
|
|
52
|
-
//-------------------------------------
|
|
53
|
-
async function parseLinesFile(linesFile: string) {
|
|
54
|
-
const fileContents = await fs.readFile(linesFile, "utf-8");
|
|
55
|
-
const splitted = fileContents.split(/\r?\n/);
|
|
56
|
-
const results: { filePath: string; lineNumber: number }[] = [];
|
|
57
|
-
|
|
58
|
-
for (const rawLine of splitted) {
|
|
59
|
-
const trimmed = rawLine.trim();
|
|
60
|
-
if (!trimmed) continue;
|
|
61
|
-
|
|
62
|
-
// Could match "N path.ts:line"
|
|
63
|
-
let match = trimmed.match(/^(\d+)\s+(.+?):(\d+)$/);
|
|
64
|
-
if (match) {
|
|
65
|
-
results.push({
|
|
66
|
-
filePath: match[2],
|
|
67
|
-
lineNumber: Number.parseInt(match[3], 10),
|
|
68
|
-
});
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Or "path.ts:line"
|
|
73
|
-
match = trimmed.match(/^(.+?):(\d+)$/);
|
|
74
|
-
if (match) {
|
|
75
|
-
results.push({
|
|
76
|
-
filePath: match[1],
|
|
77
|
-
lineNumber: Number.parseInt(match[2], 10),
|
|
78
|
-
});
|
|
79
|
-
} else {
|
|
80
|
-
relinka("warn", `Line doesn't match expected format: ${trimmed}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return results;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
//-------------------------------------
|
|
87
|
-
// 4) runTscAndParseErrors: run tsc with execa, parse error lines
|
|
88
|
-
// Example TSC error line format:
|
|
89
|
-
// src/foo.ts(12,5): error TS2322:
|
|
90
|
-
// We'll capture `src/foo.ts` and `12` as the error line number
|
|
91
|
-
//-------------------------------------
|
|
92
|
-
async function runTscAndParseErrors(
|
|
93
|
-
tscCommand: string,
|
|
94
|
-
tscPaths?: string[],
|
|
95
|
-
): Promise<{ filePath: string; lineNumber: number }[]> {
|
|
96
|
-
const linesRefs: { filePath: string; lineNumber: number }[] = [];
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
// Parse the TSC command into the command and its arguments.
|
|
100
|
-
const { cmd, args: cmdArgs } = parseCommand(tscCommand);
|
|
101
|
-
// Append any additional paths (if provided) as extra arguments.
|
|
102
|
-
if (tscPaths && tscPaths.length > 0) {
|
|
103
|
-
cmdArgs.push(...tscPaths);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Run TSC.
|
|
107
|
-
const subprocess = await execa(cmd, cmdArgs, { all: true, reject: false });
|
|
108
|
-
const combinedOutput = subprocess.all || "";
|
|
109
|
-
const splitted = combinedOutput.split(/\r?\n/);
|
|
110
|
-
const regex = /^(.+?)\((\d+),(\d+)\): error TS\d+: /;
|
|
111
|
-
|
|
112
|
-
for (const line of splitted) {
|
|
113
|
-
const trimmed = line.trim();
|
|
114
|
-
const m = trimmed.match(regex);
|
|
115
|
-
if (m) {
|
|
116
|
-
let file = m[1];
|
|
117
|
-
const row = Number.parseInt(m[2], 10);
|
|
118
|
-
if (row > 0) {
|
|
119
|
-
// Normalize Windows paths.
|
|
120
|
-
file = file.replace(/\\/g, "/");
|
|
121
|
-
linesRefs.push({
|
|
122
|
-
filePath: file,
|
|
123
|
-
lineNumber: row,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch (error: any) {
|
|
129
|
-
// In case of error, try to extract the output.
|
|
130
|
-
const combined = (error.all as string) || "";
|
|
131
|
-
if (!combined) {
|
|
132
|
-
relinka("log", `TSC returned no error lines. Possibly no TS errors?`);
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const splitted = combined.split(/\r?\n/);
|
|
137
|
-
const regex = /^(.+?)\((\d+),(\d+)\): error TS\d+: /;
|
|
138
|
-
|
|
139
|
-
for (const line of splitted) {
|
|
140
|
-
const m = line.trim().match(regex);
|
|
141
|
-
if (m) {
|
|
142
|
-
let file = m[1];
|
|
143
|
-
const row = Number.parseInt(m[2], 10);
|
|
144
|
-
if (row > 0) {
|
|
145
|
-
file = file.replace(/\\/g, "/");
|
|
146
|
-
linesRefs.push({ filePath: file, lineNumber: row });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return linesRefs;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
//-------------------------------------
|
|
156
|
-
// Helper: Check if file is within any of the provided directories
|
|
157
|
-
//-------------------------------------
|
|
158
|
-
function isWithin(filePath: string, dirs: string[]): boolean {
|
|
159
|
-
const absFile = path.resolve(filePath);
|
|
160
|
-
for (const dir of dirs) {
|
|
161
|
-
const absDir = path.resolve(dir);
|
|
162
|
-
// Ensure trailing separator for accurate prefix matching
|
|
163
|
-
const normalizedDir = absDir.endsWith(path.sep)
|
|
164
|
-
? absDir
|
|
165
|
-
: absDir + path.sep;
|
|
166
|
-
if (absFile.startsWith(normalizedDir)) {
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
//-------------------------------------
|
|
174
|
-
// 5) The injection logic
|
|
175
|
-
//-------------------------------------
|
|
176
|
-
async function injectCommentIntoFiles(
|
|
177
|
-
linesRecords: { filePath: string; lineNumber: number }[],
|
|
178
|
-
commentText: string,
|
|
179
|
-
) {
|
|
180
|
-
// Group error lines by file
|
|
181
|
-
const byFile = new Map<string, number[]>();
|
|
182
|
-
for (const rec of linesRecords) {
|
|
183
|
-
if (!byFile.has(rec.filePath)) {
|
|
184
|
-
byFile.set(rec.filePath, []);
|
|
185
|
-
}
|
|
186
|
-
byFile.get(rec.filePath)!.push(rec.lineNumber);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
for (const [filePath, lineNums] of byFile.entries()) {
|
|
190
|
-
// Sort descending so injections don't affect subsequent line numbers
|
|
191
|
-
lineNums.sort((a, b) => b - a);
|
|
192
|
-
const absPath = path.resolve(filePath);
|
|
193
|
-
relinka(
|
|
194
|
-
"log",
|
|
195
|
-
`Injecting into ${absPath} at lines: ${lineNums.join(", ")}`,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
const original = await fs.readFile(absPath, "utf-8");
|
|
200
|
-
const splitted = original.split(/\r?\n/);
|
|
201
|
-
for (const ln of lineNums) {
|
|
202
|
-
if (ln <= splitted.length) {
|
|
203
|
-
splitted.splice(ln - 1, 0, commentText);
|
|
204
|
-
} else {
|
|
205
|
-
relinka("warn", `Line ${ln} exceeds file length for ${absPath}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const newContent = splitted.join("\n");
|
|
209
|
-
await fs.writeFile(absPath, newContent, "utf-8");
|
|
210
|
-
} catch (error) {
|
|
211
|
-
relinka("error", `Failed editing ${filePath}: ${error}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
//-------------------------------------
|
|
217
|
-
// 6) The main usage function
|
|
218
|
-
//-------------------------------------
|
|
219
|
-
export async function useTsExpectError(args: {
|
|
220
|
-
files: string[];
|
|
221
|
-
comment?: string;
|
|
222
|
-
tscPaths?: string[];
|
|
223
|
-
}) {
|
|
224
|
-
// 1) load c12 config
|
|
225
|
-
const userConfig = await loadReinjectConfig();
|
|
226
|
-
const finalComment = args.comment || userConfig.injectComment!;
|
|
227
|
-
|
|
228
|
-
// Gather references
|
|
229
|
-
const lines: { filePath: string; lineNumber: number }[] = [];
|
|
230
|
-
let usedAuto = false;
|
|
231
|
-
for (const item of args.files) {
|
|
232
|
-
if (item.toLowerCase() === "auto") {
|
|
233
|
-
usedAuto = true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (usedAuto) {
|
|
238
|
-
relinka("log", "Running TSC to discover error lines...");
|
|
239
|
-
const tscCommand = userConfig.tscCommand!;
|
|
240
|
-
try {
|
|
241
|
-
const discovered = await runTscAndParseErrors(tscCommand, args.tscPaths);
|
|
242
|
-
// If tscPaths are provided, filter discovered errors to include only files within those paths.
|
|
243
|
-
if (args.tscPaths && args.tscPaths.length > 0) {
|
|
244
|
-
const filtered = discovered.filter((rec) =>
|
|
245
|
-
isWithin(rec.filePath, args.tscPaths!),
|
|
246
|
-
);
|
|
247
|
-
lines.push(...filtered);
|
|
248
|
-
} else {
|
|
249
|
-
lines.push(...discovered);
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
relinka("error", `Failed running tsc: ${error}`);
|
|
253
|
-
process.exit(1);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Parse lines from each file that isn't "auto"
|
|
258
|
-
for (const item of args.files) {
|
|
259
|
-
if (item.toLowerCase() === "auto") continue;
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
const recs = await parseLinesFile(item);
|
|
263
|
-
lines.push(...recs);
|
|
264
|
-
} catch (error) {
|
|
265
|
-
relinka("error", `Failed reading lines file ${item}: ${error}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (lines.length === 0) {
|
|
270
|
-
relinka("error", "No references found. Nothing to do.");
|
|
271
|
-
relinka("error", "Lines: ", JSON.stringify(lines));
|
|
272
|
-
process.exit(1);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
await injectCommentIntoFiles(lines, finalComment);
|
|
276
|
-
relinka("success", "All lines processed successfully.");
|
|
277
|
-
}
|