@supacloud/compiler 0.29.0 → 0.30.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/dist/cli.js +847 -10699
- package/dist/delivery-bundle-worker.js +654 -10607
- package/dist/diagnostics.d.ts +22 -0
- package/dist/diagnostics.js +262 -2
- package/dist/fixes.d.ts +3 -0
- package/dist/index.js +1114 -11040
- package/dist/inspect.d.ts +6 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/dist/diagnostics.d.ts
CHANGED
|
@@ -37,3 +37,25 @@ export declare function toEditorDiagnostics(diagnostics: readonly Diagnostic[]):
|
|
|
37
37
|
source: string;
|
|
38
38
|
code: string;
|
|
39
39
|
}[];
|
|
40
|
+
/** Semantic editor commands are emitted only after a successful read-only preview.
|
|
41
|
+
* Consumers execute the command through applyDiagnosticFix with expectedSourceHash;
|
|
42
|
+
* source drift or changed semantic preconditions reject the action.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createEditorCodeActions(diagnostics: readonly Diagnostic[], options?: {
|
|
45
|
+
rootDir?: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
title: string;
|
|
48
|
+
kind: "quickfix";
|
|
49
|
+
command: {
|
|
50
|
+
title: string;
|
|
51
|
+
command: "supacloud.applyDiagnosticFix";
|
|
52
|
+
arguments: [{
|
|
53
|
+
fix: NonNullable<Diagnostic["fix"]>;
|
|
54
|
+
expectedSourceHash: string;
|
|
55
|
+
}];
|
|
56
|
+
};
|
|
57
|
+
data: {
|
|
58
|
+
file: string;
|
|
59
|
+
preview: string;
|
|
60
|
+
};
|
|
61
|
+
}[]>;
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __returnValue = (v) => v;
|
|
3
|
+
function __exportSetter(name, newValue) {
|
|
4
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
5
|
+
}
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
set: __exportSetter.bind(all, name)
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
var __esm = (fn, res, err) => () => {
|
|
16
|
+
if (fn)
|
|
17
|
+
try {
|
|
18
|
+
res = fn(fn = 0);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
err = [e];
|
|
21
|
+
}
|
|
22
|
+
if (err)
|
|
23
|
+
throw err[0];
|
|
24
|
+
return res;
|
|
25
|
+
};
|
|
26
|
+
|
|
1
27
|
// src/repair-plan.ts
|
|
2
28
|
function createDiagnosticRepairPlan(diagnostics) {
|
|
3
29
|
return diagnostics.flatMap((diagnostic) => {
|
|
@@ -41,6 +67,213 @@ function classifyFix(fix) {
|
|
|
41
67
|
return { readiness: "preview", reason: "Preview with the semantic fix executor; source preconditions are checked before writing." };
|
|
42
68
|
}
|
|
43
69
|
|
|
70
|
+
// src/fixes.ts
|
|
71
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
72
|
+
import { lstat, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
73
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
74
|
+
import * as ts from "@typescript/typescript6";
|
|
75
|
+
async function applyDiagnosticFix(fix, options = {}) {
|
|
76
|
+
if (!fix || typeof fix.targetFile !== "string")
|
|
77
|
+
throw new Error("Invalid DiagnosticFix");
|
|
78
|
+
const root = await realpath(options.rootDir ?? process.cwd());
|
|
79
|
+
const file = resolve(root, fix.targetFile);
|
|
80
|
+
const stat = await lstat(file);
|
|
81
|
+
const resolved = await realpath(file);
|
|
82
|
+
const relativePath = relative(root, resolved);
|
|
83
|
+
if (stat.isSymbolicLink() || !stat.isFile() || isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${sep}`)) {
|
|
84
|
+
throw new Error("Fix target must be a regular file inside rootDir");
|
|
85
|
+
}
|
|
86
|
+
const original = await readFile(file, "utf8");
|
|
87
|
+
const sourceHash = createHash("sha256").update(original).digest("hex");
|
|
88
|
+
if (options.expectedSourceHash !== undefined && options.expectedSourceHash !== sourceHash) {
|
|
89
|
+
throw new Error("Fix source changed since preview; analyze the project again");
|
|
90
|
+
}
|
|
91
|
+
let source = parse(file, original);
|
|
92
|
+
let content;
|
|
93
|
+
switch (fix.type) {
|
|
94
|
+
case "set_command_mode": {
|
|
95
|
+
if (fix.property !== "transaction" && fix.property !== "idempotency" || fix.value !== "required" && fix.value !== "none") {
|
|
96
|
+
throw new Error("Command mode fix requires an explicit property and policy value");
|
|
97
|
+
}
|
|
98
|
+
const object = decoratorObject(findClass(source, fix.command), "Command");
|
|
99
|
+
const current = property(object, fix.property);
|
|
100
|
+
if (!current || current.initializer.getText(source) !== fix.expectedExpression) {
|
|
101
|
+
throw new Error("Command mode changed since diagnosis; analyze the project again");
|
|
102
|
+
}
|
|
103
|
+
content = replaceProperty(source, object, fix.property, ts.factory.createStringLiteral(fix.value));
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "add_module_import": {
|
|
107
|
+
if (!fix.importPath || !fix.symbol)
|
|
108
|
+
throw new Error("Module fix requires importPath and symbol");
|
|
109
|
+
identifier(fix.symbol);
|
|
110
|
+
const withImport = importSymbol(source, fix.importPath, fix.symbol);
|
|
111
|
+
source = parse(file, withImport);
|
|
112
|
+
const object = unique(moduleObjects(source).filter((candidate) => !fix.targetModule || stringProperty(candidate, "name") === fix.targetModule), "target module");
|
|
113
|
+
const imports = property(object, "imports");
|
|
114
|
+
if (imports && !ts.isArrayLiteralExpression(imports.initializer)) {
|
|
115
|
+
throw new Error("Module imports must be a static array");
|
|
116
|
+
}
|
|
117
|
+
const values = imports && ts.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
|
|
118
|
+
if (values.some(ts.isSpreadElement))
|
|
119
|
+
throw new Error("Module imports cannot contain spread elements");
|
|
120
|
+
content = values.some((value) => ts.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts.factory.createArrayLiteralExpression([
|
|
121
|
+
...values,
|
|
122
|
+
ts.factory.createIdentifier(fix.symbol)
|
|
123
|
+
]));
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "add_command_permission": {
|
|
127
|
+
const permission = options.permission ?? fix.permission;
|
|
128
|
+
if (!permission?.trim()) {
|
|
129
|
+
throw new Error("Permission fix requires an explicit permission; privileges are never inferred");
|
|
130
|
+
}
|
|
131
|
+
const command = findClass(source, fix.command);
|
|
132
|
+
const object = decoratorObject(command, "Command");
|
|
133
|
+
const current = property(object, "permission");
|
|
134
|
+
if (current && (!ts.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
|
|
135
|
+
throw new Error("Command permission already exists with a different value");
|
|
136
|
+
}
|
|
137
|
+
content = current ? original : replaceProperty(source, object, "permission", ts.factory.createStringLiteral(permission));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "add_route_parameter_binding": {
|
|
141
|
+
const controller = findClass(source, fix.controller);
|
|
142
|
+
const method = unique(controller.members.filter((member) => ts.isMethodDeclaration(member) && nameOf(member.name) === fix.route), "route handler");
|
|
143
|
+
const parameter = unique(method.parameters.filter((candidate) => ts.isIdentifier(candidate.name) && candidate.name.text === fix.parameter), "same-named route parameter");
|
|
144
|
+
const binding = fix.binding === "param" ? "Param" : fix.binding === "query" ? "Query" : undefined;
|
|
145
|
+
if (!binding)
|
|
146
|
+
throw new Error("Invalid route binding");
|
|
147
|
+
const decorators = ts.getDecorators(parameter) ?? [];
|
|
148
|
+
if (decorators.length > 0)
|
|
149
|
+
throw new Error("Parameter already has a decorator");
|
|
150
|
+
const framework = unique(source.statements.filter((statement) => ts.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some((element) => ["Controller", "Get", "Post", "Put", "Patch", "Delete", "Head", "Options"].includes(element.name.text))), "framework import");
|
|
151
|
+
if (!ts.isImportDeclaration(framework) || !ts.isStringLiteral(framework.moduleSpecifier)) {
|
|
152
|
+
throw new Error("Framework import must be static");
|
|
153
|
+
}
|
|
154
|
+
const edited = original.slice(0, parameter.getStart(source)) + `@${binding}(${JSON.stringify(fix.parameter)}) ` + original.slice(parameter.getStart(source));
|
|
155
|
+
content = importSymbol(parse(file, edited), framework.moduleSpecifier.text, binding);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
throw new Error(`Diagnostic fix '${fix.type}' requires a manual semantic decision; no files changed`);
|
|
160
|
+
}
|
|
161
|
+
parse(file, content);
|
|
162
|
+
const result = { file, changed: content !== original, content, sourceHash };
|
|
163
|
+
if (options.dryRun === false && result.changed) {
|
|
164
|
+
const temporary = `${file}.supacloud-fix-${randomUUID()}`;
|
|
165
|
+
try {
|
|
166
|
+
await writeFile(temporary, content, { encoding: "utf8", flag: "wx", mode: stat.mode });
|
|
167
|
+
if (await readFile(file, "utf8") !== original)
|
|
168
|
+
throw new Error("Target changed while preparing the fix");
|
|
169
|
+
await rename(temporary, file);
|
|
170
|
+
} finally {
|
|
171
|
+
await unlink(temporary).catch(() => {});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
function parse(file, text) {
|
|
177
|
+
const result = ts.transpileModule(text, {
|
|
178
|
+
fileName: file,
|
|
179
|
+
reportDiagnostics: true,
|
|
180
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext, experimentalDecorators: true }
|
|
181
|
+
});
|
|
182
|
+
if (result.diagnostics?.some((item) => item.category === ts.DiagnosticCategory.Error)) {
|
|
183
|
+
throw new Error("Cannot fix syntactically invalid TypeScript");
|
|
184
|
+
}
|
|
185
|
+
return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
186
|
+
}
|
|
187
|
+
function unique(items, description) {
|
|
188
|
+
if (items.length !== 1)
|
|
189
|
+
throw new Error(`Expected exactly one ${description}; found ${items.length}`);
|
|
190
|
+
return items[0];
|
|
191
|
+
}
|
|
192
|
+
function identifier(value) {
|
|
193
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(value))
|
|
194
|
+
throw new Error(`Invalid identifier '${value}'`);
|
|
195
|
+
}
|
|
196
|
+
function nameOf(name) {
|
|
197
|
+
if (!name)
|
|
198
|
+
return "";
|
|
199
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) ? name.text : "";
|
|
200
|
+
}
|
|
201
|
+
function property(object, key) {
|
|
202
|
+
if (object.properties.some((item) => !ts.isPropertyAssignment(item) || ts.isComputedPropertyName(item.name))) {
|
|
203
|
+
throw new Error("Fix requires explicit static object properties");
|
|
204
|
+
}
|
|
205
|
+
const values = object.properties.filter((item) => ts.isPropertyAssignment(item) && nameOf(item.name) === key);
|
|
206
|
+
if (values.length > 1)
|
|
207
|
+
throw new Error(`Duplicate '${key}' property`);
|
|
208
|
+
return values[0];
|
|
209
|
+
}
|
|
210
|
+
function stringProperty(object, key) {
|
|
211
|
+
const value = property(object, key)?.initializer;
|
|
212
|
+
return value && ts.isStringLiteral(value) ? value.text : undefined;
|
|
213
|
+
}
|
|
214
|
+
function replaceProperty(source, object, key, value) {
|
|
215
|
+
const previous = property(object, key);
|
|
216
|
+
const replacement = ts.factory.createPropertyAssignment(key, value);
|
|
217
|
+
const properties = object.properties.map((item) => item === previous ? replacement : item);
|
|
218
|
+
if (!previous)
|
|
219
|
+
properties.push(replacement);
|
|
220
|
+
const updated = ts.factory.updateObjectLiteralExpression(object, properties);
|
|
221
|
+
return source.text.slice(0, object.getStart(source)) + ts.createPrinter().printNode(ts.EmitHint.Expression, updated, source) + source.text.slice(object.end);
|
|
222
|
+
}
|
|
223
|
+
function findClass(source, name) {
|
|
224
|
+
return unique(source.statements.filter((statement) => ts.isClassDeclaration(statement) && statement.name?.text === name), `class '${name}'`);
|
|
225
|
+
}
|
|
226
|
+
function decoratorObject(node, name) {
|
|
227
|
+
const decorator = unique((ts.getDecorators(node) ?? []).filter((item) => ts.isCallExpression(item.expression) && item.expression.expression.getText() === name), `@${name} decorator`);
|
|
228
|
+
const argument = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
|
|
229
|
+
if (!argument || !ts.isObjectLiteralExpression(argument))
|
|
230
|
+
throw new Error(`@${name} requires a static object`);
|
|
231
|
+
return argument;
|
|
232
|
+
}
|
|
233
|
+
function moduleObjects(source) {
|
|
234
|
+
const result = [];
|
|
235
|
+
for (const statement of source.statements) {
|
|
236
|
+
if (ts.isClassDeclaration(statement) && (ts.getDecorators(statement) ?? []).some((item) => ts.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
|
|
237
|
+
result.push(decoratorObject(statement, "Module"));
|
|
238
|
+
}
|
|
239
|
+
if (ts.isVariableStatement(statement)) {
|
|
240
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
241
|
+
const call = declaration.initializer;
|
|
242
|
+
if (call && ts.isCallExpression(call) && ["defineModule", "defineFeatureSlice"].includes(call.expression.getText()) && call.arguments[0] && ts.isObjectLiteralExpression(call.arguments[0])) {
|
|
243
|
+
result.push(call.arguments[0]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
function importSymbol(source, path, symbol) {
|
|
251
|
+
identifier(symbol);
|
|
252
|
+
const current = resolve(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
|
|
253
|
+
const target = resolve(dirname(source.fileName), path).replace(/\.(tsx?|mts|cts)$/, "");
|
|
254
|
+
if (current === target)
|
|
255
|
+
return source.text;
|
|
256
|
+
const matches = source.statements.filter((item) => ts.isImportDeclaration(item) && ts.isStringLiteral(item.moduleSpecifier) && item.moduleSpecifier.text === path);
|
|
257
|
+
if (matches.length > 1)
|
|
258
|
+
throw new Error(`Ambiguous imports from '${path}'`);
|
|
259
|
+
const match = matches[0];
|
|
260
|
+
if (match && ts.isImportDeclaration(match) && match.importClause?.namedBindings && ts.isNamedImports(match.importClause.namedBindings) && !match.importClause.isTypeOnly) {
|
|
261
|
+
if (match.importClause.namedBindings.elements.some((item) => item.name.text === symbol))
|
|
262
|
+
return source.text;
|
|
263
|
+
const bindings = match.importClause.namedBindings;
|
|
264
|
+
const updated = ts.factory.updateNamedImports(bindings, [
|
|
265
|
+
...bindings.elements,
|
|
266
|
+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(symbol))
|
|
267
|
+
]);
|
|
268
|
+
return source.text.slice(0, bindings.getStart(source)) + ts.createPrinter().printNode(ts.EmitHint.Unspecified, updated, source) + source.text.slice(bindings.end);
|
|
269
|
+
}
|
|
270
|
+
if (match)
|
|
271
|
+
throw new Error(`Import from '${path}' is not a named value import`);
|
|
272
|
+
return `import { ${symbol} } from ${JSON.stringify(path)};
|
|
273
|
+
${source.text}`;
|
|
274
|
+
}
|
|
275
|
+
var init_fixes = () => {};
|
|
276
|
+
|
|
44
277
|
// src/diagnostics.ts
|
|
45
278
|
function createDiagnosticReport(diagnostics) {
|
|
46
279
|
return { format: 1, entries: diagnostics.map((diagnostic) => {
|
|
@@ -55,8 +288,14 @@ function createDiagnosticReport(diagnostics) {
|
|
|
55
288
|
function toEditorDiagnostics(diagnostics) {
|
|
56
289
|
return createDiagnosticReport(diagnostics).entries.map((entry) => ({
|
|
57
290
|
range: {
|
|
58
|
-
start: {
|
|
59
|
-
|
|
291
|
+
start: {
|
|
292
|
+
line: Math.max(0, (entry.diagnostic.line ?? 1) - 1),
|
|
293
|
+
character: Math.max(0, entry.diagnostic.column ?? 0)
|
|
294
|
+
},
|
|
295
|
+
end: {
|
|
296
|
+
line: Math.max(0, (entry.diagnostic.endLine ?? entry.diagnostic.line ?? 1) - 1),
|
|
297
|
+
character: Math.max(0, entry.diagnostic.endColumn ?? entry.diagnostic.column ?? 0)
|
|
298
|
+
}
|
|
60
299
|
},
|
|
61
300
|
severity: entry.diagnostic.severity === "error" ? 1 : 2,
|
|
62
301
|
source: "supacloud",
|
|
@@ -67,7 +306,28 @@ ${entry.guidance}` : ""),
|
|
|
67
306
|
data: { format: 1, ...entry }
|
|
68
307
|
}));
|
|
69
308
|
}
|
|
309
|
+
async function createEditorCodeActions(diagnostics, options = {}) {
|
|
310
|
+
await Promise.resolve().then(() => init_fixes());
|
|
311
|
+
const actions = [];
|
|
312
|
+
for (const entry of createDiagnosticReport(diagnostics).entries) {
|
|
313
|
+
if (entry.repair?.readiness !== "preview")
|
|
314
|
+
continue;
|
|
315
|
+
try {
|
|
316
|
+
const preview = await applyDiagnosticFix(entry.repair.fix, { ...options, dryRun: true });
|
|
317
|
+
if (!preview.changed)
|
|
318
|
+
continue;
|
|
319
|
+
const title = `Apply ${entry.repair.type}`;
|
|
320
|
+
actions.push({ title, kind: "quickfix", command: {
|
|
321
|
+
title,
|
|
322
|
+
command: "supacloud.applyDiagnosticFix",
|
|
323
|
+
arguments: [{ fix: entry.repair.fix, expectedSourceHash: preview.sourceHash }]
|
|
324
|
+
}, data: { file: preview.file, preview: preview.content } });
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
327
|
+
return actions;
|
|
328
|
+
}
|
|
70
329
|
export {
|
|
71
330
|
createDiagnosticReport,
|
|
331
|
+
createEditorCodeActions,
|
|
72
332
|
toEditorDiagnostics
|
|
73
333
|
};
|
package/dist/fixes.d.ts
CHANGED
|
@@ -4,10 +4,13 @@ export interface ApplyDiagnosticFixOptions {
|
|
|
4
4
|
/** Preview by default; set false to write. */
|
|
5
5
|
dryRun?: boolean;
|
|
6
6
|
permission?: string;
|
|
7
|
+
/** Reject an editor action when its previewed source has changed. */
|
|
8
|
+
expectedSourceHash?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface AppliedDiagnosticFix {
|
|
9
11
|
file: string;
|
|
10
12
|
changed: boolean;
|
|
11
13
|
content: string;
|
|
14
|
+
sourceHash: string;
|
|
12
15
|
}
|
|
13
16
|
export declare function applyDiagnosticFix(fix: DiagnosticFix, options?: ApplyDiagnosticFixOptions): Promise<AppliedDiagnosticFix>;
|