@zucker-framework/cli 1.0.1 → 1.0.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/dist/chunk-O6YSETKJ.mjs +6 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.js +2 -2
- package/dist/index.mjs +828 -0
- package/dist/release-catalog.json +59 -59
- package/dist/tooling.d.mts +53 -0
- package/dist/tooling.d.ts +53 -0
- package/dist/tooling.js +554 -0
- package/dist/tooling.mjs +516 -0
- package/package.json +28 -7
- package/tooling/agent-instructions.d.mts +25 -0
- package/tooling/agent-instructions.mjs +145 -0
package/dist/tooling.js
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var src_exports = {};
|
|
33
|
+
__export(src_exports, {
|
|
34
|
+
auditFrameworkSourceText: () => auditFrameworkSourceText,
|
|
35
|
+
auditModuleWiring: () => auditModuleWiring,
|
|
36
|
+
auditPnpmWorkflowVersions: () => auditPnpmWorkflowVersions,
|
|
37
|
+
checkTransactionBoundaries: () => checkTransactionBoundaries,
|
|
38
|
+
findTransactionBoundaryViolations: () => findTransactionBoundaryViolations
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(src_exports);
|
|
41
|
+
|
|
42
|
+
// src/tooling/module-wiring.ts
|
|
43
|
+
var import_node_fs = require("fs");
|
|
44
|
+
var import_node_path = require("path");
|
|
45
|
+
var import_typescript = __toESM(require("typescript"));
|
|
46
|
+
var LIFECYCLE_INTERFACES = /* @__PURE__ */ new Set([
|
|
47
|
+
"BeforeApplicationShutdown",
|
|
48
|
+
"OnApplicationBootstrap",
|
|
49
|
+
"OnApplicationShutdown",
|
|
50
|
+
"OnModuleDestroy",
|
|
51
|
+
"OnModuleInit"
|
|
52
|
+
]);
|
|
53
|
+
var LIFECYCLE_METHODS = /* @__PURE__ */ new Set([
|
|
54
|
+
"beforeApplicationShutdown",
|
|
55
|
+
"onApplicationBootstrap",
|
|
56
|
+
"onApplicationShutdown",
|
|
57
|
+
"onModuleDestroy",
|
|
58
|
+
"onModuleInit"
|
|
59
|
+
]);
|
|
60
|
+
function listTypeScriptFiles(directory) {
|
|
61
|
+
const files = [];
|
|
62
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, {
|
|
63
|
+
withFileTypes: true
|
|
64
|
+
})) {
|
|
65
|
+
const path = (0, import_node_path.resolve)(directory, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
files.push(...listTypeScriptFiles(path));
|
|
68
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".spec.ts") && !entry.name.endsWith(".test.ts")) {
|
|
69
|
+
files.push(path);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
__name(listTypeScriptFiles, "listTypeScriptFiles");
|
|
75
|
+
function getImportAliases(sourceFile) {
|
|
76
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const statement of sourceFile.statements) {
|
|
78
|
+
if (!import_typescript.default.isImportDeclaration(statement)) continue;
|
|
79
|
+
const bindings = statement.importClause?.namedBindings;
|
|
80
|
+
if (!bindings || !import_typescript.default.isNamedImports(bindings)) continue;
|
|
81
|
+
for (const element of bindings.elements) {
|
|
82
|
+
aliases.set(element.name.text, element.propertyName?.text ?? element.name.text);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return aliases;
|
|
86
|
+
}
|
|
87
|
+
__name(getImportAliases, "getImportAliases");
|
|
88
|
+
function getScheduleModuleImports(sourceFile) {
|
|
89
|
+
const cron = /* @__PURE__ */ new Set();
|
|
90
|
+
const modules = /* @__PURE__ */ new Set();
|
|
91
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const statement of sourceFile.statements) {
|
|
93
|
+
if (!import_typescript.default.isImportDeclaration(statement) || !import_typescript.default.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "@nestjs/schedule") {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const bindings = statement.importClause?.namedBindings;
|
|
97
|
+
if (!bindings) continue;
|
|
98
|
+
if (import_typescript.default.isNamespaceImport(bindings)) {
|
|
99
|
+
namespaces.add(bindings.name.text);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const element of bindings.elements) {
|
|
103
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
104
|
+
if (importedName === "ScheduleModule") {
|
|
105
|
+
modules.add(element.name.text);
|
|
106
|
+
} else if (importedName === "Cron") {
|
|
107
|
+
cron.add(element.name.text);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
cron,
|
|
113
|
+
modules,
|
|
114
|
+
namespaces
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
__name(getScheduleModuleImports, "getScheduleModuleImports");
|
|
118
|
+
function isImportedScheduleModule(expression, imports) {
|
|
119
|
+
if (import_typescript.default.isIdentifier(expression)) {
|
|
120
|
+
return imports.modules.has(expression.text);
|
|
121
|
+
}
|
|
122
|
+
return import_typescript.default.isPropertyAccessExpression(expression) && import_typescript.default.isIdentifier(expression.expression) && imports.namespaces.has(expression.expression.text) && expression.name.text === "ScheduleModule";
|
|
123
|
+
}
|
|
124
|
+
__name(isImportedScheduleModule, "isImportedScheduleModule");
|
|
125
|
+
function isImportedCron(expression, imports) {
|
|
126
|
+
if (import_typescript.default.isIdentifier(expression)) {
|
|
127
|
+
return imports.cron.has(expression.text);
|
|
128
|
+
}
|
|
129
|
+
return import_typescript.default.isPropertyAccessExpression(expression) && import_typescript.default.isIdentifier(expression.expression) && imports.namespaces.has(expression.expression.text) && expression.name.text === "Cron";
|
|
130
|
+
}
|
|
131
|
+
__name(isImportedCron, "isImportedCron");
|
|
132
|
+
function waitsForCompletion(call) {
|
|
133
|
+
const options = call.arguments[1];
|
|
134
|
+
if (!options || !import_typescript.default.isObjectLiteralExpression(options)) return false;
|
|
135
|
+
const property = findProperty(options, "waitForCompletion");
|
|
136
|
+
return property?.initializer.kind === import_typescript.default.SyntaxKind.TrueKeyword;
|
|
137
|
+
}
|
|
138
|
+
__name(waitsForCompletion, "waitsForCompletion");
|
|
139
|
+
function enclosingClassName(node) {
|
|
140
|
+
let current = node.parent;
|
|
141
|
+
while (current) {
|
|
142
|
+
if (import_typescript.default.isClassDeclaration(current)) return current.name?.text;
|
|
143
|
+
current = current.parent;
|
|
144
|
+
}
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
__name(enclosingClassName, "enclosingClassName");
|
|
148
|
+
function getCronClassNames(sourceFile, scheduleImports) {
|
|
149
|
+
const names = [];
|
|
150
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
151
|
+
if (import_typescript.default.isCallExpression(node) && import_typescript.default.isDecorator(node.parent) && isImportedCron(node.expression, scheduleImports)) {
|
|
152
|
+
const className = enclosingClassName(node);
|
|
153
|
+
if (className) names.push(className);
|
|
154
|
+
}
|
|
155
|
+
import_typescript.default.forEachChild(node, visit);
|
|
156
|
+
}, "visit");
|
|
157
|
+
visit(sourceFile);
|
|
158
|
+
return names;
|
|
159
|
+
}
|
|
160
|
+
__name(getCronClassNames, "getCronClassNames");
|
|
161
|
+
function expressionName(expression, aliases) {
|
|
162
|
+
if (!expression) return void 0;
|
|
163
|
+
if (import_typescript.default.isIdentifier(expression)) {
|
|
164
|
+
return aliases.get(expression.text) ?? expression.text;
|
|
165
|
+
}
|
|
166
|
+
if (import_typescript.default.isParenthesizedExpression(expression)) {
|
|
167
|
+
return expressionName(expression.expression, aliases);
|
|
168
|
+
}
|
|
169
|
+
if (import_typescript.default.isAsExpression(expression) || import_typescript.default.isTypeAssertionExpression(expression)) {
|
|
170
|
+
return expressionName(expression.expression, aliases);
|
|
171
|
+
}
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
__name(expressionName, "expressionName");
|
|
175
|
+
function propertyName(property) {
|
|
176
|
+
const name = property.name;
|
|
177
|
+
if (!name) return void 0;
|
|
178
|
+
if (import_typescript.default.isIdentifier(name) || import_typescript.default.isStringLiteral(name)) return name.text;
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
__name(propertyName, "propertyName");
|
|
182
|
+
function findProperty(object, name) {
|
|
183
|
+
return object.properties.find((property) => import_typescript.default.isPropertyAssignment(property) && propertyName(property) === name);
|
|
184
|
+
}
|
|
185
|
+
__name(findProperty, "findProperty");
|
|
186
|
+
function getModuleMetadata(sourceFile, aliases) {
|
|
187
|
+
const metadata = [];
|
|
188
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
189
|
+
if (import_typescript.default.isCallExpression(node) && import_typescript.default.isIdentifier(node.expression) && expressionName(node.expression, aliases) === "Module" && node.arguments.length === 1 && import_typescript.default.isObjectLiteralExpression(node.arguments[0])) {
|
|
190
|
+
metadata.push(node.arguments[0]);
|
|
191
|
+
}
|
|
192
|
+
import_typescript.default.forEachChild(node, visit);
|
|
193
|
+
}, "visit");
|
|
194
|
+
visit(sourceFile);
|
|
195
|
+
return metadata;
|
|
196
|
+
}
|
|
197
|
+
__name(getModuleMetadata, "getModuleMetadata");
|
|
198
|
+
function getProviders(metadata) {
|
|
199
|
+
const providers = findProperty(metadata, "providers");
|
|
200
|
+
if (!providers || !import_typescript.default.isArrayLiteralExpression(providers.initializer)) return [];
|
|
201
|
+
return providers.initializer.elements.filter((element) => !import_typescript.default.isSpreadElement(element));
|
|
202
|
+
}
|
|
203
|
+
__name(getProviders, "getProviders");
|
|
204
|
+
function getImplementedLifecycleNames(sourceFile, aliases) {
|
|
205
|
+
const names = [];
|
|
206
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
207
|
+
if (import_typescript.default.isClassDeclaration(node) && node.name) {
|
|
208
|
+
const implementsLifecycle = (node.heritageClauses ?? []).some((clause) => clause.token === import_typescript.default.SyntaxKind.ImplementsKeyword && clause.types.some((type) => {
|
|
209
|
+
const localName = type.expression.getText(sourceFile).split(".").at(-1);
|
|
210
|
+
const name = localName ? aliases.get(localName) ?? localName : void 0;
|
|
211
|
+
return name && LIFECYCLE_INTERFACES.has(name);
|
|
212
|
+
}));
|
|
213
|
+
const declaresLifecycleHook = node.members.some((member) => {
|
|
214
|
+
const name = propertyName(member);
|
|
215
|
+
return name ? LIFECYCLE_METHODS.has(name) : false;
|
|
216
|
+
});
|
|
217
|
+
if (implementsLifecycle || declaresLifecycleHook) names.push(node.name.text);
|
|
218
|
+
}
|
|
219
|
+
import_typescript.default.forEachChild(node, visit);
|
|
220
|
+
}, "visit");
|
|
221
|
+
visit(sourceFile);
|
|
222
|
+
return names;
|
|
223
|
+
}
|
|
224
|
+
__name(getImplementedLifecycleNames, "getImplementedLifecycleNames");
|
|
225
|
+
function factoryReturnsInjectedInstance(factory, injectedParameterNames) {
|
|
226
|
+
const returnsInjected = /* @__PURE__ */ __name((expression) => {
|
|
227
|
+
if (import_typescript.default.isIdentifier(expression)) {
|
|
228
|
+
return injectedParameterNames.has(expression.text);
|
|
229
|
+
}
|
|
230
|
+
if (import_typescript.default.isParenthesizedExpression(expression)) {
|
|
231
|
+
return returnsInjected(expression.expression);
|
|
232
|
+
}
|
|
233
|
+
if (import_typescript.default.isAsExpression(expression) || import_typescript.default.isTypeAssertionExpression(expression)) {
|
|
234
|
+
return returnsInjected(expression.expression);
|
|
235
|
+
}
|
|
236
|
+
if (import_typescript.default.isConditionalExpression(expression)) {
|
|
237
|
+
return returnsInjected(expression.whenTrue) || returnsInjected(expression.whenFalse);
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}, "returnsInjected");
|
|
241
|
+
if (!import_typescript.default.isArrowFunction(factory) && !import_typescript.default.isFunctionExpression(factory)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (!import_typescript.default.isBlock(factory.body)) return returnsInjected(factory.body);
|
|
245
|
+
let unsafe = false;
|
|
246
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
247
|
+
if (import_typescript.default.isReturnStatement(node) && node.expression) {
|
|
248
|
+
unsafe ||= returnsInjected(node.expression);
|
|
249
|
+
}
|
|
250
|
+
import_typescript.default.forEachChild(node, visit);
|
|
251
|
+
}, "visit");
|
|
252
|
+
visit(factory.body);
|
|
253
|
+
return unsafe;
|
|
254
|
+
}
|
|
255
|
+
__name(factoryReturnsInjectedInstance, "factoryReturnsInjectedInstance");
|
|
256
|
+
function location(sourceRoot, file, node, sourceFile) {
|
|
257
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
258
|
+
return `${(0, import_node_path.relative)(sourceRoot, file)}:${start.line + 1}`;
|
|
259
|
+
}
|
|
260
|
+
__name(location, "location");
|
|
261
|
+
function finding(code, message, at) {
|
|
262
|
+
return {
|
|
263
|
+
code,
|
|
264
|
+
message,
|
|
265
|
+
location: at
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
__name(finding, "finding");
|
|
269
|
+
function auditModuleWiring({ sourceRoot, schedulerRoot, globalGuards, infrastructureProviders = {} }) {
|
|
270
|
+
const expectedGlobalGuards = /* @__PURE__ */ new Set([
|
|
271
|
+
...(globalGuards?.classes ?? []).map((name) => `useClass:${name}`),
|
|
272
|
+
...(globalGuards?.factories ?? []).map((name) => `useFactory:${name}`)
|
|
273
|
+
]);
|
|
274
|
+
if (!(0, import_node_fs.statSync)(sourceRoot).isDirectory()) {
|
|
275
|
+
throw new Error(`Module source root is not a directory: ${sourceRoot}`);
|
|
276
|
+
}
|
|
277
|
+
const files = listTypeScriptFiles(sourceRoot);
|
|
278
|
+
const parsed = files.map((file) => {
|
|
279
|
+
const source = (0, import_node_fs.readFileSync)(file, "utf8");
|
|
280
|
+
const sourceFile = import_typescript.default.createSourceFile(file, source, import_typescript.default.ScriptTarget.Latest, true, import_typescript.default.ScriptKind.TS);
|
|
281
|
+
return {
|
|
282
|
+
aliases: getImportAliases(sourceFile),
|
|
283
|
+
file,
|
|
284
|
+
scheduleImports: getScheduleModuleImports(sourceFile),
|
|
285
|
+
sourceFile
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
const lifecycleClasses = new Set(parsed.flatMap(({ aliases, sourceFile }) => getImplementedLifecycleNames(sourceFile, aliases)));
|
|
289
|
+
const cronClasses = new Set(parsed.flatMap(({ scheduleImports, sourceFile }) => getCronClassNames(sourceFile, scheduleImports)));
|
|
290
|
+
const findings = [];
|
|
291
|
+
const schedules = [];
|
|
292
|
+
const cronJobs = [];
|
|
293
|
+
const guards = [];
|
|
294
|
+
const coreProviders = Object.fromEntries(Object.keys(infrastructureProviders).map((name) => [
|
|
295
|
+
name,
|
|
296
|
+
[]
|
|
297
|
+
]));
|
|
298
|
+
const lifecycleProviders = /* @__PURE__ */ new Map();
|
|
299
|
+
const cronProviders = /* @__PURE__ */ new Map();
|
|
300
|
+
for (const { aliases, file, scheduleImports, sourceFile } of parsed) {
|
|
301
|
+
const relativeFile = (0, import_node_path.relative)(sourceRoot, file);
|
|
302
|
+
const visitSchedule = /* @__PURE__ */ __name((node) => {
|
|
303
|
+
if (import_typescript.default.isCallExpression(node) && import_typescript.default.isDecorator(node.parent) && isImportedCron(node.expression, scheduleImports)) {
|
|
304
|
+
const at = location(sourceRoot, file, node, sourceFile);
|
|
305
|
+
cronJobs.push(at);
|
|
306
|
+
if (!waitsForCompletion(node)) {
|
|
307
|
+
findings.push(finding("cron-overlap-guard", "@Cron jobs must set waitForCompletion: true to prevent overlapping runs in one process", at));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (import_typescript.default.isCallExpression(node) && import_typescript.default.isPropertyAccessExpression(node.expression) && node.expression.name.text === "forRoot" && isImportedScheduleModule(node.expression.expression, scheduleImports)) {
|
|
311
|
+
schedules.push(location(sourceRoot, file, node, sourceFile));
|
|
312
|
+
}
|
|
313
|
+
import_typescript.default.forEachChild(node, visitSchedule);
|
|
314
|
+
}, "visitSchedule");
|
|
315
|
+
visitSchedule(sourceFile);
|
|
316
|
+
for (const metadata of getModuleMetadata(sourceFile, aliases)) {
|
|
317
|
+
for (const provider of getProviders(metadata)) {
|
|
318
|
+
const directName = expressionName(provider, aliases);
|
|
319
|
+
if (directName && Object.hasOwn(coreProviders, directName)) {
|
|
320
|
+
coreProviders[directName].push(location(sourceRoot, file, provider, sourceFile));
|
|
321
|
+
}
|
|
322
|
+
if (directName && lifecycleClasses.has(directName)) {
|
|
323
|
+
const locations = lifecycleProviders.get(directName) ?? [];
|
|
324
|
+
locations.push(location(sourceRoot, file, provider, sourceFile));
|
|
325
|
+
lifecycleProviders.set(directName, locations);
|
|
326
|
+
}
|
|
327
|
+
if (directName && cronClasses.has(directName)) {
|
|
328
|
+
const locations = cronProviders.get(directName) ?? [];
|
|
329
|
+
locations.push(location(sourceRoot, file, provider, sourceFile));
|
|
330
|
+
cronProviders.set(directName, locations);
|
|
331
|
+
}
|
|
332
|
+
if (!import_typescript.default.isObjectLiteralExpression(provider)) continue;
|
|
333
|
+
const provide = findProperty(provider, "provide");
|
|
334
|
+
const useClass = findProperty(provider, "useClass");
|
|
335
|
+
const factory = findProperty(provider, "useFactory");
|
|
336
|
+
const useClassName = useClass ? expressionName(useClass.initializer, aliases) : void 0;
|
|
337
|
+
if (useClassName && Object.hasOwn(coreProviders, useClassName)) {
|
|
338
|
+
coreProviders[useClassName].push(location(sourceRoot, file, provider, sourceFile));
|
|
339
|
+
}
|
|
340
|
+
if (useClassName && lifecycleClasses.has(useClassName)) {
|
|
341
|
+
const locations = lifecycleProviders.get(useClassName) ?? [];
|
|
342
|
+
locations.push(location(sourceRoot, file, provider, sourceFile));
|
|
343
|
+
lifecycleProviders.set(useClassName, locations);
|
|
344
|
+
}
|
|
345
|
+
if (useClassName && cronClasses.has(useClassName)) {
|
|
346
|
+
const locations = cronProviders.get(useClassName) ?? [];
|
|
347
|
+
locations.push(location(sourceRoot, file, provider, sourceFile));
|
|
348
|
+
cronProviders.set(useClassName, locations);
|
|
349
|
+
}
|
|
350
|
+
if (provide && expressionName(provide.initializer, aliases) === "APP_GUARD") {
|
|
351
|
+
const factoryName = factory ? expressionName(factory.initializer, aliases) : void 0;
|
|
352
|
+
guards.push({
|
|
353
|
+
identity: useClassName && !factory ? `useClass:${useClassName}` : factoryName && !useClass ? `useFactory:${factoryName}` : void 0,
|
|
354
|
+
location: location(sourceRoot, file, provider, sourceFile),
|
|
355
|
+
relativeFile
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const useExisting = findProperty(provider, "useExisting");
|
|
359
|
+
const existingName = useExisting ? expressionName(useExisting.initializer, aliases) : void 0;
|
|
360
|
+
if (existingName && lifecycleClasses.has(existingName)) {
|
|
361
|
+
findings.push(finding("lifecycle-provider-alias", `${existingName} implements a Nest lifecycle hook and must not be exposed through useExisting`, location(sourceRoot, file, provider, sourceFile)));
|
|
362
|
+
}
|
|
363
|
+
const inject = findProperty(provider, "inject");
|
|
364
|
+
if (!factory || !inject || !import_typescript.default.isArrayLiteralExpression(inject.initializer) || !import_typescript.default.isArrowFunction(factory.initializer) && !import_typescript.default.isFunctionExpression(factory.initializer)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const factoryFunction = factory.initializer;
|
|
368
|
+
const injectedParameterNames = /* @__PURE__ */ new Set();
|
|
369
|
+
inject.initializer.elements.forEach((element, index) => {
|
|
370
|
+
const injectedName = expressionName(element, aliases);
|
|
371
|
+
const parameter = factoryFunction.parameters[index];
|
|
372
|
+
if (injectedName && lifecycleClasses.has(injectedName) && parameter && import_typescript.default.isIdentifier(parameter.name)) {
|
|
373
|
+
injectedParameterNames.add(parameter.name.text);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
if (injectedParameterNames.size > 0 && factoryReturnsInjectedInstance(factory.initializer, injectedParameterNames)) {
|
|
377
|
+
findings.push(finding("lifecycle-provider-factory-alias", "factory returns an injected lifecycle provider directly; Nest can invoke its hooks through both provider wrappers", location(sourceRoot, file, provider, sourceFile)));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (schedulerRoot && (schedules.length !== 1 || !schedules[0]?.startsWith(`${schedulerRoot}:`))) {
|
|
383
|
+
findings.push(finding("schedule-root-count", `ScheduleModule.forRoot() must appear exactly once in ${schedulerRoot}; found ${schedules.length}`, schedules.join(", ") || "not found"));
|
|
384
|
+
}
|
|
385
|
+
for (const [provider, expectedFile] of Object.entries(infrastructureProviders)) {
|
|
386
|
+
const locations = coreProviders[provider];
|
|
387
|
+
if (locations.length !== 1 || !locations[0].startsWith(`${expectedFile}:`)) {
|
|
388
|
+
findings.push(finding("infrastructure-provider-owner", `${provider} must be provided exactly once by ${expectedFile}; found ${locations.length}`, locations.join(", ") || "not found"));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const guardNames = guards.map(({ identity }) => identity).filter((name) => Boolean(name));
|
|
392
|
+
const duplicateGuards = guardNames.filter((name, index) => guardNames.indexOf(name) !== index);
|
|
393
|
+
const actualGuardSet = new Set(guardNames);
|
|
394
|
+
if (globalGuards && (guards.some(({ relativeFile }) => relativeFile !== globalGuards.owner) || guards.some(({ identity }) => !identity) || duplicateGuards.length > 0 || actualGuardSet.size !== expectedGlobalGuards.size || [
|
|
395
|
+
...expectedGlobalGuards
|
|
396
|
+
].some((name) => !actualGuardSet.has(name)))) {
|
|
397
|
+
findings.push(finding("global-guard-composition", `APP_GUARD must be registered once per expected guard in ${globalGuards.owner}; found ${guardNames.join(", ") || "none"}`, guards.map(({ location: at }) => at).join(", ") || "not found"));
|
|
398
|
+
}
|
|
399
|
+
for (const [className, locations] of lifecycleProviders) {
|
|
400
|
+
if (locations.length > 1) {
|
|
401
|
+
findings.push(finding("duplicate-lifecycle-provider", `${className} is provided by multiple modules and would have multiple lifecycle owners`, locations.join(", ")));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
for (const className of cronClasses) {
|
|
405
|
+
const locations = cronProviders.get(className) ?? [];
|
|
406
|
+
if (locations.length !== 1) {
|
|
407
|
+
findings.push(finding("cron-provider-owner", `${className} contains @Cron jobs and must be provided exactly once; found ${locations.length}`, locations.join(", ") || "not found"));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
ok: findings.length === 0,
|
|
412
|
+
findings,
|
|
413
|
+
stats: {
|
|
414
|
+
files: files.length,
|
|
415
|
+
cronJobs: cronJobs.length,
|
|
416
|
+
cronProviders: cronProviders.size,
|
|
417
|
+
globalGuards: guards.length,
|
|
418
|
+
lifecycleClasses: lifecycleClasses.size,
|
|
419
|
+
scheduleRoots: schedules.length
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
__name(auditModuleWiring, "auditModuleWiring");
|
|
424
|
+
|
|
425
|
+
// src/tooling/transaction-boundaries.ts
|
|
426
|
+
var import_node_fs2 = require("fs");
|
|
427
|
+
var import_node_path2 = require("path");
|
|
428
|
+
var import_typescript2 = __toESM(require("typescript"));
|
|
429
|
+
function findTransactionBoundaryViolations(source, fileName, policy) {
|
|
430
|
+
const sourceFile = import_typescript2.default.createSourceFile(fileName, source, import_typescript2.default.ScriptTarget.Latest, true);
|
|
431
|
+
const violations = [];
|
|
432
|
+
const addViolation = /* @__PURE__ */ __name((node, message) => {
|
|
433
|
+
violations.push({
|
|
434
|
+
file: fileName,
|
|
435
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
|
|
436
|
+
message
|
|
437
|
+
});
|
|
438
|
+
}, "addViolation");
|
|
439
|
+
const inspectTransaction = /* @__PURE__ */ __name((callback) => {
|
|
440
|
+
const transactionClient = callback.parameters[0]?.name.getText(sourceFile);
|
|
441
|
+
if (!transactionClient) return;
|
|
442
|
+
const inspectBody = /* @__PURE__ */ __name((node) => {
|
|
443
|
+
if (import_typescript2.default.isCallExpression(node)) {
|
|
444
|
+
const callee = node.expression.getText(sourceFile);
|
|
445
|
+
if (policy.rootClientNames.some((name) => callee.startsWith(`${name}.`) && transactionClient !== name)) {
|
|
446
|
+
addViolation(node, "interactive transaction uses the root Prisma client");
|
|
447
|
+
}
|
|
448
|
+
if (callee === "Promise.all" || callee === "Promise.allSettled") {
|
|
449
|
+
addViolation(node, "interactive transaction runs parallel work");
|
|
450
|
+
}
|
|
451
|
+
if (policy.externalCallPattern.test(callee)) {
|
|
452
|
+
addViolation(node, `interactive transaction performs external work via ${callee}`);
|
|
453
|
+
}
|
|
454
|
+
if (policy.transactionClientRequiredPattern.test(callee) && !node.arguments.some((argument) => argument.getText(sourceFile) === transactionClient)) {
|
|
455
|
+
addViolation(node, `service call ${callee} does not receive the transaction client`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
import_typescript2.default.forEachChild(node, inspectBody);
|
|
459
|
+
}, "inspectBody");
|
|
460
|
+
inspectBody(callback.body);
|
|
461
|
+
}, "inspectTransaction");
|
|
462
|
+
const visit = /* @__PURE__ */ __name((node) => {
|
|
463
|
+
if (import_typescript2.default.isCallExpression(node) && node.expression.getText(sourceFile).endsWith(".$transaction")) {
|
|
464
|
+
const callback = node.arguments[0];
|
|
465
|
+
if (callback && (import_typescript2.default.isArrowFunction(callback) || import_typescript2.default.isFunctionExpression(callback))) inspectTransaction(callback);
|
|
466
|
+
}
|
|
467
|
+
import_typescript2.default.forEachChild(node, visit);
|
|
468
|
+
}, "visit");
|
|
469
|
+
visit(sourceFile);
|
|
470
|
+
return violations;
|
|
471
|
+
}
|
|
472
|
+
__name(findTransactionBoundaryViolations, "findTransactionBoundaryViolations");
|
|
473
|
+
function checkTransactionBoundaries(rootDirectory, policy) {
|
|
474
|
+
const scan = /* @__PURE__ */ __name((directory) => (0, import_node_fs2.readdirSync)(directory, {
|
|
475
|
+
withFileTypes: true
|
|
476
|
+
}).flatMap((entry) => {
|
|
477
|
+
const entryPath = (0, import_node_path2.join)(directory, entry.name);
|
|
478
|
+
if (entry.isDirectory()) return scan(entryPath);
|
|
479
|
+
if (!entry.name.endsWith(".ts") || entry.name.endsWith(".spec.ts")) return [];
|
|
480
|
+
return findTransactionBoundaryViolations((0, import_node_fs2.readFileSync)(entryPath, "utf8"), entryPath, policy);
|
|
481
|
+
}), "scan");
|
|
482
|
+
return scan(rootDirectory);
|
|
483
|
+
}
|
|
484
|
+
__name(checkTransactionBoundaries, "checkTransactionBoundaries");
|
|
485
|
+
|
|
486
|
+
// src/tooling/framework-boundaries.ts
|
|
487
|
+
var PUBLIC_FRAMEWORK_SUBPATHS = /* @__PURE__ */ new Set([
|
|
488
|
+
"@zucker-framework/cli/agent-instructions",
|
|
489
|
+
"@zucker-framework/ai/langchain",
|
|
490
|
+
"@zucker-framework/core/privacy",
|
|
491
|
+
"@zucker-framework/core/user-agent"
|
|
492
|
+
]);
|
|
493
|
+
function auditFrameworkSourceText(file, source, rules = []) {
|
|
494
|
+
const violations = [];
|
|
495
|
+
const normalizedFile = file.replaceAll("\\", "/");
|
|
496
|
+
for (const specifier of importedSpecifiers(source)) {
|
|
497
|
+
if (specifier.startsWith("@zucker/")) {
|
|
498
|
+
violations.push(`${file}: replace retired @zucker/ imports with @zucker-framework/`);
|
|
499
|
+
}
|
|
500
|
+
if (specifier.startsWith("@zucker-framework/")) {
|
|
501
|
+
const segments = specifier.split("/");
|
|
502
|
+
if (segments.length !== 2 && !PUBLIC_FRAMEWORK_SUBPATHS.has(specifier)) {
|
|
503
|
+
violations.push(`${file}: import ${specifier} through a documented package public API`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (specifier.includes("zucker-framework/packages/") || specifier.includes("zucker-framework/src/")) {
|
|
507
|
+
violations.push(`${file}: do not import Zucker Framework source directly`);
|
|
508
|
+
}
|
|
509
|
+
for (const rule of rules) {
|
|
510
|
+
if (specifier === rule.specifier && rule.matchesFile(normalizedFile)) {
|
|
511
|
+
violations.push(`${file}: ${rule.message}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return violations;
|
|
516
|
+
}
|
|
517
|
+
__name(auditFrameworkSourceText, "auditFrameworkSourceText");
|
|
518
|
+
function auditPnpmWorkflowVersions(packageManager, workflowSources) {
|
|
519
|
+
const violations = [];
|
|
520
|
+
if (!/^pnpm@\d+\.\d+\.\d+$/.test(packageManager ?? "")) {
|
|
521
|
+
violations.push("package.json: packageManager must pin an exact pnpm version");
|
|
522
|
+
return violations;
|
|
523
|
+
}
|
|
524
|
+
const expectedPnpm = packageManager.slice("pnpm@".length);
|
|
525
|
+
for (const [file, source] of workflowSources) {
|
|
526
|
+
const setupPattern = /uses:\s*pnpm\/action-setup@[^\n]+/g;
|
|
527
|
+
let match;
|
|
528
|
+
while (match = setupPattern.exec(source)) {
|
|
529
|
+
const setupBlock = source.slice(match.index, match.index + 260);
|
|
530
|
+
const configuredPnpm = setupBlock.match(/\n\s*version:\s*([^\s#]+)/)?.[1];
|
|
531
|
+
if (configuredPnpm !== expectedPnpm) {
|
|
532
|
+
violations.push(`${file}: pnpm ${configuredPnpm ?? "missing"} does not match packageManager ${expectedPnpm}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return violations;
|
|
537
|
+
}
|
|
538
|
+
__name(auditPnpmWorkflowVersions, "auditPnpmWorkflowVersions");
|
|
539
|
+
function importedSpecifiers(source) {
|
|
540
|
+
const specifiers = [];
|
|
541
|
+
const importPattern = /(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']([^"']+)["']/g;
|
|
542
|
+
let match;
|
|
543
|
+
while (match = importPattern.exec(source)) specifiers.push(match[1]);
|
|
544
|
+
return specifiers;
|
|
545
|
+
}
|
|
546
|
+
__name(importedSpecifiers, "importedSpecifiers");
|
|
547
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
548
|
+
0 && (module.exports = {
|
|
549
|
+
auditFrameworkSourceText,
|
|
550
|
+
auditModuleWiring,
|
|
551
|
+
auditPnpmWorkflowVersions,
|
|
552
|
+
checkTransactionBoundaries,
|
|
553
|
+
findTransactionBoundaryViolations
|
|
554
|
+
});
|