@hublo/sentinel 1.2.0-alpha.3 → 1.2.0-alpha.30
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/bin/sentinel.d.ts +1 -0
- package/dist/bin/sentinel.js +28 -2
- package/dist/{chunk-YZ4EKBK7.js → chunk-37Y7RALA.js} +1543 -772
- package/dist/index.js +1 -1
- package/dist/roles/build/nest/toolchain.d.ts +130 -0
- package/dist/roles/build/nest/toolchain.js +403 -0
- package/dist/roles/test/react/toolchain.d.ts +2 -0
- package/dist/roles/test/react/toolchain.js +9 -0
- package/lint/react-lib.json +3 -1
- package/lint/react.json +4 -1
- package/package.json +26 -5
- package/plugins/hublo.js +68 -0
|
@@ -67,19 +67,6 @@ function resolve(target, preset, runner) {
|
|
|
67
67
|
return matching[0];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// src/core/base-adapter.ts
|
|
71
|
-
var BaseAdapter = class {
|
|
72
|
-
inspect(_ctx) {
|
|
73
|
-
throw new Error(`${this.runner}: --inspect not implemented yet`);
|
|
74
|
-
}
|
|
75
|
-
report(_ctx) {
|
|
76
|
-
throw new Error(`${this.runner}: --report not implemented yet`);
|
|
77
|
-
}
|
|
78
|
-
status(_ctx) {
|
|
79
|
-
throw new Error(`${this.runner}: --status not implemented yet`);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
70
|
// src/shared/package-json.ts
|
|
84
71
|
import { existsSync, readFileSync } from "fs";
|
|
85
72
|
import { basename, dirname, join } from "path";
|
|
@@ -132,6 +119,19 @@ function moduleName(dir) {
|
|
|
132
119
|
return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
|
|
133
120
|
}
|
|
134
121
|
|
|
122
|
+
// src/core/base-adapter.ts
|
|
123
|
+
var BaseAdapter = class {
|
|
124
|
+
inspect(_ctx) {
|
|
125
|
+
throw new Error(`${this.runner}: --inspect not implemented yet`);
|
|
126
|
+
}
|
|
127
|
+
report(_ctx) {
|
|
128
|
+
throw new Error(`${this.runner}: --report not implemented yet`);
|
|
129
|
+
}
|
|
130
|
+
status(_ctx) {
|
|
131
|
+
throw new Error(`${this.runner}: --status not implemented yet`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
135
|
// src/core/domain.ts
|
|
136
136
|
var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
|
|
137
137
|
var TARGETS = [
|
|
@@ -165,86 +165,99 @@ function palette(stream) {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// src/roles/build/
|
|
169
|
-
import {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const looksLikeOption = arg.startsWith("-");
|
|
181
|
-
const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
|
|
182
|
-
if (!previousTakesValue && !looksLikeOption && existsSync2(target)) paths.push(arg);
|
|
183
|
-
else options.push(arg);
|
|
184
|
-
previousTakesValue = !arg.includes("=") && takesValue.has(arg);
|
|
168
|
+
// src/roles/build/nest/nx-target.ts
|
|
169
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
170
|
+
import { join as join2, relative } from "path";
|
|
171
|
+
var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
|
|
172
|
+
var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
|
|
173
|
+
var readProjectJson = (cwd) => {
|
|
174
|
+
const path = join2(cwd, "project.json");
|
|
175
|
+
if (!existsSync2(path)) return void 0;
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
178
|
+
} catch {
|
|
179
|
+
return void 0;
|
|
185
180
|
}
|
|
186
|
-
|
|
181
|
+
};
|
|
182
|
+
function webpackBuildTarget(cwd) {
|
|
183
|
+
const project = readProjectJson(cwd);
|
|
184
|
+
const target = project?.targets?.build;
|
|
185
|
+
if (target?.executor !== DEPRECATED_NX_BUILDER) return void 0;
|
|
186
|
+
const { outputPath, main, webpackConfig, transformers, assets } = target.options ?? {};
|
|
187
|
+
if (typeof project?.name !== "string" || typeof outputPath !== "string" || typeof main !== "string" || typeof webpackConfig !== "string") {
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
project: project.name,
|
|
192
|
+
outputPath,
|
|
193
|
+
main,
|
|
194
|
+
webpackConfig,
|
|
195
|
+
configurations: target.configurations,
|
|
196
|
+
transformers,
|
|
197
|
+
assets
|
|
198
|
+
};
|
|
187
199
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
import { createRequire } from "module";
|
|
192
|
-
import { delimiter, dirname as dirname2, join as join2 } from "path";
|
|
193
|
-
var require2 = createRequire(import.meta.url);
|
|
194
|
-
function resolveBin(fromDir, name) {
|
|
195
|
-
let dir = fromDir;
|
|
196
|
-
for (; ; ) {
|
|
197
|
-
const candidate = join2(dir, "node_modules", ".bin", name);
|
|
198
|
-
if (existsSync3(candidate)) return candidate;
|
|
199
|
-
const parent = dirname2(dir);
|
|
200
|
-
if (parent === dir) return void 0;
|
|
201
|
-
dir = parent;
|
|
200
|
+
function refusalFor(cwd, target) {
|
|
201
|
+
if (workspaceRootFor(cwd, target) === void 0) {
|
|
202
|
+
return `this service's build target declares \`main: ${target.main}\`, which does not place this directory inside a workspace. Every value the config needs is derived from that path, so a wrong one would resolve \`workspaceRoot\` to the service itself and leave the alias table resolving nothing. Nothing was written.`;
|
|
202
203
|
}
|
|
204
|
+
if (target.webpackConfig !== TRANSLATABLE_WEBPACK_CONFIG) {
|
|
205
|
+
return `this service builds with ${target.webpackConfig}, not the shared ${TRANSLATABLE_WEBPACK_CONFIG} this preset reproduces. Its own config adds loaders or rules the preset has no equivalent for, and translating it anyway would produce a build that succeeds and a service that fails later. Read that file, decide what the Vite config needs, and write it by hand. Nothing was written.`;
|
|
206
|
+
}
|
|
207
|
+
const live = liveFileReplacements(cwd, target);
|
|
208
|
+
if (live.length > 0) {
|
|
209
|
+
return `this service's production configuration replaces ${live.join(", ")}, and those files exist. The preset has no equivalent, so translating the target would drop a real substitution. Nothing was written.`;
|
|
210
|
+
}
|
|
211
|
+
return void 0;
|
|
203
212
|
}
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
function liveFileReplacements(cwd, target) {
|
|
214
|
+
const workspaceRoot = workspaceRootFor(cwd, target);
|
|
215
|
+
if (workspaceRoot === void 0) return [];
|
|
216
|
+
const configurations = target.configurations ?? {};
|
|
217
|
+
const live = [];
|
|
218
|
+
for (const configuration of Object.values(configurations)) {
|
|
219
|
+
for (const replacement of configuration.fileReplacements ?? []) {
|
|
220
|
+
const exists = existsSync2(join2(workspaceRoot, replacement.replace)) || existsSync2(join2(workspaceRoot, replacement.with));
|
|
221
|
+
if (exists) live.push(replacement.replace);
|
|
222
|
+
}
|
|
214
223
|
}
|
|
224
|
+
return live;
|
|
215
225
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
226
|
+
function workspaceRootFor(cwd, target) {
|
|
227
|
+
const moduleRoot = target.main.replace(/\/src\/.*$/, "");
|
|
228
|
+
if (moduleRoot === target.main || !cwd.endsWith(moduleRoot)) return void 0;
|
|
229
|
+
return cwd.slice(0, cwd.length - moduleRoot.length - 1);
|
|
230
|
+
}
|
|
231
|
+
function workspaceRootHops(cwd, target) {
|
|
232
|
+
const root = workspaceRootFor(cwd, target);
|
|
233
|
+
if (root === void 0) {
|
|
234
|
+
throw new Error(`sentinel build(nest): cannot place ${cwd} in a workspace from ${target.main}`);
|
|
235
|
+
}
|
|
236
|
+
const rel = relative(root, cwd);
|
|
237
|
+
return rel === "" ? 0 : rel.split(/[\\/]/).length;
|
|
220
238
|
}
|
|
221
239
|
|
|
222
|
-
// src/roles/build/
|
|
223
|
-
import { existsSync as
|
|
224
|
-
import { join as
|
|
225
|
-
|
|
226
|
-
// src/core/config/existing-command.ts
|
|
227
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
228
|
-
import { join as join4 } from "path";
|
|
240
|
+
// src/roles/build/read-adoption.ts
|
|
241
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
242
|
+
import { join as join5 } from "path";
|
|
229
243
|
|
|
230
|
-
// src/
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
return value;
|
|
244
|
+
// src/core/config/module-type.ts
|
|
245
|
+
function esmReadiness(fileName, packageType) {
|
|
246
|
+
if (/\.(mts|mjs)$/.test(fileName)) return { kind: "ready" };
|
|
247
|
+
if (packageType === "module") return { kind: "ready" };
|
|
248
|
+
const rename = fileName.replace(/\.(ts|js)$/, ".mts");
|
|
249
|
+
return {
|
|
250
|
+
kind: "blocked",
|
|
251
|
+
why: `sentinel is ESM only, and this module ${packageType === void 0 ? 'declares no `"type"`' : `declares \`"type": "${packageType}"\``}, so Node loads ${fileName} as CommonJS and the import fails. Two remedies, and adoption takes neither by itself because both reach beyond this build: add \`"type": "module"\` to this module's package.json, which changes how EVERY \`.js\` here loads, or rename the config to \`${rename}\`, which settles it in the filename where nothing can undo it.`
|
|
252
|
+
};
|
|
240
253
|
}
|
|
241
254
|
|
|
242
255
|
// src/core/config/manifest.ts
|
|
243
|
-
import { existsSync as
|
|
256
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
244
257
|
import { basename as basename2, join as join3 } from "path";
|
|
245
258
|
function moduleScripts(cwd) {
|
|
246
259
|
try {
|
|
247
|
-
const pkg = JSON.parse(
|
|
260
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
|
|
248
261
|
return pkg.scripts ?? {};
|
|
249
262
|
} catch {
|
|
250
263
|
return {};
|
|
@@ -252,7 +265,7 @@ function moduleScripts(cwd) {
|
|
|
252
265
|
}
|
|
253
266
|
function moduleName2(cwd) {
|
|
254
267
|
try {
|
|
255
|
-
const pkg = JSON.parse(
|
|
268
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
|
|
256
269
|
return pkg.name;
|
|
257
270
|
} catch {
|
|
258
271
|
return void 0;
|
|
@@ -266,120 +279,36 @@ function selfCommand(cwd, flags) {
|
|
|
266
279
|
if (!isSelfAdoption(cwd) || !own.bin) return void 0;
|
|
267
280
|
return `node ${own.bin.replace(/^\.\//, "")} ${flags}`;
|
|
268
281
|
}
|
|
282
|
+
function adoptedPackageName(cwd) {
|
|
283
|
+
return readProjectPackageJson(cwd).name ?? readNxProjectName(cwd) ?? basename2(cwd);
|
|
284
|
+
}
|
|
269
285
|
function manifestOperation(cwd, scripts) {
|
|
270
286
|
const own = readOwnPackage();
|
|
271
287
|
const value = {
|
|
272
288
|
scripts,
|
|
273
289
|
devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
|
|
274
290
|
};
|
|
275
|
-
if (
|
|
291
|
+
if (existsSync3(join3(cwd, "package.json"))) {
|
|
276
292
|
return { kind: "merge-json", path: "package.json", value };
|
|
277
293
|
}
|
|
278
294
|
return {
|
|
279
295
|
kind: "merge-json",
|
|
280
296
|
path: "package.json",
|
|
281
|
-
value: { name:
|
|
297
|
+
value: { name: adoptedPackageName(cwd), private: true, ...value }
|
|
282
298
|
};
|
|
283
299
|
}
|
|
284
300
|
|
|
285
|
-
// src/core/config/
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
// src/core/config/project-json.ts
|
|
302
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
303
|
+
import { join as join4 } from "path";
|
|
304
|
+
function projectTargets(cwd) {
|
|
305
|
+
const path = join4(cwd, "project.json");
|
|
306
|
+
if (!existsSync4(path)) return void 0;
|
|
288
307
|
try {
|
|
289
|
-
|
|
290
|
-
readFileSync3(join4(cwd, "project.json"), "utf8"),
|
|
291
|
-
"project.json"
|
|
292
|
-
);
|
|
308
|
+
return JSON.parse(readFileSync4(path, "utf8")).targets;
|
|
293
309
|
} catch {
|
|
294
310
|
return void 0;
|
|
295
311
|
}
|
|
296
|
-
const entry = project.targets?.[target];
|
|
297
|
-
if (!entry || entry.executor !== "nx:run-commands") return void 0;
|
|
298
|
-
const { command, commands, cwd: targetCwd } = entry.options ?? {};
|
|
299
|
-
const ranInModule = typeof targetCwd === "string" && targetCwd.trim() !== "";
|
|
300
|
-
if (typeof command === "string" && command.trim() !== "") {
|
|
301
|
-
return { command: command.trim(), ranInModule };
|
|
302
|
-
}
|
|
303
|
-
if (Array.isArray(commands)) {
|
|
304
|
-
const parts = commands.filter((c) => typeof c === "string" && c.trim() !== "");
|
|
305
|
-
if (parts.length > 0) return { command: parts.map((c) => c.trim()).join(" && "), ranInModule };
|
|
306
|
-
}
|
|
307
|
-
return void 0;
|
|
308
|
-
}
|
|
309
|
-
function rootedForScript(command) {
|
|
310
|
-
return command.replace(/(^|&&\s*)pnpm exec /g, "$1pnpm --workspace-root exec ");
|
|
311
|
-
}
|
|
312
|
-
function existingCommand(cwd, target) {
|
|
313
|
-
const script = moduleScripts(cwd)[target];
|
|
314
|
-
if (script !== void 0 && script.trim() !== "") return script;
|
|
315
|
-
const fromTarget = nxTargetCommand(cwd, target);
|
|
316
|
-
if (fromTarget === void 0) return void 0;
|
|
317
|
-
return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// src/core/config/nx-target.ts
|
|
321
|
-
import { existsSync as existsSync5 } from "fs";
|
|
322
|
-
import { join as join5 } from "path";
|
|
323
|
-
function nxTargetOperations(options) {
|
|
324
|
-
const { cwd, targets } = options;
|
|
325
|
-
const names = Object.keys(targets);
|
|
326
|
-
if (names.length === 0) return [];
|
|
327
|
-
const operations = [];
|
|
328
|
-
if (existsSync5(join5(cwd, "project.json"))) {
|
|
329
|
-
operations.push({
|
|
330
|
-
kind: "remove-json-keys",
|
|
331
|
-
path: "project.json",
|
|
332
|
-
keys: names.map((name) => ["targets", name])
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
operations.push({
|
|
336
|
-
kind: "merge-json",
|
|
337
|
-
path: "package.json",
|
|
338
|
-
value: { nx: { targets } }
|
|
339
|
-
});
|
|
340
|
-
return operations;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// src/core/config/tool-script.ts
|
|
344
|
-
var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
|
|
345
|
-
function isSeparatorAt(index) {
|
|
346
|
-
return index % 2 === 1;
|
|
347
|
-
}
|
|
348
|
-
function invokes(segment, binary) {
|
|
349
|
-
return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
|
|
350
|
-
}
|
|
351
|
-
function isSentinelSegment(segment, roleFlag) {
|
|
352
|
-
const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
|
|
353
|
-
return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
|
|
354
|
-
}
|
|
355
|
-
function composeToolScript(existing, options) {
|
|
356
|
-
const { command } = options;
|
|
357
|
-
if (!existing || existing.trim() === "") return command;
|
|
358
|
-
const { roleFlag } = options;
|
|
359
|
-
const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
|
|
360
|
-
const parts = existing.split(SEGMENT_SEPARATOR);
|
|
361
|
-
const commands = parts.filter((_, index) => !isSeparatorAt(index));
|
|
362
|
-
if (!commands.some(isReplaceable)) return existing;
|
|
363
|
-
let replacedOnce = false;
|
|
364
|
-
const rebuilt = parts.map((part, index) => {
|
|
365
|
-
if (isSeparatorAt(index) || !isReplaceable(part)) return part;
|
|
366
|
-
if (replacedOnce) return null;
|
|
367
|
-
replacedOnce = true;
|
|
368
|
-
return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
|
|
369
|
-
});
|
|
370
|
-
const kept = [];
|
|
371
|
-
for (let index = 0; index < rebuilt.length; index += 1) {
|
|
372
|
-
const part = rebuilt[index];
|
|
373
|
-
if (part === null) {
|
|
374
|
-
if (kept.length > 0) kept.pop();
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
kept.push(part);
|
|
378
|
-
}
|
|
379
|
-
return kept.join("").trim();
|
|
380
|
-
}
|
|
381
|
-
function keepsOtherCommands(script, sentinelCommand) {
|
|
382
|
-
return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
|
|
383
312
|
}
|
|
384
313
|
|
|
385
314
|
// src/roles/build/presets/requirements.json
|
|
@@ -408,7 +337,8 @@ var toolchain_default = {
|
|
|
408
337
|
"UserConfig",
|
|
409
338
|
"UserConfigExport"
|
|
410
339
|
],
|
|
411
|
-
why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck."
|
|
340
|
+
why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck.",
|
|
341
|
+
floor: "Never below 8.2.2. Vite 8.0.8 resolves rolldown 1.0.0-rc.15, a release candidate whose deferred-module-initialisation codegen is broken: it wrapped a plain re-export barrel in an init helper, then required the module without binding its exports before calling the very function that binding provides. Measured, that crashed 7 of the 8 Nest services with a transformer at their first require (`init_<module> is not defined`), and quietly moved the published contract of 4 others. 8.2.2 resolves rolldown 1.2.6 and all 35 services regenerate their committed OpenAPI document identically. The version is not a preference, it is the floor at which those measurements hold."
|
|
412
342
|
},
|
|
413
343
|
{
|
|
414
344
|
package: "@vitejs/plugin-react",
|
|
@@ -467,16 +397,28 @@ var BUILD_CONFIG_FILES = [
|
|
|
467
397
|
"vite.config.js",
|
|
468
398
|
"vite.config.mjs"
|
|
469
399
|
];
|
|
470
|
-
var
|
|
400
|
+
var BUILD_PRESET_SPECIFIERS = {
|
|
401
|
+
react: "@hublo/sentinel/build/react",
|
|
402
|
+
nest: "@hublo/sentinel/build/nest"
|
|
403
|
+
};
|
|
404
|
+
var BUILD_PRESET_SPECIFIER = BUILD_PRESET_SPECIFIERS.react;
|
|
471
405
|
var BUILD_SCRIPT_NAME = "build";
|
|
472
|
-
var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
|
|
406
|
+
var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
|
|
473
407
|
var DEV_SCRIPT_NAME = "serve";
|
|
474
408
|
var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
|
|
475
|
-
|
|
409
|
+
var BUILD_OUTPUT_CANDIDATES = ["dist", ".output", "build"];
|
|
410
|
+
function buildTarget(options) {
|
|
411
|
+
const { moduleName: moduleName3, configFiles = BUILD_CONFIG_FILES, outputs } = options;
|
|
476
412
|
return {
|
|
477
413
|
[BUILD_SCRIPT_NAME]: {
|
|
414
|
+
executor: "nx:run-commands",
|
|
415
|
+
options: {
|
|
416
|
+
command: `pnpm --filter ${moduleName3} run ${BUILD_SCRIPT_NAME}`,
|
|
417
|
+
forwardAllArgs: false
|
|
418
|
+
},
|
|
478
419
|
cache: true,
|
|
479
|
-
inputs: ["default", "^default", ...
|
|
420
|
+
inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
|
|
421
|
+
outputs: [...outputs]
|
|
480
422
|
}
|
|
481
423
|
};
|
|
482
424
|
}
|
|
@@ -484,29 +426,58 @@ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
|
|
|
484
426
|
(entry) => entry.package
|
|
485
427
|
);
|
|
486
428
|
|
|
487
|
-
// src/roles/build/
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
429
|
+
// src/roles/build/adoption-drift.ts
|
|
430
|
+
function buildAdoptionDrift(cwd) {
|
|
431
|
+
const drift = [];
|
|
432
|
+
const manifest = readProjectPackageJson(cwd);
|
|
433
|
+
const script = moduleScripts(cwd)[BUILD_SCRIPT_NAME];
|
|
434
|
+
if (script === void 0) {
|
|
435
|
+
drift.push(
|
|
436
|
+
`has no \`${BUILD_SCRIPT_NAME}\` npm script, so \`pnpm run ${BUILD_SCRIPT_NAME}\` does not build it and only nx can`
|
|
437
|
+
);
|
|
438
|
+
} else if (!script.includes(SENTINEL_BUILD_COMMAND.replace(/ --$/, ""))) {
|
|
439
|
+
drift.push(
|
|
440
|
+
`its \`${BUILD_SCRIPT_NAME}\` script does not invoke sentinel (${script}), so the config points at sentinel while the script runs something else`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const target = manifest.nx?.targets?.[BUILD_SCRIPT_NAME];
|
|
444
|
+
if (target === void 0) {
|
|
445
|
+
drift.push(
|
|
446
|
+
`declares no \`nx.targets.${BUILD_SCRIPT_NAME}\`, so its build runs on nx's default inputs while every other target it has declares its own`
|
|
447
|
+
);
|
|
448
|
+
} else {
|
|
449
|
+
if (target.cache === true && (target.outputs ?? []).length === 0) {
|
|
450
|
+
drift.push(
|
|
451
|
+
`caches its build and declares no \`outputs\`, so nx stores nothing and a cache hit reports success with no build on disk`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const inputs = target.inputs ?? [];
|
|
455
|
+
if (inputs.length > 0 && !inputs.includes("^default")) {
|
|
456
|
+
drift.push(
|
|
457
|
+
`declares \`inputs\` without \`^default\`, which REPLACES nx's default and stops hashing its dependencies: the build can replay a stale bundle after one changes`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (target.options?.forwardAllArgs !== false) {
|
|
461
|
+
drift.push(
|
|
462
|
+
`does not set \`forwardAllArgs: false\`, so a caller's extra flag reaches the bundler, which refuses it (the image passes \`--generatePackageJson\`)`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (projectTargets(cwd)?.[BUILD_SCRIPT_NAME] !== void 0) {
|
|
467
|
+
drift.push(
|
|
468
|
+
`still declares a \`${BUILD_SCRIPT_NAME}\` target in project.json beside the one in package.json, so the module has two and which runs depends on the caller`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const devScript = moduleScripts(cwd)[DEV_SCRIPT_NAME];
|
|
472
|
+
if (devScript?.includes(SENTINEL_DEV_COMMAND) === true && projectTargets(cwd)?.[DEV_SCRIPT_NAME] !== void 0) {
|
|
473
|
+
drift.push(
|
|
474
|
+
`runs its \`${DEV_SCRIPT_NAME}\` script through sentinel but still declares a \`${DEV_SCRIPT_NAME}\` target in project.json, so one dev server has two declarations and editing the target stops the script from being what runs`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return drift;
|
|
505
478
|
}
|
|
506
479
|
|
|
507
480
|
// src/roles/build/read-adoption.ts
|
|
508
|
-
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
509
|
-
import { join as join6 } from "path";
|
|
510
481
|
var NOT_ADOPTED = (configFile, unreadable = null) => ({
|
|
511
482
|
configFile,
|
|
512
483
|
preset: null,
|
|
@@ -517,19 +488,25 @@ var NOT_ADOPTED = (configFile, unreadable = null) => ({
|
|
|
517
488
|
unreadable
|
|
518
489
|
});
|
|
519
490
|
function buildConfigFile(cwd) {
|
|
520
|
-
return BUILD_CONFIG_FILES.find((name) =>
|
|
491
|
+
return BUILD_CONFIG_FILES.find((name) => existsSync5(join5(cwd, name)));
|
|
521
492
|
}
|
|
522
493
|
function importsSpecifier(source, specifier) {
|
|
523
494
|
const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
524
495
|
return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
|
|
525
496
|
}
|
|
526
497
|
function importsPreset(source) {
|
|
527
|
-
return
|
|
498
|
+
return Object.values(BUILD_PRESET_SPECIFIERS).some(
|
|
499
|
+
(specifier) => importsSpecifier(source, specifier)
|
|
500
|
+
);
|
|
528
501
|
}
|
|
529
502
|
var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
|
|
530
503
|
function declaredBuildPreset(cwd) {
|
|
531
504
|
const source = readBuildConfigSource(cwd);
|
|
532
|
-
if (source === void 0)
|
|
505
|
+
if (source === void 0) {
|
|
506
|
+
if (buildConfigFile(cwd) !== void 0) return void 0;
|
|
507
|
+
return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
|
|
508
|
+
}
|
|
509
|
+
if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
|
|
533
510
|
if (importsPreset(source)) return "react";
|
|
534
511
|
return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
|
|
535
512
|
}
|
|
@@ -537,7 +514,7 @@ function readBuildConfigSource(cwd) {
|
|
|
537
514
|
const configFile = buildConfigFile(cwd);
|
|
538
515
|
if (!configFile) return void 0;
|
|
539
516
|
try {
|
|
540
|
-
return
|
|
517
|
+
return readFileSync5(join5(cwd, configFile), "utf8");
|
|
541
518
|
} catch {
|
|
542
519
|
return void 0;
|
|
543
520
|
}
|
|
@@ -547,38 +524,494 @@ function readBuildAdoption(cwd) {
|
|
|
547
524
|
if (!configFile) return NOT_ADOPTED(null);
|
|
548
525
|
let source;
|
|
549
526
|
try {
|
|
550
|
-
source =
|
|
527
|
+
source = readFileSync5(join5(cwd, configFile), "utf8");
|
|
551
528
|
} catch (error) {
|
|
552
529
|
return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
|
|
553
530
|
}
|
|
554
|
-
if (!importsPreset(source))
|
|
531
|
+
if (!importsPreset(source)) {
|
|
532
|
+
const esm = esmReadiness(configFile, readProjectPackageJson(cwd).type);
|
|
533
|
+
return NOT_ADOPTED(configFile, esm.kind === "blocked" ? esm.why : null);
|
|
534
|
+
}
|
|
555
535
|
const manifest = readProjectPackageJson(cwd);
|
|
556
536
|
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
557
537
|
const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
|
|
558
538
|
(name) => ({ name, version: declared[name] })
|
|
559
539
|
);
|
|
540
|
+
const drift = [
|
|
541
|
+
...ownDeclarations.map(
|
|
542
|
+
({ name, version }) => `declares ${name}@${version} of its own; sentinel owns it, and two copies in one build give the plugins a different Vite than the one running them`
|
|
543
|
+
),
|
|
544
|
+
...buildAdoptionDrift(cwd)
|
|
545
|
+
];
|
|
560
546
|
return {
|
|
561
547
|
configFile,
|
|
562
|
-
|
|
548
|
+
// Read from the config rather than assumed: the two presets are adopted the same way and
|
|
549
|
+
// reporting the wrong one would make `--inspect` lie about what a module builds.
|
|
550
|
+
preset: declaredBuildPreset(cwd) ?? "react",
|
|
563
551
|
adopted: true,
|
|
564
|
-
conformant:
|
|
565
|
-
drift
|
|
566
|
-
({ name, version }) => `declares ${name}@${version} of its own; sentinel owns it, and two copies in one build give the plugins a different Vite than the one running them`
|
|
567
|
-
),
|
|
552
|
+
conformant: drift.length === 0,
|
|
553
|
+
drift,
|
|
568
554
|
ownDeclarations,
|
|
569
555
|
unreadable: null
|
|
570
556
|
};
|
|
571
557
|
}
|
|
572
558
|
|
|
573
|
-
// src/roles/build/
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
559
|
+
// src/roles/build/adapters/vite/vite-role.adapter.ts
|
|
560
|
+
import { spawnSync } from "child_process";
|
|
561
|
+
|
|
562
|
+
// src/core/config/tool-args.ts
|
|
563
|
+
import { existsSync as existsSync6 } from "fs";
|
|
564
|
+
import { isAbsolute, resolve as resolve2 } from "path";
|
|
565
|
+
function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
|
|
566
|
+
const takesValue = new Set(valueFlags);
|
|
567
|
+
const options = [];
|
|
568
|
+
const paths = [];
|
|
569
|
+
let previousTakesValue = false;
|
|
570
|
+
for (const arg of toolArgs) {
|
|
571
|
+
const looksLikeOption = arg.startsWith("-");
|
|
572
|
+
const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
|
|
573
|
+
if (!previousTakesValue && !looksLikeOption && existsSync6(target)) paths.push(arg);
|
|
574
|
+
else options.push(arg);
|
|
575
|
+
previousTakesValue = !arg.includes("=") && takesValue.has(arg);
|
|
576
|
+
}
|
|
577
|
+
return { options, paths };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/shared/resolve-bin.ts
|
|
581
|
+
import { existsSync as existsSync7 } from "fs";
|
|
582
|
+
import { createRequire } from "module";
|
|
583
|
+
import { delimiter, dirname as dirname2, join as join6 } from "path";
|
|
584
|
+
var require2 = createRequire(import.meta.url);
|
|
585
|
+
function resolveBin(fromDir, name) {
|
|
586
|
+
let dir = fromDir;
|
|
587
|
+
for (; ; ) {
|
|
588
|
+
const candidate = join6(dir, "node_modules", ".bin", name);
|
|
589
|
+
if (existsSync7(candidate)) return candidate;
|
|
590
|
+
const parent = dirname2(dir);
|
|
591
|
+
if (parent === dir) return void 0;
|
|
592
|
+
dir = parent;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function binFromOwnInstall(packageName, binName) {
|
|
596
|
+
try {
|
|
597
|
+
const manifest = require2.resolve(`${packageName}/package.json`);
|
|
598
|
+
const bin = require2(manifest).bin;
|
|
599
|
+
const relative5 = typeof bin === "string" ? bin : bin?.[binName];
|
|
600
|
+
if (!relative5) return void 0;
|
|
601
|
+
const executable = join6(dirname2(manifest), relative5);
|
|
602
|
+
return existsSync7(executable) ? executable : void 0;
|
|
603
|
+
} catch {
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function binSearchPath(cwd) {
|
|
608
|
+
return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
|
|
609
|
+
" then "
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/roles/build/plan.ts
|
|
614
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
615
|
+
import { join as join11 } from "path";
|
|
616
|
+
|
|
617
|
+
// src/core/config/existing-command.ts
|
|
618
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
619
|
+
import { join as join7 } from "path";
|
|
620
|
+
|
|
621
|
+
// src/shared/jsonc.ts
|
|
622
|
+
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
623
|
+
function parseJsonc(text, source = "config") {
|
|
624
|
+
const errors = [];
|
|
625
|
+
const value = parse(text, errors, { allowTrailingComma: true });
|
|
626
|
+
if (errors.length > 0) {
|
|
627
|
+
const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
|
|
628
|
+
throw new Error(`${source}: malformed JSONC (${details}).`);
|
|
629
|
+
}
|
|
630
|
+
return value;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/core/config/existing-command.ts
|
|
634
|
+
function nxTargetCommand(cwd, target) {
|
|
635
|
+
let project;
|
|
636
|
+
try {
|
|
637
|
+
project = parseJsonc(
|
|
638
|
+
readFileSync6(join7(cwd, "project.json"), "utf8"),
|
|
639
|
+
"project.json"
|
|
640
|
+
);
|
|
641
|
+
} catch {
|
|
642
|
+
return void 0;
|
|
643
|
+
}
|
|
644
|
+
const entry = project.targets?.[target];
|
|
645
|
+
if (!entry || entry.executor !== "nx:run-commands") return void 0;
|
|
646
|
+
const { command, commands, cwd: targetCwd } = entry.options ?? {};
|
|
647
|
+
const ranInModule = typeof targetCwd === "string" && targetCwd.trim() !== "";
|
|
648
|
+
if (typeof command === "string" && command.trim() !== "") {
|
|
649
|
+
return { command: command.trim(), ranInModule };
|
|
650
|
+
}
|
|
651
|
+
if (Array.isArray(commands)) {
|
|
652
|
+
const parts = commands.filter((c) => typeof c === "string" && c.trim() !== "");
|
|
653
|
+
if (parts.length > 0) return { command: parts.map((c) => c.trim()).join(" && "), ranInModule };
|
|
654
|
+
}
|
|
655
|
+
return void 0;
|
|
656
|
+
}
|
|
657
|
+
function rootedForScript(command) {
|
|
658
|
+
return command.replace(/(^|&&\s*)pnpm exec /g, "$1pnpm --workspace-root exec ");
|
|
659
|
+
}
|
|
660
|
+
function existingCommand(cwd, target) {
|
|
661
|
+
const script = moduleScripts(cwd)[target];
|
|
662
|
+
if (script !== void 0 && script.trim() !== "") return script;
|
|
663
|
+
const fromTarget = nxTargetCommand(cwd, target);
|
|
664
|
+
if (fromTarget === void 0) return void 0;
|
|
665
|
+
return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/core/config/nx-target.ts
|
|
669
|
+
import { existsSync as existsSync8 } from "fs";
|
|
670
|
+
import { join as join8 } from "path";
|
|
671
|
+
function nxTargetMetadataOperations(options) {
|
|
672
|
+
const { targets } = options;
|
|
673
|
+
if (Object.keys(targets).length === 0) return [];
|
|
674
|
+
return [{ kind: "merge-json", path: "package.json", value: { nx: { targets } } }];
|
|
675
|
+
}
|
|
676
|
+
function nxTargetOperations(options) {
|
|
677
|
+
const { cwd, targets } = options;
|
|
678
|
+
const names = Object.keys(targets);
|
|
679
|
+
if (names.length === 0) return [];
|
|
680
|
+
const operations = [];
|
|
681
|
+
if (existsSync8(join8(cwd, "project.json"))) {
|
|
682
|
+
operations.push({
|
|
683
|
+
kind: "remove-json-keys",
|
|
684
|
+
path: "project.json",
|
|
685
|
+
keys: names.map((name) => ["targets", name])
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
operations.push(...nxTargetMetadataOperations(options));
|
|
689
|
+
return operations;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/core/config/tool-script.ts
|
|
693
|
+
var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
|
|
694
|
+
function isSeparatorAt(index) {
|
|
695
|
+
return index % 2 === 1;
|
|
696
|
+
}
|
|
697
|
+
function invokes(segment, binary) {
|
|
698
|
+
return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
|
|
699
|
+
}
|
|
700
|
+
function isSentinelSegment(segment, roleFlag) {
|
|
701
|
+
const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
|
|
702
|
+
return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
|
|
703
|
+
}
|
|
704
|
+
function composeToolScript(existing, options) {
|
|
705
|
+
const { command } = options;
|
|
706
|
+
if (!existing || existing.trim() === "") return command;
|
|
707
|
+
const { roleFlag } = options;
|
|
708
|
+
const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
|
|
709
|
+
const parts = existing.split(SEGMENT_SEPARATOR);
|
|
710
|
+
const commands = parts.filter((_, index) => !isSeparatorAt(index));
|
|
711
|
+
if (!commands.some(isReplaceable)) return existing;
|
|
712
|
+
let replacedOnce = false;
|
|
713
|
+
const rebuilt = parts.map((part, index) => {
|
|
714
|
+
if (isSeparatorAt(index) || !isReplaceable(part)) return part;
|
|
715
|
+
if (replacedOnce) return null;
|
|
716
|
+
replacedOnce = true;
|
|
717
|
+
return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
|
|
718
|
+
});
|
|
719
|
+
const kept = [];
|
|
720
|
+
for (let index = 0; index < rebuilt.length; index += 1) {
|
|
721
|
+
const part = rebuilt[index];
|
|
722
|
+
if (part === null) {
|
|
723
|
+
if (kept.length > 0) kept.pop();
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
kept.push(part);
|
|
727
|
+
}
|
|
728
|
+
return kept.join("").trim();
|
|
729
|
+
}
|
|
730
|
+
function keepsOtherCommands(script, sentinelCommand) {
|
|
731
|
+
return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/roles/format/read-adoption.ts
|
|
735
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
736
|
+
import { join as join9 } from "path";
|
|
737
|
+
|
|
738
|
+
// src/roles/format/config-policy.ts
|
|
739
|
+
var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
|
|
740
|
+
var FORMAT_SCRIPT_NAME = "format";
|
|
741
|
+
var PROVENANCE_KEY = "$sentinel";
|
|
742
|
+
var PERMITTED_LOCAL_KEYS = [
|
|
743
|
+
"useTabs",
|
|
744
|
+
"tabWidth",
|
|
745
|
+
"printWidth",
|
|
746
|
+
"semi",
|
|
747
|
+
"singleQuote",
|
|
748
|
+
"jsxSingleQuote",
|
|
749
|
+
"trailingComma",
|
|
750
|
+
"quoteProps",
|
|
751
|
+
"bracketSpacing",
|
|
752
|
+
"bracketSameLine",
|
|
753
|
+
"arrowParens",
|
|
754
|
+
"endOfLine",
|
|
755
|
+
"objectWrap",
|
|
756
|
+
"proseWrap",
|
|
757
|
+
"experimentalOperatorPosition"
|
|
758
|
+
];
|
|
759
|
+
var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
|
|
760
|
+
var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
|
|
761
|
+
var PRETTIER_CONFIG_FILES = [
|
|
762
|
+
".prettierrc",
|
|
763
|
+
".prettierrc.json",
|
|
764
|
+
".prettierrc.json5",
|
|
765
|
+
".prettierrc.yml",
|
|
766
|
+
".prettierrc.yaml",
|
|
767
|
+
".prettierrc.js",
|
|
768
|
+
".prettierrc.cjs",
|
|
769
|
+
".prettierrc.mjs",
|
|
770
|
+
"prettier.config.js",
|
|
771
|
+
"prettier.config.cjs",
|
|
772
|
+
"prettier.config.mjs"
|
|
773
|
+
];
|
|
774
|
+
function formatTargets() {
|
|
775
|
+
return {
|
|
776
|
+
[FORMAT_SCRIPT_NAME]: {
|
|
777
|
+
cache: true,
|
|
778
|
+
inputs: ["default", `{projectRoot}/${FORMAT_CONFIG_FILE}`]
|
|
779
|
+
},
|
|
780
|
+
[`${FORMAT_SCRIPT_NAME}:fix`]: { cache: false }
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/roles/format/presets/base.json
|
|
785
|
+
var base_default = {
|
|
786
|
+
printWidth: 80,
|
|
787
|
+
semi: false,
|
|
788
|
+
singleQuote: true,
|
|
789
|
+
trailingComma: "all",
|
|
790
|
+
sortPackageJson: false,
|
|
791
|
+
sortImports: false
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// src/roles/format/presets/nest-imports.json
|
|
795
|
+
var nest_imports_default = {
|
|
796
|
+
customGroups: [
|
|
797
|
+
{
|
|
798
|
+
groupName: "workspace",
|
|
799
|
+
elementNamePattern: ["@hublo/**", "@shared/**", "@front/**", "@network/**"]
|
|
800
|
+
}
|
|
801
|
+
],
|
|
802
|
+
groups: ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
|
|
803
|
+
newlinesBetween: true,
|
|
804
|
+
order: "asc",
|
|
805
|
+
ignoreCase: false
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// src/roles/format/preset-data.ts
|
|
809
|
+
var FORMAT_PRESETS = ["base", "svelte", "nest"];
|
|
810
|
+
function hasFormatPreset(name) {
|
|
811
|
+
return FORMAT_PRESETS.includes(name);
|
|
812
|
+
}
|
|
813
|
+
function formatPresetName(declared) {
|
|
814
|
+
if (declared === "svelte") return "svelte";
|
|
815
|
+
if (declared === "nest") return "nest";
|
|
816
|
+
return "base";
|
|
817
|
+
}
|
|
818
|
+
function formatPresetFor(preset) {
|
|
819
|
+
const base = base_default;
|
|
820
|
+
const name = formatPresetName(preset);
|
|
821
|
+
if (name === "svelte") return { ...base, svelte: true };
|
|
822
|
+
if (name === "nest") return { ...base, sortImports: nest_imports_default };
|
|
823
|
+
return base;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/roles/format/read-adoption.ts
|
|
827
|
+
var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
|
|
828
|
+
configFile,
|
|
829
|
+
preset: null,
|
|
830
|
+
adopted: false,
|
|
831
|
+
conformant: false,
|
|
832
|
+
drift: [],
|
|
833
|
+
local: {},
|
|
834
|
+
presetVersion: null,
|
|
835
|
+
effective: {},
|
|
836
|
+
unreadable
|
|
837
|
+
});
|
|
838
|
+
function sameValue(a, b) {
|
|
839
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
840
|
+
}
|
|
841
|
+
function readFormatAdoption(cwd) {
|
|
842
|
+
const path = join9(cwd, FORMAT_CONFIG_FILE);
|
|
843
|
+
if (!existsSync9(path)) return NOT_ADOPTED2(null);
|
|
844
|
+
let parsed;
|
|
845
|
+
try {
|
|
846
|
+
parsed = parseJsonc(readFileSync7(path, "utf8"), FORMAT_CONFIG_FILE);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
849
|
+
return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
|
|
850
|
+
}
|
|
851
|
+
const provenance = parsed[PROVENANCE_KEY];
|
|
852
|
+
if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED2(FORMAT_CONFIG_FILE);
|
|
853
|
+
if (!hasFormatPreset(provenance.preset)) {
|
|
854
|
+
return NOT_ADOPTED2(
|
|
855
|
+
FORMAT_CONFIG_FILE,
|
|
856
|
+
`${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const declaredLocal = Array.isArray(provenance.local) ? provenance.local : [];
|
|
860
|
+
const preset = formatPresetFor(provenance.preset);
|
|
861
|
+
const drift = [];
|
|
862
|
+
const local = {};
|
|
863
|
+
for (const [key, expected] of Object.entries(preset)) {
|
|
864
|
+
if (declaredLocal.includes(key)) {
|
|
865
|
+
local[key] = parsed[key];
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (!(key in parsed) || !sameValue(parsed[key], expected)) drift.push(key);
|
|
869
|
+
}
|
|
870
|
+
for (const key of declaredLocal) {
|
|
871
|
+
if (!(key in preset)) local[key] = parsed[key];
|
|
872
|
+
}
|
|
873
|
+
for (const key of ALWAYS_LOCAL_KEYS) {
|
|
874
|
+
if (key in parsed) local[key] = parsed[key];
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
configFile: FORMAT_CONFIG_FILE,
|
|
878
|
+
preset: provenance.preset,
|
|
879
|
+
adopted: true,
|
|
880
|
+
conformant: drift.length === 0,
|
|
881
|
+
drift,
|
|
882
|
+
local,
|
|
883
|
+
presetVersion: typeof provenance.version === "string" ? provenance.version : null,
|
|
884
|
+
effective: parsed,
|
|
885
|
+
unreadable: null
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/roles/build/dev-script.ts
|
|
890
|
+
var CHAIN = " -- ";
|
|
891
|
+
var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
|
|
892
|
+
var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
|
|
893
|
+
function appOptions(rest) {
|
|
894
|
+
const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
|
|
895
|
+
const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
|
|
896
|
+
return kept.join(" ");
|
|
897
|
+
}
|
|
898
|
+
function composeDevScript(existing, command) {
|
|
899
|
+
if (existing === void 0 || existing.trim() === "") return command;
|
|
900
|
+
const chunks = existing.trim().split(CHAIN);
|
|
901
|
+
const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
|
|
902
|
+
if (at === -1) return existing;
|
|
903
|
+
const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
|
|
904
|
+
const options = appOptions(rest);
|
|
905
|
+
chunks[at] = options === "" ? `${command} --` : `${command} -- ${options}`;
|
|
906
|
+
return chunks.join(CHAIN);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/roles/build/dev-target.ts
|
|
910
|
+
var DELEGATION_KEYS = /* @__PURE__ */ new Set(["executor", "options"]);
|
|
911
|
+
var DELEGATION_OPTION_KEYS = /* @__PURE__ */ new Set(["command", "cwd"]);
|
|
912
|
+
function delegatesToScript(target, scriptName) {
|
|
913
|
+
if (typeof target !== "object" || target === null || Array.isArray(target)) return false;
|
|
914
|
+
const entries = target;
|
|
915
|
+
if (!Object.keys(entries).every((key) => DELEGATION_KEYS.has(key))) return false;
|
|
916
|
+
if (entries["executor"] !== "nx:run-commands") return false;
|
|
917
|
+
const options = entries["options"];
|
|
918
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) return false;
|
|
919
|
+
const declared = options;
|
|
920
|
+
if (!Object.keys(declared).every((key) => DELEGATION_OPTION_KEYS.has(key))) return false;
|
|
921
|
+
const command = declared["command"];
|
|
922
|
+
if (typeof command !== "string") return false;
|
|
923
|
+
return new RegExp(String.raw`(^|\s)run\s+${scriptName}$`).test(command.trim());
|
|
924
|
+
}
|
|
925
|
+
function devScriptIsAdopted(devScript) {
|
|
926
|
+
return devScript !== void 0 && devScript.includes(SENTINEL_DEV_COMMAND);
|
|
927
|
+
}
|
|
928
|
+
function devTargetOperations(cwd, devScript) {
|
|
929
|
+
if (!devScriptIsAdopted(devScript)) return { operations: [] };
|
|
930
|
+
const target = projectTargets(cwd)?.[DEV_SCRIPT_NAME];
|
|
931
|
+
if (target === void 0) return { operations: [] };
|
|
932
|
+
if (!delegatesToScript(target, DEV_SCRIPT_NAME)) {
|
|
933
|
+
return {
|
|
934
|
+
operations: [],
|
|
935
|
+
note: `left the \`${DEV_SCRIPT_NAME}\` target in project.json alone: it does more than call \`pnpm run ${DEV_SCRIPT_NAME}\`, so removing it would drop behaviour this role never declared. Move what it does into the script, or keep it and accept the second home.`
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
operations: [
|
|
940
|
+
{ kind: "remove-json-keys", path: "project.json", keys: [["targets", DEV_SCRIPT_NAME]] }
|
|
941
|
+
],
|
|
942
|
+
note: `removed the \`${DEV_SCRIPT_NAME}\` target from project.json: it only called \`pnpm run ${DEV_SCRIPT_NAME}\`, which nx infers from the script itself. host-admin has run without that target all along.`
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/roles/build/nest/adopt.ts
|
|
947
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
948
|
+
import { join as join10 } from "path";
|
|
949
|
+
var NEST_CONFIG_FILE = "vite.config.mts";
|
|
950
|
+
function asSource(value, depth) {
|
|
951
|
+
const pad = " ".repeat(depth);
|
|
952
|
+
const inner = " ".repeat(depth + 1);
|
|
953
|
+
if (Array.isArray(value)) {
|
|
954
|
+
if (value.length === 0) return "[]";
|
|
955
|
+
return `[
|
|
956
|
+
${value.map((item) => `${inner}${asSource(item, depth + 1)}`).join(",\n")},
|
|
957
|
+
${pad}]`;
|
|
958
|
+
}
|
|
959
|
+
if (value !== null && typeof value === "object") {
|
|
960
|
+
const entries = Object.entries(value);
|
|
961
|
+
if (entries.length === 0) return "{}";
|
|
962
|
+
return `{
|
|
963
|
+
${entries.map(([key, item]) => `${inner}${asKey(key)}: ${asSource(item, depth + 1)}`).join(",\n")},
|
|
964
|
+
${pad}}`;
|
|
965
|
+
}
|
|
966
|
+
if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
967
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
968
|
+
throw new Error(
|
|
969
|
+
`sentinel build(nest): cannot write ${typeof value} into the generated config. The build target carries a shape this translation was not written for, and it should refuse rather than emit something nobody has read.`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
function asKey(key) {
|
|
973
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${key}'`;
|
|
974
|
+
}
|
|
975
|
+
function nestConfigSource(cwd, target) {
|
|
976
|
+
const hops = workspaceRootHops(cwd, target);
|
|
977
|
+
const up = Array.from({ length: hops }, () => "..").join("/");
|
|
978
|
+
const carried = (name, value) => value === void 0 || value.length === 0 ? "" : ` ${name}: ${asSource(value, 1)},
|
|
979
|
+
`;
|
|
980
|
+
return `import path from 'node:path'
|
|
981
|
+
|
|
982
|
+
import { nestService } from '${BUILD_PRESET_SPECIFIERS.nest}'
|
|
983
|
+
|
|
984
|
+
export default nestService({
|
|
985
|
+
project: '${target.project}',
|
|
986
|
+
root: __dirname,
|
|
987
|
+
workspaceRoot: path.resolve(__dirname, '${up}'),
|
|
988
|
+
outDir: '${target.outputPath}',
|
|
989
|
+
${carried("transformers", target.transformers)}${carried("assets", target.assets)}})
|
|
990
|
+
`;
|
|
991
|
+
}
|
|
992
|
+
function nestBuildOutputs(target) {
|
|
993
|
+
return [`{workspaceRoot}/${target.outputPath}`];
|
|
994
|
+
}
|
|
995
|
+
var OUT_DIR_IN_CONFIG = /^\s*outDir:\s*'([^']+)'/m;
|
|
996
|
+
function nestOutputsFromConfig(cwd) {
|
|
997
|
+
const configPath = join10(cwd, NEST_CONFIG_FILE);
|
|
998
|
+
if (!existsSync10(configPath)) return void 0;
|
|
999
|
+
const outDir = OUT_DIR_IN_CONFIG.exec(readFileSync8(configPath, "utf8"))?.[1];
|
|
1000
|
+
return outDir === void 0 ? void 0 : [`{workspaceRoot}/${outDir}`];
|
|
1001
|
+
}
|
|
1002
|
+
function nestAdoptionOperations(cwd, target) {
|
|
1003
|
+
return [{ kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) }];
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/roles/build/requirements.ts
|
|
1007
|
+
function atLeast(declared, needed) {
|
|
1008
|
+
const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
1009
|
+
const left = parse2(declared);
|
|
1010
|
+
const right = parse2(needed);
|
|
1011
|
+
for (let at = 0; at < Math.max(left.length, right.length); at++) {
|
|
1012
|
+
const a = left[at] ?? 0;
|
|
1013
|
+
const b = right[at] ?? 0;
|
|
1014
|
+
if (a !== b) return a > b;
|
|
582
1015
|
}
|
|
583
1016
|
return true;
|
|
584
1017
|
}
|
|
@@ -598,32 +1031,26 @@ function describeUnmet(preset, unmet) {
|
|
|
598
1031
|
return `the build role does not apply to this module yet: ${list}. ${entry?.why ?? ""}. Nothing was written. Raise those versions, check the module still builds and its tests still pass, then re-run \`sentinel --init --build\`.`;
|
|
599
1032
|
}
|
|
600
1033
|
|
|
601
|
-
// src/
|
|
1034
|
+
// src/core/config/rewrite-imports.ts
|
|
602
1035
|
import { parseSync } from "oxc-parser";
|
|
603
|
-
var
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (packageType === "module") return void 0;
|
|
607
|
-
return `sentinel is ESM only, and this module does not declare \`"type": "module"\`, so Vite would load ${fileName} as CommonJS and fail to require it. Add \`"type": "module"\` to this module's package.json, or rename the config to \`${fileName.replace(/\.(ts|js)$/, ".m$1")}\`, then run this again.`;
|
|
608
|
-
}
|
|
609
|
-
var DEFAULT_EXPORT_NAMES = Object.fromEntries(
|
|
610
|
-
OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
|
|
611
|
-
entry.specifier,
|
|
612
|
-
entry.default
|
|
613
|
-
])
|
|
1036
|
+
var movedSpecifiers = (toolchain) => new Set(toolchain.owned.map((entry) => entry.specifier));
|
|
1037
|
+
var defaultExportNames = (toolchain) => Object.fromEntries(
|
|
1038
|
+
toolchain.owned.filter((entry) => entry.default !== void 0).map((entry) => [entry.specifier, entry.default])
|
|
614
1039
|
);
|
|
615
|
-
function renderImport(bindings) {
|
|
1040
|
+
function renderImport(bindings, presetSpecifier) {
|
|
616
1041
|
const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
|
|
617
1042
|
const values = bindings.filter((b) => !b.typeOnly);
|
|
618
1043
|
const types = bindings.filter((b) => b.typeOnly);
|
|
619
1044
|
const lines = [];
|
|
620
|
-
if (values.length > 0) lines.push(`import { ${render(values)} } from '${
|
|
1045
|
+
if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'`);
|
|
621
1046
|
if (types.length > 0) {
|
|
622
|
-
lines.push(`import type { ${render(types)} } from '${
|
|
1047
|
+
lines.push(`import type { ${render(types)} } from '${presetSpecifier}'`);
|
|
623
1048
|
}
|
|
624
1049
|
return lines.join("\n");
|
|
625
1050
|
}
|
|
626
|
-
function
|
|
1051
|
+
function rewriteToolchainImports(toolchain, source, fileName) {
|
|
1052
|
+
const moved = movedSpecifiers(toolchain);
|
|
1053
|
+
const defaults = defaultExportNames(toolchain);
|
|
627
1054
|
const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
|
|
628
1055
|
if (errors.length > 0) {
|
|
629
1056
|
return {
|
|
@@ -634,10 +1061,10 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
634
1061
|
const imports = program.body.filter(
|
|
635
1062
|
(node) => node.type === "ImportDeclaration"
|
|
636
1063
|
);
|
|
637
|
-
if (imports.some((node) => node.source.value ===
|
|
1064
|
+
if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
|
|
638
1065
|
return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
|
|
639
1066
|
}
|
|
640
|
-
const moving = imports.filter((node) =>
|
|
1067
|
+
const moving = imports.filter((node) => moved.has(node.source.value));
|
|
641
1068
|
if (moving.length === 0) {
|
|
642
1069
|
return {
|
|
643
1070
|
kind: "unchanged",
|
|
@@ -654,7 +1081,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
654
1081
|
for (const spec of specifiers) {
|
|
655
1082
|
const typeOnly = node.importKind === "type" || spec.importKind === "type";
|
|
656
1083
|
if (spec.type === "ImportDefaultSpecifier") {
|
|
657
|
-
const exported =
|
|
1084
|
+
const exported = defaults[from];
|
|
658
1085
|
if (!exported) {
|
|
659
1086
|
return {
|
|
660
1087
|
kind: "blocked",
|
|
@@ -686,7 +1113,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
686
1113
|
const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
|
|
687
1114
|
out = out.slice(0, span.start) + out.slice(span.end + after);
|
|
688
1115
|
}
|
|
689
|
-
out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
|
|
1116
|
+
out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier) + out.slice(first.end);
|
|
690
1117
|
return {
|
|
691
1118
|
kind: "rewritten",
|
|
692
1119
|
source: out,
|
|
@@ -694,7 +1121,19 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
694
1121
|
};
|
|
695
1122
|
}
|
|
696
1123
|
|
|
1124
|
+
// src/roles/build/rewrite-imports.ts
|
|
1125
|
+
var BUILD_TOOLCHAIN = {
|
|
1126
|
+
presetSpecifier: BUILD_PRESET_SPECIFIER,
|
|
1127
|
+
owned: OWNED_PACKAGES
|
|
1128
|
+
};
|
|
1129
|
+
function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
1130
|
+
return rewriteToolchainImports(BUILD_TOOLCHAIN, source, fileName);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
697
1133
|
// src/roles/build/plan.ts
|
|
1134
|
+
function candidateOutputs() {
|
|
1135
|
+
return BUILD_OUTPUT_CANDIDATES.map((name) => `{projectRoot}/${name}`);
|
|
1136
|
+
}
|
|
698
1137
|
function ownedBuildDependencies(cwd) {
|
|
699
1138
|
const manifest = readProjectPackageJson(cwd);
|
|
700
1139
|
const keys = [];
|
|
@@ -726,6 +1165,8 @@ function buildScripts(cwd) {
|
|
|
726
1165
|
const own2 = moduleName(cwd);
|
|
727
1166
|
scripts[DEV_SCRIPT_NAME] = composeDevScript(
|
|
728
1167
|
existingDev,
|
|
1168
|
+
// No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
|
|
1169
|
+
// sides produced `-- --`, handing Vite an argument nobody wrote.
|
|
729
1170
|
`${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
|
|
730
1171
|
);
|
|
731
1172
|
}
|
|
@@ -740,54 +1181,73 @@ function plan(context) {
|
|
|
740
1181
|
if (unmet.length > 0) {
|
|
741
1182
|
return { operations: [], skipped: describeUnmet(context.preset, unmet) };
|
|
742
1183
|
}
|
|
743
|
-
|
|
1184
|
+
const declaredPreset2 = declaredBuildPreset(context.cwd);
|
|
1185
|
+
const configFile = buildConfigFile(context.cwd);
|
|
1186
|
+
if (declaredPreset2 === void 0 && configFile !== void 0) {
|
|
1187
|
+
return {
|
|
1188
|
+
operations: [],
|
|
1189
|
+
skipped: `this module has a Vite config, but it builds neither a React app (no @vitejs/plugin-react or @tanstack/react-start) nor a Nest service (no ${BUILD_PRESET_SPECIFIERS.nest}). Those are the two families this role serves.`
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const webpackTarget = webpackBuildTarget(context.cwd);
|
|
1193
|
+
if (declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0) {
|
|
1194
|
+
return planNestService(context, webpackTarget);
|
|
1195
|
+
}
|
|
1196
|
+
if (configFile === void 0) {
|
|
744
1197
|
return {
|
|
745
1198
|
operations: [],
|
|
746
|
-
skipped: `this module
|
|
1199
|
+
skipped: `no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (38 services here build through \`@nx/webpack\`), the build role does not apply to it.`
|
|
747
1200
|
};
|
|
748
1201
|
}
|
|
1202
|
+
return planReactApp(context, configFile);
|
|
1203
|
+
}
|
|
1204
|
+
function planReactApp(context, configFile) {
|
|
749
1205
|
const notes = [];
|
|
750
1206
|
const operations = [];
|
|
751
|
-
const
|
|
752
|
-
if (
|
|
1207
|
+
const esm = esmReadiness(configFile, readProjectPackageJson(context.cwd).type);
|
|
1208
|
+
if (esm.kind === "blocked") return { operations: [], skipped: esm.why };
|
|
1209
|
+
const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
|
|
1210
|
+
if (rewrite.kind === "blocked") {
|
|
753
1211
|
return {
|
|
754
1212
|
operations: [],
|
|
755
|
-
skipped:
|
|
1213
|
+
skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
|
|
756
1214
|
};
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
for (const name of COMPANION_CONFIG_FILES) {
|
|
771
|
-
const source = readConfigSource(context.cwd, name);
|
|
772
|
-
if (source === "") continue;
|
|
773
|
-
const companion = rewriteBuildImports(source, name);
|
|
774
|
-
if (companion.kind === "rewritten") {
|
|
775
|
-
operations.push({ kind: "write", path: name, contents: companion.source });
|
|
776
|
-
notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
|
|
777
|
-
} else if (companion.kind === "blocked") {
|
|
778
|
-
notes.push(
|
|
779
|
-
`${name} imports the toolchain and could NOT be pointed at sentinel: ${companion.why}. It will stop resolving when the workspace root drops these packages, and it will fail your TESTS rather than your build.`
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
if (rewrite.kind === "rewritten") {
|
|
1215
|
+
}
|
|
1216
|
+
if (rewrite.kind === "rewritten") {
|
|
1217
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
1218
|
+
}
|
|
1219
|
+
for (const name of COMPANION_CONFIG_FILES) {
|
|
1220
|
+
const source = readConfigSource(context.cwd, name);
|
|
1221
|
+
if (source === "") continue;
|
|
1222
|
+
const companion = rewriteBuildImports(source, name);
|
|
1223
|
+
if (companion.kind === "rewritten") {
|
|
1224
|
+
operations.push({ kind: "write", path: name, contents: companion.source });
|
|
1225
|
+
notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
|
|
1226
|
+
} else if (companion.kind === "blocked") {
|
|
784
1227
|
notes.push(
|
|
785
|
-
`${
|
|
1228
|
+
`${name} imports the toolchain and could NOT be pointed at sentinel: ${companion.why}. It will stop resolving when the workspace root drops these packages, and it will fail your TESTS rather than your build.`
|
|
786
1229
|
);
|
|
787
1230
|
}
|
|
788
1231
|
}
|
|
789
|
-
|
|
790
|
-
|
|
1232
|
+
if (rewrite.kind === "rewritten") {
|
|
1233
|
+
notes.push(
|
|
1234
|
+
`${configFile} now imports ${rewrite.moved.join(", ")} from \`${BUILD_PRESET_SPECIFIER}\`. Only the import lines changed: every attribute, alias and path is untouched, so this build produces what it produced before.`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const scripts = buildScripts(context.cwd);
|
|
1238
|
+
operations.push(manifestOperation(context.cwd, scripts));
|
|
1239
|
+
operations.push(
|
|
1240
|
+
...nxTargetOperations({
|
|
1241
|
+
cwd: context.cwd,
|
|
1242
|
+
targets: buildTarget({
|
|
1243
|
+
moduleName: adoptedPackageName(context.cwd),
|
|
1244
|
+
outputs: candidateOutputs()
|
|
1245
|
+
})
|
|
1246
|
+
})
|
|
1247
|
+
);
|
|
1248
|
+
const dev = devTargetOperations(context.cwd, scripts[DEV_SCRIPT_NAME]);
|
|
1249
|
+
operations.push(...dev.operations);
|
|
1250
|
+
if (dev.note !== void 0) notes.push(dev.note);
|
|
791
1251
|
const owned = ownedBuildDependencies(context.cwd);
|
|
792
1252
|
if (owned.length > 0) {
|
|
793
1253
|
operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
|
|
@@ -795,12 +1255,53 @@ function plan(context) {
|
|
|
795
1255
|
`removed ${owned.map(([, name]) => name).join(", ")} from this module: sentinel owns them now, and two copies of Vite in one build give the plugins a different Vite than the one running them.`
|
|
796
1256
|
);
|
|
797
1257
|
}
|
|
1258
|
+
const formatted = readFormatAdoption(context.cwd).adopted;
|
|
1259
|
+
notes.push(
|
|
1260
|
+
formatted ? `run \`sentinel --run --format --fix\` on this module: adoption rewrote ${configFile}, and a rewritten import can be longer than your formatter wraps at.` : `this module is formatted by the workspace, not by sentinel, so format ${configFile} with whatever owns it before committing: a rewritten import can be longer than that formatter wraps at, and the check would fail on a file nobody typed.`
|
|
1261
|
+
);
|
|
1262
|
+
notes.push(
|
|
1263
|
+
`this config is code, and it runs before the build does: any environment variable it reads has to be present wherever you build, including a fresh clone, another CI job or an image. Adoption does not change what it reads, but a failure there arrives early and names this role rather than the variable.`
|
|
1264
|
+
);
|
|
798
1265
|
return { operations, notes };
|
|
799
1266
|
}
|
|
1267
|
+
function planNestService(context, webpackTarget) {
|
|
1268
|
+
const outputs = webpackTarget === void 0 ? nestOutputsFromConfig(context.cwd) : nestBuildOutputs(webpackTarget);
|
|
1269
|
+
if (outputs === void 0) {
|
|
1270
|
+
return {
|
|
1271
|
+
operations: [],
|
|
1272
|
+
blocked: `${NEST_CONFIG_FILE} does not declare an \`outDir\`, so sentinel cannot say where this service's build lands. Declaring an nx target without it would cache the build and restore nothing from that cache. Nothing was written.`
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const metadata = [
|
|
1276
|
+
manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND }),
|
|
1277
|
+
...nxTargetOperations({
|
|
1278
|
+
cwd: context.cwd,
|
|
1279
|
+
targets: buildTarget({
|
|
1280
|
+
moduleName: adoptedPackageName(context.cwd),
|
|
1281
|
+
configFiles: [NEST_CONFIG_FILE],
|
|
1282
|
+
outputs
|
|
1283
|
+
})
|
|
1284
|
+
})
|
|
1285
|
+
];
|
|
1286
|
+
if (webpackTarget === void 0) return { operations: metadata };
|
|
1287
|
+
const refusal = refusalFor(context.cwd, webpackTarget);
|
|
1288
|
+
if (refusal !== void 0) return { operations: [], blocked: refusal };
|
|
1289
|
+
const dropped = Object.keys(webpackTarget.configurations ?? {});
|
|
1290
|
+
return {
|
|
1291
|
+
operations: [...nestAdoptionOperations(context.cwd, webpackTarget), ...metadata],
|
|
1292
|
+
notes: [
|
|
1293
|
+
`wrote ${NEST_CONFIG_FILE} and pointed the nx build target at sentinel. The target stays \`nx:run-commands\` with \`forwardAllArgs: false\`, so the image's \`--generatePackageJson\` still works and the Dockerfile needs no change.`,
|
|
1294
|
+
...dropped.length > 0 ? [
|
|
1295
|
+
`dropped the \`${dropped.join("`, `")}\` configuration${dropped.length > 1 ? "s" : ""} from the build target: its fileReplacements point at files that do not exist, so it replaced nothing. A live one would have blocked this instead.`
|
|
1296
|
+
] : [],
|
|
1297
|
+
`verify with \`nx run ${webpackTarget.project}:generate-swagger-file\` and \`git diff --exit-code\`: it runs the built bundle, so it proves the service boots AND that its published contract did not move.`
|
|
1298
|
+
]
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
800
1301
|
function readConfigSource(cwd, configFile) {
|
|
801
|
-
if (!
|
|
1302
|
+
if (!existsSync11(join11(cwd, configFile))) return "";
|
|
802
1303
|
try {
|
|
803
|
-
return
|
|
1304
|
+
return readFileSync9(join11(cwd, configFile), "utf8");
|
|
804
1305
|
} catch {
|
|
805
1306
|
return "";
|
|
806
1307
|
}
|
|
@@ -840,11 +1341,17 @@ var ViteRoleAdapter = class extends BaseAdapter {
|
|
|
840
1341
|
* resolution let it run at all — filtering here would give "no adapter for preset svelte",
|
|
841
1342
|
* which tells nobody what to do about it.
|
|
842
1343
|
*
|
|
843
|
-
* Nest
|
|
844
|
-
*
|
|
1344
|
+
* Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
|
|
1345
|
+
* grounds that "there is nothing to bundle", which was never true: those 38 services bundle
|
|
1346
|
+
* with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
|
|
1347
|
+
* the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
|
|
1348
|
+
* migration generator refuses every one of them because they use `@nx/js:node`
|
|
1349
|
+
* (nrwl/nx#36389). So the way out runs through here.
|
|
1350
|
+
*
|
|
1351
|
+
* `node` and `tools` remain genuinely declined: nothing under them bundles at all.
|
|
845
1352
|
*/
|
|
846
1353
|
appliesTo(preset) {
|
|
847
|
-
return preset === "react" || preset === "svelte";
|
|
1354
|
+
return preset === "react" || preset === "svelte" || preset === "nest";
|
|
848
1355
|
}
|
|
849
1356
|
/**
|
|
850
1357
|
* `react`, read from the module's Vite config rather than from its dependencies.
|
|
@@ -935,40 +1442,252 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
|
|
|
935
1442
|
return "dev";
|
|
936
1443
|
}
|
|
937
1444
|
/**
|
|
938
|
-
*
|
|
1445
|
+
* `--init --dev` does the same thing as `--init --build`, and now says so.
|
|
1446
|
+
*
|
|
1447
|
+
* The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
|
|
1448
|
+
* because adopting one without the other leaves the module half-migrated. What was missing
|
|
1449
|
+
* was only the telling: the run printed a message about the build and nothing about `serve`,
|
|
1450
|
+
* so a developer could believe the dev server had been adopted on its own. Refusing was the
|
|
1451
|
+
* wrong correction, it added friction to a command that already produced the right result.
|
|
1452
|
+
*/
|
|
1453
|
+
plan(context) {
|
|
1454
|
+
if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
|
|
1455
|
+
return {
|
|
1456
|
+
operations: [],
|
|
1457
|
+
skipped: `a Nest service has no dev server, so --dev has nothing to adopt here. Its build is \`sentinel --init --build\`, and it runs as a Node process afterwards.`
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
const planned = super.plan(context);
|
|
1461
|
+
if (planned.operations.length === 0) return planned;
|
|
1462
|
+
return {
|
|
1463
|
+
...planned,
|
|
1464
|
+
notes: [
|
|
1465
|
+
...planned.notes ?? [],
|
|
1466
|
+
"--init --dev adopts the build too: one plan writes both `build` and `serve`."
|
|
1467
|
+
]
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Start the dev server. It does not return until stopped, so there is no verdict to report
|
|
1472
|
+
* beyond the exit code the developer's own Ctrl-C produces.
|
|
939
1473
|
*
|
|
940
|
-
* `
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
* is the worst of the options: it teaches that dev can be adopted by itself, and that belief
|
|
944
|
-
* is what produces the half-migrated module this role exists to prevent (see the class
|
|
945
|
-
* comment above, and `registerBuild`).
|
|
1474
|
+
* No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
|
|
1475
|
+
* is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
|
|
1476
|
+
* running it again would race the watcher that is about to own the file.
|
|
946
1477
|
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
1478
|
+
* No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
|
|
1479
|
+
* bare form is the one every version accepts.
|
|
949
1480
|
*/
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1481
|
+
async run(ctx) {
|
|
1482
|
+
const ready = this.prepare(ctx);
|
|
1483
|
+
if (ready.kind === "stop") return ready.result;
|
|
1484
|
+
return this.spawnVite(ctx, ready.vite, [], 0);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
// src/roles/build/declared-outputs.ts
|
|
1489
|
+
import { existsSync as existsSync13, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1490
|
+
import { join as join13 } from "path";
|
|
1491
|
+
|
|
1492
|
+
// src/core/workspace-prep.ts
|
|
1493
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync } from "fs";
|
|
1494
|
+
import { dirname as dirname3, join as join12, relative as relative2 } from "path";
|
|
1495
|
+
|
|
1496
|
+
// src/core/settings.ts
|
|
1497
|
+
var WORKSPACE_ROOT_MARKER = "nx.json";
|
|
1498
|
+
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
1499
|
+
|
|
1500
|
+
// src/core/workspace-prep.ts
|
|
1501
|
+
var OVERRIDE_KEY = "i18next>typescript";
|
|
1502
|
+
var NATIVE_TS_ALIAS = "@typescript/native";
|
|
1503
|
+
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
1504
|
+
var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
|
|
1505
|
+
var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
|
|
1506
|
+
function findWorkspaceRoot(startDir) {
|
|
1507
|
+
let dir = startDir;
|
|
1508
|
+
for (; ; ) {
|
|
1509
|
+
if (existsSync12(join12(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
1510
|
+
const parent = dirname3(dir);
|
|
1511
|
+
if (parent === dir) return void 0;
|
|
1512
|
+
dir = parent;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function declaredNativeTs(pkg) {
|
|
1516
|
+
const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
|
|
1517
|
+
if (!spec) return void 0;
|
|
1518
|
+
const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
|
|
1519
|
+
return version || void 0;
|
|
1520
|
+
}
|
|
1521
|
+
function ensureI18nextSingleton(root, dryRun) {
|
|
1522
|
+
const pkgPath = join12(root, "package.json");
|
|
1523
|
+
if (!existsSync12(pkgPath)) return void 0;
|
|
1524
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
1525
|
+
const want = declaredNativeTs(pkg);
|
|
1526
|
+
if (!want) return void 0;
|
|
1527
|
+
const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
1528
|
+
if (have === want) return void 0;
|
|
1529
|
+
if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
|
|
1530
|
+
pkg.pnpm ??= {};
|
|
1531
|
+
pkg.pnpm.overrides ??= {};
|
|
1532
|
+
pkg.pnpm.overrides[OVERRIDE_KEY] = want;
|
|
1533
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1534
|
+
return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
|
|
1535
|
+
}
|
|
1536
|
+
function ensureReleaseAgeAllowList(root, dryRun) {
|
|
1537
|
+
const yamlPath = join12(root, WORKSPACE_YAML);
|
|
1538
|
+
if (!existsSync12(yamlPath)) return void 0;
|
|
1539
|
+
const own = readOwnPackage().name;
|
|
1540
|
+
const lines = readFileSync10(yamlPath, "utf8").split("\n");
|
|
1541
|
+
const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
|
|
1542
|
+
if (keyIdx === -1) return void 0;
|
|
1543
|
+
let lastItemIdx = keyIdx;
|
|
1544
|
+
let indent = " ";
|
|
1545
|
+
for (let i = keyIdx + 1; i < lines.length; i++) {
|
|
1546
|
+
const match = lines[i]?.match(LIST_ITEM);
|
|
1547
|
+
if (!match) break;
|
|
1548
|
+
indent = match[1] ?? indent;
|
|
1549
|
+
lastItemIdx = i;
|
|
1550
|
+
if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
|
|
1551
|
+
}
|
|
1552
|
+
if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
|
|
1553
|
+
lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
|
|
1554
|
+
writeFileSync(yamlPath, lines.join("\n"));
|
|
1555
|
+
return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
|
|
1556
|
+
}
|
|
1557
|
+
var PRETTIER_IGNORE = ".prettierignore";
|
|
1558
|
+
var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
|
|
1559
|
+
var ROOT_PRETTIER_CONFIGS = [
|
|
1560
|
+
".prettierrc",
|
|
1561
|
+
".prettierrc.json",
|
|
1562
|
+
".prettierrc.json5",
|
|
1563
|
+
".prettierrc.yml",
|
|
1564
|
+
".prettierrc.yaml",
|
|
1565
|
+
".prettierrc.js",
|
|
1566
|
+
".prettierrc.cjs",
|
|
1567
|
+
".prettierrc.mjs",
|
|
1568
|
+
"prettier.config.js",
|
|
1569
|
+
"prettier.config.cjs",
|
|
1570
|
+
"prettier.config.mjs"
|
|
1571
|
+
];
|
|
1572
|
+
function ensureFormatterExclusion(root, moduleDir, dryRun) {
|
|
1573
|
+
const rel = relative2(root, moduleDir).replaceAll("\\", "/");
|
|
1574
|
+
if (rel === "" || rel.startsWith("..")) return void 0;
|
|
1575
|
+
const ignorePath = join12(root, PRETTIER_IGNORE);
|
|
1576
|
+
const hasPrettier = existsSync12(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync12(join12(root, name)));
|
|
1577
|
+
if (!hasPrettier) return void 0;
|
|
1578
|
+
const pattern = `/${rel}/`;
|
|
1579
|
+
const existing = existsSync12(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
|
|
1580
|
+
const lines = existing.split("\n");
|
|
1581
|
+
if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
|
|
1582
|
+
return void 0;
|
|
1583
|
+
}
|
|
1584
|
+
if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
|
|
1585
|
+
const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
|
|
1586
|
+
if (headerIdx === -1) {
|
|
1587
|
+
const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
|
|
1588
|
+
|
|
1589
|
+
`;
|
|
1590
|
+
writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
|
|
1591
|
+
${pattern}
|
|
1592
|
+
`);
|
|
1593
|
+
} else {
|
|
1594
|
+
let end = headerIdx + 1;
|
|
1595
|
+
while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
|
|
1596
|
+
const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
|
|
1597
|
+
writeFileSync(
|
|
1598
|
+
ignorePath,
|
|
1599
|
+
[...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
|
|
953
1600
|
);
|
|
954
1601
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1602
|
+
return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
|
|
1603
|
+
}
|
|
1604
|
+
function ensureWorkspacePrep(opts) {
|
|
1605
|
+
const dryRun = Boolean(opts.dryRun);
|
|
1606
|
+
return [
|
|
1607
|
+
ensureI18nextSingleton(opts.root, dryRun),
|
|
1608
|
+
ensureReleaseAgeAllowList(opts.root, dryRun),
|
|
1609
|
+
opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
|
|
1610
|
+
].filter((message) => message !== void 0);
|
|
1611
|
+
}
|
|
1612
|
+
function inspectWorkspacePrep(root) {
|
|
1613
|
+
const entries = [];
|
|
1614
|
+
let pkg = {};
|
|
1615
|
+
const pkgPath = join12(root, "package.json");
|
|
1616
|
+
if (existsSync12(pkgPath)) {
|
|
1617
|
+
try {
|
|
1618
|
+
pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
1619
|
+
} catch {
|
|
1620
|
+
pkg = {};
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
1624
|
+
if (pinned !== void 0) {
|
|
1625
|
+
const stillForks = declaredNativeTs(pkg);
|
|
1626
|
+
entries.push({
|
|
1627
|
+
rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
|
|
1628
|
+
reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
const own = readOwnPackage().name;
|
|
1632
|
+
const yamlPath = join12(root, WORKSPACE_YAML);
|
|
1633
|
+
if (existsSync12(yamlPath)) {
|
|
1634
|
+
const yaml = readFileSync10(yamlPath, "utf8");
|
|
1635
|
+
const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
|
|
1636
|
+
if (listed) {
|
|
1637
|
+
const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
|
|
1638
|
+
entries.push({
|
|
1639
|
+
rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
|
|
1640
|
+
reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return entries;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/roles/build/declared-outputs.ts
|
|
1648
|
+
function declaredBuildTarget(cwd) {
|
|
1649
|
+
return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
|
|
1650
|
+
}
|
|
1651
|
+
function resolveOutputToken(token, cwd) {
|
|
1652
|
+
if (token.startsWith("{projectRoot}/")) return join13(cwd, token.slice("{projectRoot}/".length));
|
|
1653
|
+
if (token.startsWith("{workspaceRoot}/")) {
|
|
1654
|
+
const root = findWorkspaceRoot(cwd);
|
|
1655
|
+
return root === void 0 ? void 0 : join13(root, token.slice("{workspaceRoot}/".length));
|
|
1656
|
+
}
|
|
1657
|
+
return join13(cwd, token);
|
|
1658
|
+
}
|
|
1659
|
+
function buildStamp(cwd) {
|
|
1660
|
+
const marker = join13(cwd, `.sentinel-build-stamp-${process.pid}`);
|
|
1661
|
+
try {
|
|
1662
|
+
writeFileSync2(marker, "");
|
|
1663
|
+
const stamp = statSync(marker).mtimeMs;
|
|
1664
|
+
rmSync(marker, { force: true });
|
|
1665
|
+
return stamp;
|
|
1666
|
+
} catch {
|
|
1667
|
+
return Date.now();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function writtenSince(path, startedAt) {
|
|
1671
|
+
if (!existsSync13(path)) return false;
|
|
1672
|
+
try {
|
|
1673
|
+
return statSync(path).mtimeMs >= startedAt;
|
|
1674
|
+
} catch {
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function outputsProblem(cwd, startedAt) {
|
|
1679
|
+
const target = declaredBuildTarget(cwd);
|
|
1680
|
+
if (target === void 0) return void 0;
|
|
1681
|
+
const declared = target.outputs ?? [];
|
|
1682
|
+
if (declared.length === 0) {
|
|
1683
|
+
if (target.cache !== true) return void 0;
|
|
1684
|
+
return `this module's nx build target sets \`cache: true\` and declares no \`outputs\`, so nx will cache this build and restore nothing from it: the next cache hit reports success and leaves no build on disk. Add the directory this build writes to under \`nx.targets.${BUILD_SCRIPT_NAME}.outputs\` in package.json, or re-run \`sentinel --init --build\`, which declares it.`;
|
|
970
1685
|
}
|
|
971
|
-
};
|
|
1686
|
+
const resolved = declared.map((token) => ({ token, path: resolveOutputToken(token, cwd) })).filter((entry) => entry.path !== void 0);
|
|
1687
|
+
if (resolved.length === 0) return void 0;
|
|
1688
|
+
if (resolved.some((entry) => writtenSince(entry.path, startedAt))) return void 0;
|
|
1689
|
+
return `the build wrote nothing to any of the directories this module declares as its nx \`outputs\` (${declared.join(", ")}), so nx would cache this build and restore an empty result from it. Whatever directory the build DID write to belongs in \`nx.targets.${BUILD_SCRIPT_NAME}.outputs\` in package.json. The build itself succeeded; only its cache declaration is wrong.`;
|
|
1690
|
+
}
|
|
972
1691
|
|
|
973
1692
|
// src/roles/build/prerequisite.ts
|
|
974
1693
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1018,7 +1737,15 @@ var ViteAdapter = class extends ViteRoleAdapter {
|
|
|
1018
1737
|
);
|
|
1019
1738
|
return { ok: false, code: prerequisite.code };
|
|
1020
1739
|
}
|
|
1021
|
-
|
|
1740
|
+
const startedAt = buildStamp(ctx.cwd);
|
|
1741
|
+
const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
|
|
1742
|
+
if (!result.ok) return result;
|
|
1743
|
+
const problem = outputsProblem(ctx.cwd, startedAt);
|
|
1744
|
+
if (problem !== void 0) {
|
|
1745
|
+
this.say(problem);
|
|
1746
|
+
return { ok: false, code: 1 };
|
|
1747
|
+
}
|
|
1748
|
+
return result;
|
|
1022
1749
|
}
|
|
1023
1750
|
/**
|
|
1024
1751
|
* What this module builds with, WITHOUT loading its config.
|
|
@@ -1090,12 +1817,12 @@ function registerBuild() {
|
|
|
1090
1817
|
|
|
1091
1818
|
// src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
|
|
1092
1819
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1093
|
-
import { existsSync as
|
|
1094
|
-
import { join as
|
|
1820
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
1821
|
+
import { join as join18 } from "path";
|
|
1095
1822
|
|
|
1096
1823
|
// src/core/config/has-source.ts
|
|
1097
1824
|
import { readdirSync } from "fs";
|
|
1098
|
-
import { extname, join as
|
|
1825
|
+
import { extname, join as join14, relative as relative3 } from "path";
|
|
1099
1826
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1100
1827
|
"node_modules",
|
|
1101
1828
|
"dist",
|
|
@@ -1146,7 +1873,7 @@ function hasSourceFiles(cwd, extensions) {
|
|
|
1146
1873
|
}
|
|
1147
1874
|
for (const entry of entries) {
|
|
1148
1875
|
if (entry.isDirectory()) {
|
|
1149
|
-
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(
|
|
1876
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join14(dir, entry.name));
|
|
1150
1877
|
continue;
|
|
1151
1878
|
}
|
|
1152
1879
|
if (wanted.has(extname(entry.name))) return true;
|
|
@@ -1167,12 +1894,12 @@ function listSourceFiles(cwd, extensions) {
|
|
|
1167
1894
|
continue;
|
|
1168
1895
|
}
|
|
1169
1896
|
for (const entry of entries) {
|
|
1170
|
-
const full =
|
|
1897
|
+
const full = join14(dir, entry.name);
|
|
1171
1898
|
if (entry.isDirectory()) {
|
|
1172
1899
|
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
|
|
1173
1900
|
continue;
|
|
1174
1901
|
}
|
|
1175
|
-
if (wanted.has(extname(entry.name))) found.push(
|
|
1902
|
+
if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
|
|
1176
1903
|
}
|
|
1177
1904
|
}
|
|
1178
1905
|
return found;
|
|
@@ -1180,207 +1907,9 @@ function listSourceFiles(cwd, extensions) {
|
|
|
1180
1907
|
|
|
1181
1908
|
// src/core/named-scope.ts
|
|
1182
1909
|
var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
|
|
1183
|
-
function scopeFlags(paths) {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
// src/core/settings.ts
|
|
1188
|
-
var WORKSPACE_ROOT_MARKER = "nx.json";
|
|
1189
|
-
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
1190
|
-
|
|
1191
|
-
// src/core/workspace-prep.ts
|
|
1192
|
-
import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
|
|
1193
|
-
import { dirname as dirname3, join as join9, relative as relative2 } from "path";
|
|
1194
|
-
var OVERRIDE_KEY = "i18next>typescript";
|
|
1195
|
-
var NATIVE_TS_ALIAS = "@typescript/native";
|
|
1196
|
-
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
1197
|
-
var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
|
|
1198
|
-
var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
|
|
1199
|
-
function findWorkspaceRoot(startDir) {
|
|
1200
|
-
let dir = startDir;
|
|
1201
|
-
for (; ; ) {
|
|
1202
|
-
if (existsSync8(join9(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
1203
|
-
const parent = dirname3(dir);
|
|
1204
|
-
if (parent === dir) return void 0;
|
|
1205
|
-
dir = parent;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
function declaredNativeTs(pkg) {
|
|
1209
|
-
const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
|
|
1210
|
-
if (!spec) return void 0;
|
|
1211
|
-
const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
|
|
1212
|
-
return version || void 0;
|
|
1213
|
-
}
|
|
1214
|
-
function ensureI18nextSingleton(root, dryRun) {
|
|
1215
|
-
const pkgPath = join9(root, "package.json");
|
|
1216
|
-
if (!existsSync8(pkgPath)) return void 0;
|
|
1217
|
-
const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
|
|
1218
|
-
const want = declaredNativeTs(pkg);
|
|
1219
|
-
if (!want) return void 0;
|
|
1220
|
-
const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
1221
|
-
if (have === want) return void 0;
|
|
1222
|
-
if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
|
|
1223
|
-
pkg.pnpm ??= {};
|
|
1224
|
-
pkg.pnpm.overrides ??= {};
|
|
1225
|
-
pkg.pnpm.overrides[OVERRIDE_KEY] = want;
|
|
1226
|
-
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1227
|
-
return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
|
|
1228
|
-
}
|
|
1229
|
-
function ensureReleaseAgeAllowList(root, dryRun) {
|
|
1230
|
-
const yamlPath = join9(root, WORKSPACE_YAML);
|
|
1231
|
-
if (!existsSync8(yamlPath)) return void 0;
|
|
1232
|
-
const own = readOwnPackage().name;
|
|
1233
|
-
const lines = readFileSync6(yamlPath, "utf8").split("\n");
|
|
1234
|
-
const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
|
|
1235
|
-
if (keyIdx === -1) return void 0;
|
|
1236
|
-
let lastItemIdx = keyIdx;
|
|
1237
|
-
let indent = " ";
|
|
1238
|
-
for (let i = keyIdx + 1; i < lines.length; i++) {
|
|
1239
|
-
const match = lines[i]?.match(LIST_ITEM);
|
|
1240
|
-
if (!match) break;
|
|
1241
|
-
indent = match[1] ?? indent;
|
|
1242
|
-
lastItemIdx = i;
|
|
1243
|
-
if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
|
|
1244
|
-
}
|
|
1245
|
-
if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
|
|
1246
|
-
lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
|
|
1247
|
-
writeFileSync(yamlPath, lines.join("\n"));
|
|
1248
|
-
return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
|
|
1249
|
-
}
|
|
1250
|
-
var PRETTIER_IGNORE = ".prettierignore";
|
|
1251
|
-
var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
|
|
1252
|
-
var ROOT_PRETTIER_CONFIGS = [
|
|
1253
|
-
".prettierrc",
|
|
1254
|
-
".prettierrc.json",
|
|
1255
|
-
".prettierrc.json5",
|
|
1256
|
-
".prettierrc.yml",
|
|
1257
|
-
".prettierrc.yaml",
|
|
1258
|
-
".prettierrc.js",
|
|
1259
|
-
".prettierrc.cjs",
|
|
1260
|
-
".prettierrc.mjs",
|
|
1261
|
-
"prettier.config.js",
|
|
1262
|
-
"prettier.config.cjs",
|
|
1263
|
-
"prettier.config.mjs"
|
|
1264
|
-
];
|
|
1265
|
-
function ensureFormatterExclusion(root, moduleDir, dryRun) {
|
|
1266
|
-
const rel = relative2(root, moduleDir).replaceAll("\\", "/");
|
|
1267
|
-
if (rel === "" || rel.startsWith("..")) return void 0;
|
|
1268
|
-
const ignorePath = join9(root, PRETTIER_IGNORE);
|
|
1269
|
-
const hasPrettier = existsSync8(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync8(join9(root, name)));
|
|
1270
|
-
if (!hasPrettier) return void 0;
|
|
1271
|
-
const pattern = `/${rel}/`;
|
|
1272
|
-
const existing = existsSync8(ignorePath) ? readFileSync6(ignorePath, "utf8") : "";
|
|
1273
|
-
const lines = existing.split("\n");
|
|
1274
|
-
if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
|
|
1275
|
-
return void 0;
|
|
1276
|
-
}
|
|
1277
|
-
if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
|
|
1278
|
-
const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
|
|
1279
|
-
if (headerIdx === -1) {
|
|
1280
|
-
const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
|
|
1281
|
-
|
|
1282
|
-
`;
|
|
1283
|
-
writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
|
|
1284
|
-
${pattern}
|
|
1285
|
-
`);
|
|
1286
|
-
} else {
|
|
1287
|
-
let end = headerIdx + 1;
|
|
1288
|
-
while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
|
|
1289
|
-
const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
|
|
1290
|
-
writeFileSync(
|
|
1291
|
-
ignorePath,
|
|
1292
|
-
[...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
|
|
1293
|
-
);
|
|
1294
|
-
}
|
|
1295
|
-
return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
|
|
1296
|
-
}
|
|
1297
|
-
function ensureWorkspacePrep(opts) {
|
|
1298
|
-
const dryRun = Boolean(opts.dryRun);
|
|
1299
|
-
return [
|
|
1300
|
-
ensureI18nextSingleton(opts.root, dryRun),
|
|
1301
|
-
ensureReleaseAgeAllowList(opts.root, dryRun),
|
|
1302
|
-
opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
|
|
1303
|
-
].filter((message) => message !== void 0);
|
|
1304
|
-
}
|
|
1305
|
-
function inspectWorkspacePrep(root) {
|
|
1306
|
-
const entries = [];
|
|
1307
|
-
let pkg = {};
|
|
1308
|
-
const pkgPath = join9(root, "package.json");
|
|
1309
|
-
if (existsSync8(pkgPath)) {
|
|
1310
|
-
try {
|
|
1311
|
-
pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
|
|
1312
|
-
} catch {
|
|
1313
|
-
pkg = {};
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
1317
|
-
if (pinned !== void 0) {
|
|
1318
|
-
const stillForks = declaredNativeTs(pkg);
|
|
1319
|
-
entries.push({
|
|
1320
|
-
rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
|
|
1321
|
-
reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
const own = readOwnPackage().name;
|
|
1325
|
-
const yamlPath = join9(root, WORKSPACE_YAML);
|
|
1326
|
-
if (existsSync8(yamlPath)) {
|
|
1327
|
-
const yaml = readFileSync6(yamlPath, "utf8");
|
|
1328
|
-
const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
|
|
1329
|
-
if (listed) {
|
|
1330
|
-
const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
|
|
1331
|
-
entries.push({
|
|
1332
|
-
rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
|
|
1333
|
-
reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
return entries;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
// src/roles/format/config-policy.ts
|
|
1341
|
-
var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
|
|
1342
|
-
var FORMAT_SCRIPT_NAME = "format";
|
|
1343
|
-
var PROVENANCE_KEY = "$sentinel";
|
|
1344
|
-
var PERMITTED_LOCAL_KEYS = [
|
|
1345
|
-
"useTabs",
|
|
1346
|
-
"tabWidth",
|
|
1347
|
-
"printWidth",
|
|
1348
|
-
"semi",
|
|
1349
|
-
"singleQuote",
|
|
1350
|
-
"jsxSingleQuote",
|
|
1351
|
-
"trailingComma",
|
|
1352
|
-
"quoteProps",
|
|
1353
|
-
"bracketSpacing",
|
|
1354
|
-
"bracketSameLine",
|
|
1355
|
-
"arrowParens",
|
|
1356
|
-
"endOfLine",
|
|
1357
|
-
"objectWrap",
|
|
1358
|
-
"proseWrap",
|
|
1359
|
-
"experimentalOperatorPosition"
|
|
1360
|
-
];
|
|
1361
|
-
var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
|
|
1362
|
-
var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
|
|
1363
|
-
var PRETTIER_CONFIG_FILES = [
|
|
1364
|
-
".prettierrc",
|
|
1365
|
-
".prettierrc.json",
|
|
1366
|
-
".prettierrc.json5",
|
|
1367
|
-
".prettierrc.yml",
|
|
1368
|
-
".prettierrc.yaml",
|
|
1369
|
-
".prettierrc.js",
|
|
1370
|
-
".prettierrc.cjs",
|
|
1371
|
-
".prettierrc.mjs",
|
|
1372
|
-
"prettier.config.js",
|
|
1373
|
-
"prettier.config.cjs",
|
|
1374
|
-
"prettier.config.mjs"
|
|
1375
|
-
];
|
|
1376
|
-
function formatTargets() {
|
|
1377
|
-
return {
|
|
1378
|
-
[FORMAT_SCRIPT_NAME]: {
|
|
1379
|
-
cache: true,
|
|
1380
|
-
inputs: ["default", `{projectRoot}/${FORMAT_CONFIG_FILE}`]
|
|
1381
|
-
},
|
|
1382
|
-
[`${FORMAT_SCRIPT_NAME}:fix`]: { cache: false }
|
|
1383
|
-
};
|
|
1910
|
+
function scopeFlags(paths, options = []) {
|
|
1911
|
+
if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
|
|
1912
|
+
return [TOLERATE_EMPTY_FLAG];
|
|
1384
1913
|
}
|
|
1385
1914
|
|
|
1386
1915
|
// src/roles/format/format-script.ts
|
|
@@ -1411,8 +1940,8 @@ function writesWhenRewritten(name, command) {
|
|
|
1411
1940
|
var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
|
|
1412
1941
|
|
|
1413
1942
|
// src/roles/format/inherited-ignores.ts
|
|
1414
|
-
import { existsSync as
|
|
1415
|
-
import { join as
|
|
1943
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
|
|
1944
|
+
import { join as join15 } from "path";
|
|
1416
1945
|
var ROOT_IGNORE_FILE = ".prettierignore";
|
|
1417
1946
|
function isPattern(line) {
|
|
1418
1947
|
const trimmed = line.trim();
|
|
@@ -1427,11 +1956,11 @@ function toModulePattern(pattern) {
|
|
|
1427
1956
|
}
|
|
1428
1957
|
function inheritedIgnorePatterns(workspaceRoot) {
|
|
1429
1958
|
if (!workspaceRoot) return [];
|
|
1430
|
-
const path =
|
|
1431
|
-
if (!
|
|
1959
|
+
const path = join15(workspaceRoot, ROOT_IGNORE_FILE);
|
|
1960
|
+
if (!existsSync14(path)) return [];
|
|
1432
1961
|
let contents;
|
|
1433
1962
|
try {
|
|
1434
|
-
contents =
|
|
1963
|
+
contents = readFileSync11(path, "utf8");
|
|
1435
1964
|
} catch {
|
|
1436
1965
|
return [];
|
|
1437
1966
|
}
|
|
@@ -1453,78 +1982,36 @@ function unparseableFiles(output) {
|
|
|
1453
1982
|
const location = LOCATION.exec(raw);
|
|
1454
1983
|
if (location && pending) {
|
|
1455
1984
|
diagnostics.push({
|
|
1456
|
-
file: location[1] ?? "",
|
|
1457
|
-
line: Number(location[2] ?? 0),
|
|
1458
|
-
message: pending
|
|
1459
|
-
});
|
|
1460
|
-
pending = void 0;
|
|
1461
|
-
continue;
|
|
1462
|
-
}
|
|
1463
|
-
const message = MESSAGE.exec(raw);
|
|
1464
|
-
if (message) pending = message[1];
|
|
1465
|
-
}
|
|
1466
|
-
return diagnostics;
|
|
1467
|
-
}
|
|
1468
|
-
function oxfmtSaid(output) {
|
|
1469
|
-
const lines = plain(output).split("\n");
|
|
1470
|
-
const diagnostic = lines.find((line) => MESSAGE.test(line));
|
|
1471
|
-
if (diagnostic) return MESSAGE.exec(diagnostic)?.[1] ?? diagnostic.trim();
|
|
1472
|
-
return [...lines].reverse().find((line) => line.trim().length > 0)?.trim() ?? "no output";
|
|
1473
|
-
}
|
|
1474
|
-
function needsSvelteCompiler(output) {
|
|
1475
|
-
return /svelte\/compiler/.test(output);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// src/roles/format/presets/base.json
|
|
1479
|
-
var base_default = {
|
|
1480
|
-
printWidth: 80,
|
|
1481
|
-
semi: false,
|
|
1482
|
-
singleQuote: true,
|
|
1483
|
-
trailingComma: "all",
|
|
1484
|
-
sortPackageJson: false,
|
|
1485
|
-
sortImports: false
|
|
1486
|
-
};
|
|
1487
|
-
|
|
1488
|
-
// src/roles/format/presets/nest-imports.json
|
|
1489
|
-
var nest_imports_default = {
|
|
1490
|
-
customGroups: [
|
|
1491
|
-
{
|
|
1492
|
-
groupName: "workspace",
|
|
1493
|
-
elementNamePattern: ["@hublo/**", "@shared/**", "@front/**", "@network/**"]
|
|
1494
|
-
}
|
|
1495
|
-
],
|
|
1496
|
-
groups: ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
|
|
1497
|
-
newlinesBetween: true,
|
|
1498
|
-
order: "asc",
|
|
1499
|
-
ignoreCase: false
|
|
1500
|
-
};
|
|
1501
|
-
|
|
1502
|
-
// src/roles/format/preset-data.ts
|
|
1503
|
-
var FORMAT_PRESETS = ["base", "svelte", "nest"];
|
|
1504
|
-
function hasFormatPreset(name) {
|
|
1505
|
-
return FORMAT_PRESETS.includes(name);
|
|
1985
|
+
file: location[1] ?? "",
|
|
1986
|
+
line: Number(location[2] ?? 0),
|
|
1987
|
+
message: pending
|
|
1988
|
+
});
|
|
1989
|
+
pending = void 0;
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
const message = MESSAGE.exec(raw);
|
|
1993
|
+
if (message) pending = message[1];
|
|
1994
|
+
}
|
|
1995
|
+
return diagnostics;
|
|
1506
1996
|
}
|
|
1507
|
-
function
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
return
|
|
1997
|
+
function oxfmtSaid(output) {
|
|
1998
|
+
const lines = plain(output).split("\n");
|
|
1999
|
+
const diagnostic = lines.find((line) => MESSAGE.test(line));
|
|
2000
|
+
if (diagnostic) return MESSAGE.exec(diagnostic)?.[1] ?? diagnostic.trim();
|
|
2001
|
+
return [...lines].reverse().find((line) => line.trim().length > 0)?.trim() ?? "no output";
|
|
1511
2002
|
}
|
|
1512
|
-
function
|
|
1513
|
-
|
|
1514
|
-
const name = formatPresetName(preset);
|
|
1515
|
-
if (name === "svelte") return { ...base, svelte: true };
|
|
1516
|
-
if (name === "nest") return { ...base, sortImports: nest_imports_default };
|
|
1517
|
-
return base;
|
|
2003
|
+
function needsSvelteCompiler(output) {
|
|
2004
|
+
return /svelte\/compiler/.test(output);
|
|
1518
2005
|
}
|
|
1519
2006
|
|
|
1520
2007
|
// src/roles/format/prettier-config.ts
|
|
1521
|
-
import { existsSync as
|
|
1522
|
-
import { join as
|
|
2008
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
2009
|
+
import { join as join17 } from "path";
|
|
1523
2010
|
|
|
1524
2011
|
// src/roles/format/resolve-oxfmt.ts
|
|
1525
|
-
import { readFileSync as
|
|
2012
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
1526
2013
|
import { createRequire as createRequire2 } from "module";
|
|
1527
|
-
import { dirname as dirname4, join as
|
|
2014
|
+
import { dirname as dirname4, join as join16 } from "path";
|
|
1528
2015
|
function resolveOxfmt(cwd) {
|
|
1529
2016
|
return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
|
|
1530
2017
|
}
|
|
@@ -1553,8 +2040,8 @@ function configSchema() {
|
|
|
1553
2040
|
function readConfigSchema() {
|
|
1554
2041
|
try {
|
|
1555
2042
|
const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
|
|
1556
|
-
const schemaPath =
|
|
1557
|
-
return JSON.parse(
|
|
2043
|
+
const schemaPath = join16(dirname4(manifest), "configuration_schema.json");
|
|
2044
|
+
return JSON.parse(readFileSync12(schemaPath, "utf8"));
|
|
1558
2045
|
} catch {
|
|
1559
2046
|
return void 0;
|
|
1560
2047
|
}
|
|
@@ -1585,14 +2072,14 @@ function toOxfmtOverrides(value) {
|
|
|
1585
2072
|
return { overrides, unresolved };
|
|
1586
2073
|
}
|
|
1587
2074
|
function readPrettierSettings(cwd) {
|
|
1588
|
-
const file = PRETTIER_CONFIG_FILES.find((name) =>
|
|
2075
|
+
const file = PRETTIER_CONFIG_FILES.find((name) => existsSync15(join17(cwd, name)));
|
|
1589
2076
|
if (!file) return { options: {}, unresolved: [] };
|
|
1590
2077
|
if (/\.(js|cjs|mjs)$/.test(file)) {
|
|
1591
2078
|
return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
|
|
1592
2079
|
}
|
|
1593
2080
|
let parsed;
|
|
1594
2081
|
try {
|
|
1595
|
-
parsed = parseJsonc(
|
|
2082
|
+
parsed = parseJsonc(readFileSync13(join17(cwd, file), "utf8"), file);
|
|
1596
2083
|
} catch {
|
|
1597
2084
|
return { options: {}, file, unresolved: [`${file} could not be parsed`] };
|
|
1598
2085
|
}
|
|
@@ -1621,71 +2108,6 @@ function readPrettierSettings(cwd) {
|
|
|
1621
2108
|
return { options, file, unresolved };
|
|
1622
2109
|
}
|
|
1623
2110
|
|
|
1624
|
-
// src/roles/format/read-adoption.ts
|
|
1625
|
-
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
1626
|
-
import { join as join13 } from "path";
|
|
1627
|
-
var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
|
|
1628
|
-
configFile,
|
|
1629
|
-
preset: null,
|
|
1630
|
-
adopted: false,
|
|
1631
|
-
conformant: false,
|
|
1632
|
-
drift: [],
|
|
1633
|
-
local: {},
|
|
1634
|
-
presetVersion: null,
|
|
1635
|
-
effective: {},
|
|
1636
|
-
unreadable
|
|
1637
|
-
});
|
|
1638
|
-
function sameValue(a, b) {
|
|
1639
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
1640
|
-
}
|
|
1641
|
-
function readFormatAdoption(cwd) {
|
|
1642
|
-
const path = join13(cwd, FORMAT_CONFIG_FILE);
|
|
1643
|
-
if (!existsSync11(path)) return NOT_ADOPTED2(null);
|
|
1644
|
-
let parsed;
|
|
1645
|
-
try {
|
|
1646
|
-
parsed = parseJsonc(readFileSync10(path, "utf8"), FORMAT_CONFIG_FILE);
|
|
1647
|
-
} catch (error) {
|
|
1648
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
1649
|
-
return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
|
|
1650
|
-
}
|
|
1651
|
-
const provenance = parsed[PROVENANCE_KEY];
|
|
1652
|
-
if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED2(FORMAT_CONFIG_FILE);
|
|
1653
|
-
if (!hasFormatPreset(provenance.preset)) {
|
|
1654
|
-
return NOT_ADOPTED2(
|
|
1655
|
-
FORMAT_CONFIG_FILE,
|
|
1656
|
-
`${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
|
|
1657
|
-
);
|
|
1658
|
-
}
|
|
1659
|
-
const declaredLocal = Array.isArray(provenance.local) ? provenance.local : [];
|
|
1660
|
-
const preset = formatPresetFor(provenance.preset);
|
|
1661
|
-
const drift = [];
|
|
1662
|
-
const local = {};
|
|
1663
|
-
for (const [key, expected] of Object.entries(preset)) {
|
|
1664
|
-
if (declaredLocal.includes(key)) {
|
|
1665
|
-
local[key] = parsed[key];
|
|
1666
|
-
continue;
|
|
1667
|
-
}
|
|
1668
|
-
if (!(key in parsed) || !sameValue(parsed[key], expected)) drift.push(key);
|
|
1669
|
-
}
|
|
1670
|
-
for (const key of declaredLocal) {
|
|
1671
|
-
if (!(key in preset)) local[key] = parsed[key];
|
|
1672
|
-
}
|
|
1673
|
-
for (const key of ALWAYS_LOCAL_KEYS) {
|
|
1674
|
-
if (key in parsed) local[key] = parsed[key];
|
|
1675
|
-
}
|
|
1676
|
-
return {
|
|
1677
|
-
configFile: FORMAT_CONFIG_FILE,
|
|
1678
|
-
preset: provenance.preset,
|
|
1679
|
-
adopted: true,
|
|
1680
|
-
conformant: drift.length === 0,
|
|
1681
|
-
drift,
|
|
1682
|
-
local,
|
|
1683
|
-
presetVersion: typeof provenance.version === "string" ? provenance.version : null,
|
|
1684
|
-
effective: parsed,
|
|
1685
|
-
unreadable: null
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
2111
|
// src/roles/format/unenforced.ts
|
|
1690
2112
|
function unenforcedFormatOptions(options) {
|
|
1691
2113
|
const gaps = [];
|
|
@@ -1768,7 +2190,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1768
2190
|
manifestOperation(context.cwd, this.formatScripts(context.cwd))
|
|
1769
2191
|
];
|
|
1770
2192
|
const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
|
|
1771
|
-
(name) =>
|
|
2193
|
+
(name) => existsSync16(join18(context.cwd, name))
|
|
1772
2194
|
);
|
|
1773
2195
|
for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
|
|
1774
2196
|
const removableDeps = this.modulePrettierDependencies(context.cwd);
|
|
@@ -1892,7 +2314,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1892
2314
|
});
|
|
1893
2315
|
const result = spawnSync3(
|
|
1894
2316
|
oxfmt,
|
|
1895
|
-
[mode, ...scopeFlags(paths), ...options, ...paths.length > 0 ? paths : ["."]],
|
|
2317
|
+
[mode, ...scopeFlags(paths, options), ...options, ...paths.length > 0 ? paths : ["."]],
|
|
1896
2318
|
{
|
|
1897
2319
|
cwd: ctx.cwd,
|
|
1898
2320
|
stdio: "inherit"
|
|
@@ -2008,7 +2430,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
2008
2430
|
modulePrettierDependencies(cwd) {
|
|
2009
2431
|
const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
|
|
2010
2432
|
try {
|
|
2011
|
-
const manifest = JSON.parse(
|
|
2433
|
+
const manifest = JSON.parse(readFileSync14(join18(cwd, "package.json"), "utf8"));
|
|
2012
2434
|
return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
|
|
2013
2435
|
} catch {
|
|
2014
2436
|
return [];
|
|
@@ -2090,8 +2512,8 @@ function registerFormat() {
|
|
|
2090
2512
|
|
|
2091
2513
|
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
2092
2514
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2093
|
-
import { existsSync as
|
|
2094
|
-
import { join as
|
|
2515
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2516
|
+
import { join as join28 } from "path";
|
|
2095
2517
|
|
|
2096
2518
|
// src/core/config/deferred-rules.ts
|
|
2097
2519
|
function deferredRuleNames(rules) {
|
|
@@ -2099,8 +2521,8 @@ function deferredRuleNames(rules) {
|
|
|
2099
2521
|
}
|
|
2100
2522
|
|
|
2101
2523
|
// src/core/fix-report.ts
|
|
2102
|
-
import { statSync } from "fs";
|
|
2103
|
-
import { join as
|
|
2524
|
+
import { statSync as statSync2 } from "fs";
|
|
2525
|
+
import { join as join19 } from "path";
|
|
2104
2526
|
var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
|
|
2105
2527
|
function readFailure(output) {
|
|
2106
2528
|
const lines = output.split("\n").map((line2) => line2.trim());
|
|
@@ -2111,7 +2533,7 @@ function readFailure(output) {
|
|
|
2111
2533
|
}
|
|
2112
2534
|
function fingerprint(path) {
|
|
2113
2535
|
try {
|
|
2114
|
-
const stat =
|
|
2536
|
+
const stat = statSync2(path);
|
|
2115
2537
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
2116
2538
|
} catch {
|
|
2117
2539
|
return void 0;
|
|
@@ -2120,7 +2542,7 @@ function fingerprint(path) {
|
|
|
2120
2542
|
function snapshotFiles(cwd, extensions) {
|
|
2121
2543
|
const snapshot = /* @__PURE__ */ new Map();
|
|
2122
2544
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2123
|
-
const mark = fingerprint(
|
|
2545
|
+
const mark = fingerprint(join19(cwd, file));
|
|
2124
2546
|
if (mark !== void 0) snapshot.set(file, mark);
|
|
2125
2547
|
}
|
|
2126
2548
|
return snapshot;
|
|
@@ -2128,7 +2550,7 @@ function snapshotFiles(cwd, extensions) {
|
|
|
2128
2550
|
function changedSince(cwd, before, extensions) {
|
|
2129
2551
|
const changed = [];
|
|
2130
2552
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2131
|
-
const now = fingerprint(
|
|
2553
|
+
const now = fingerprint(join19(cwd, file));
|
|
2132
2554
|
if (now === void 0) continue;
|
|
2133
2555
|
if (before.get(file) !== now) changed.push(file);
|
|
2134
2556
|
}
|
|
@@ -2147,8 +2569,8 @@ function describeFixOutcome(outcome, toolLabel) {
|
|
|
2147
2569
|
}
|
|
2148
2570
|
|
|
2149
2571
|
// src/roles/lint/module-baseline.ts
|
|
2150
|
-
import { existsSync as
|
|
2151
|
-
import { join as
|
|
2572
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
2573
|
+
import { join as join20 } from "path";
|
|
2152
2574
|
var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
|
|
2153
2575
|
var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
|
|
2154
2576
|
var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
|
|
@@ -2192,11 +2614,11 @@ function describeHolds(holds) {
|
|
|
2192
2614
|
return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
|
|
2193
2615
|
}
|
|
2194
2616
|
function heldRules(cwd) {
|
|
2195
|
-
const path =
|
|
2196
|
-
if (!
|
|
2617
|
+
const path = join20(cwd, LINT_BASELINE_FILE);
|
|
2618
|
+
if (!existsSync17(path)) return [];
|
|
2197
2619
|
try {
|
|
2198
2620
|
const parsed = parseJsonc(
|
|
2199
|
-
|
|
2621
|
+
readFileSync15(path, "utf8"),
|
|
2200
2622
|
LINT_BASELINE_FILE
|
|
2201
2623
|
);
|
|
2202
2624
|
return Object.keys(parsed.rules ?? {});
|
|
@@ -2627,7 +3049,8 @@ var react_lib_default = {
|
|
|
2627
3049
|
{
|
|
2628
3050
|
name: "storybook",
|
|
2629
3051
|
specifier: "eslint-plugin-storybook"
|
|
2630
|
-
}
|
|
3052
|
+
},
|
|
3053
|
+
"../plugins/hublo.js"
|
|
2631
3054
|
],
|
|
2632
3055
|
settings: null,
|
|
2633
3056
|
renames: {
|
|
@@ -2636,6 +3059,7 @@ var react_lib_default = {
|
|
|
2636
3059
|
rules: {
|
|
2637
3060
|
curly: ["error", "all"],
|
|
2638
3061
|
"for-direction": "error",
|
|
3062
|
+
"hublo/no-regex-lookbehind": "error",
|
|
2639
3063
|
"jest-dom/prefer-checked": "error",
|
|
2640
3064
|
"jest-dom/prefer-empty": "error",
|
|
2641
3065
|
"jest-dom/prefer-enabled-disabled": "error",
|
|
@@ -2987,7 +3411,7 @@ var react_lib_default = {
|
|
|
2987
3411
|
],
|
|
2988
3412
|
uncovered: {
|
|
2989
3413
|
"no-octal": "no oxlint equivalent",
|
|
2990
|
-
"no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead,
|
|
3414
|
+
"no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead, so this tier hosts `../plugins/hublo.js` too. It enforces exactly one of them: `hublo/no-regex-lookbehind`, because a library is where that bug is WRITTEN and it reaches an app tier only by being imported (HUB-43431 shipped from libs/front/components into the worker app). The palette selectors stay app-tier only.",
|
|
2991
3415
|
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
|
|
2992
3416
|
}
|
|
2993
3417
|
};
|
|
@@ -3051,6 +3475,7 @@ var react_default = {
|
|
|
3051
3475
|
severity: "off",
|
|
3052
3476
|
reason: "the house rule finds 3 real violations; they are fixed as their own change rather than inside a linter swap, so the rule ships visible-but-off and can be switched on once they are"
|
|
3053
3477
|
},
|
|
3478
|
+
"hublo/no-regex-lookbehind": "error",
|
|
3054
3479
|
"jest/expect-expect": {
|
|
3055
3480
|
severity: "off",
|
|
3056
3481
|
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
@@ -3537,7 +3962,7 @@ var react_default = {
|
|
|
3537
3962
|
uncovered: {
|
|
3538
3963
|
"import/order": "import ordering is FORMATTING, and the format role owns it: oxfmt's `sortImports` reproduces exactly these groups (builtin, external, internal, parent/index, sibling, alphabetical, blank line between) and sorts a file completely in ONE pass, because it rewrites the whole import block. A lint fixer cannot: it emits text edits over overlapping ranges, so this rule needed several runs to finish (five imports took four passes, measured) and sentinel deliberately runs autofix once. Enforced by `pnpm run format`, not by the linter, which is also why `eslint-plugin-import` is no longer hosted at all: its only other rule, no-duplicates, is native in oxlint.",
|
|
3539
3964
|
"no-octal": "no oxlint equivalent",
|
|
3540
|
-
"no-restricted-syntax": "no oxlint equivalent as configuration, so the
|
|
3965
|
+
"no-restricted-syntax": "no oxlint equivalent as configuration, so the AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Two are hosted today: the eight palette selectors as `hublo/no-palette-common`, and the lookbehind selector as `hublo/no-regex-lookbehind`. Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
|
|
3541
3966
|
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
|
|
3542
3967
|
}
|
|
3543
3968
|
};
|
|
@@ -4019,14 +4444,14 @@ function downgradedRulesFor(preset) {
|
|
|
4019
4444
|
}
|
|
4020
4445
|
|
|
4021
4446
|
// src/roles/lint/extra-layers.ts
|
|
4022
|
-
import { existsSync as
|
|
4023
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4447
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
4448
|
+
import { isAbsolute as isAbsolute2, join as join21, resolve as resolve3 } from "path";
|
|
4024
4449
|
function ruleCount(cwd, specifier) {
|
|
4025
4450
|
const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
|
|
4026
|
-
if (!
|
|
4451
|
+
if (!existsSync18(path)) return void 0;
|
|
4027
4452
|
try {
|
|
4028
4453
|
const parsed = parseJsonc(
|
|
4029
|
-
|
|
4454
|
+
readFileSync16(path, "utf8"),
|
|
4030
4455
|
specifier
|
|
4031
4456
|
);
|
|
4032
4457
|
return Object.keys(parsed.rules ?? {}).length;
|
|
@@ -4046,7 +4471,7 @@ function extraLayers(cwd, extendsList) {
|
|
|
4046
4471
|
function committedExtendsList(cwd, configFile) {
|
|
4047
4472
|
try {
|
|
4048
4473
|
const parsed = parseJsonc(
|
|
4049
|
-
|
|
4474
|
+
readFileSync16(join21(cwd, configFile), "utf8"),
|
|
4050
4475
|
configFile
|
|
4051
4476
|
);
|
|
4052
4477
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4105,8 +4530,8 @@ function errorCountsByConfigRule(stdout) {
|
|
|
4105
4530
|
}
|
|
4106
4531
|
|
|
4107
4532
|
// src/core/config/read-adoption.ts
|
|
4108
|
-
import { existsSync as
|
|
4109
|
-
import { join as
|
|
4533
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
|
|
4534
|
+
import { join as join23 } from "path";
|
|
4110
4535
|
|
|
4111
4536
|
// src/core/config/owned-keys.ts
|
|
4112
4537
|
function presetOwnedKeys(config, permitted, presetSets) {
|
|
@@ -4121,12 +4546,12 @@ function localOnlyKeys(config, permitted, presetSets) {
|
|
|
4121
4546
|
}
|
|
4122
4547
|
|
|
4123
4548
|
// src/core/config/resolve-config-target.ts
|
|
4124
|
-
import { existsSync as
|
|
4125
|
-
import { join as
|
|
4549
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
|
|
4550
|
+
import { join as join22 } from "path";
|
|
4126
4551
|
function readExtends(absolutePath) {
|
|
4127
4552
|
let parsed;
|
|
4128
4553
|
try {
|
|
4129
|
-
parsed = parseJsonc(
|
|
4554
|
+
parsed = parseJsonc(readFileSync17(absolutePath, "utf8"), absolutePath);
|
|
4130
4555
|
} catch {
|
|
4131
4556
|
return [];
|
|
4132
4557
|
}
|
|
@@ -4140,8 +4565,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
|
|
|
4140
4565
|
let existing;
|
|
4141
4566
|
let existingExtendsSomething = false;
|
|
4142
4567
|
for (const candidate of candidates) {
|
|
4143
|
-
const absolutePath =
|
|
4144
|
-
if (!
|
|
4568
|
+
const absolutePath = join22(moduleDir, candidate);
|
|
4569
|
+
if (!existsSync19(absolutePath)) continue;
|
|
4145
4570
|
const chain = readExtends(absolutePath);
|
|
4146
4571
|
if (existing === void 0) {
|
|
4147
4572
|
existing = candidate;
|
|
@@ -4174,12 +4599,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
|
|
|
4174
4599
|
});
|
|
4175
4600
|
function readAdoption(cwd, options) {
|
|
4176
4601
|
const target = resolveConfigTarget(cwd, options);
|
|
4177
|
-
if (target.reason === "none" || !
|
|
4602
|
+
if (target.reason === "none" || !existsSync20(join23(cwd, target.path))) {
|
|
4178
4603
|
return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
|
|
4179
4604
|
}
|
|
4180
4605
|
let parsed;
|
|
4181
4606
|
try {
|
|
4182
|
-
parsed = parseJsonc(
|
|
4607
|
+
parsed = parseJsonc(readFileSync18(join23(cwd, target.path), "utf8"), target.path);
|
|
4183
4608
|
} catch (error) {
|
|
4184
4609
|
const reason = error instanceof Error ? error.message : String(error);
|
|
4185
4610
|
return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
|
|
@@ -4213,9 +4638,9 @@ function readLintAdoption(cwd) {
|
|
|
4213
4638
|
}
|
|
4214
4639
|
|
|
4215
4640
|
// src/roles/lint/resolve-oxlint.ts
|
|
4216
|
-
import { existsSync as
|
|
4641
|
+
import { existsSync as existsSync21 } from "fs";
|
|
4217
4642
|
import { createRequire as createRequire3 } from "module";
|
|
4218
|
-
import { delimiter as delimiter2, dirname as dirname5, join as
|
|
4643
|
+
import { delimiter as delimiter2, dirname as dirname5, join as join24 } from "path";
|
|
4219
4644
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4220
4645
|
var PACKAGE_OF = {
|
|
4221
4646
|
oxlint: "oxlint",
|
|
@@ -4240,30 +4665,30 @@ function tsgolintShim(cwd) {
|
|
|
4240
4665
|
for (const owner of ["oxlint-tsgolint", "oxlint"]) {
|
|
4241
4666
|
try {
|
|
4242
4667
|
const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
|
|
4243
|
-
candidates.push(
|
|
4244
|
-
candidates.push(
|
|
4668
|
+
candidates.push(join24(packageDir, "node_modules", ".bin", "tsgolint"));
|
|
4669
|
+
candidates.push(join24(packageDir, "..", ".bin", "tsgolint"));
|
|
4245
4670
|
} catch {
|
|
4246
4671
|
}
|
|
4247
4672
|
}
|
|
4248
4673
|
try {
|
|
4249
4674
|
const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
|
|
4250
|
-
candidates.push(
|
|
4675
|
+
candidates.push(join24(ownRoot, "node_modules", ".bin", "tsgolint"));
|
|
4251
4676
|
} catch {
|
|
4252
4677
|
candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
|
|
4253
4678
|
}
|
|
4254
|
-
return candidates.find((candidate) => candidate !== "" &&
|
|
4679
|
+
return candidates.find((candidate) => candidate !== "" && existsSync21(candidate));
|
|
4255
4680
|
}
|
|
4256
4681
|
function oxlintSearchPath(cwd) {
|
|
4257
4682
|
return binSearchPath(cwd);
|
|
4258
4683
|
}
|
|
4259
4684
|
|
|
4260
4685
|
// src/roles/lint/adapters/oxlint/plan.ts
|
|
4261
|
-
import { existsSync as
|
|
4262
|
-
import { join as
|
|
4686
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
|
|
4687
|
+
import { join as join27 } from "path";
|
|
4263
4688
|
|
|
4264
4689
|
// src/roles/lint/eslint-ignores.ts
|
|
4265
|
-
import { existsSync as
|
|
4266
|
-
import { join as
|
|
4690
|
+
import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
|
|
4691
|
+
import { join as join25 } from "path";
|
|
4267
4692
|
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4268
4693
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
4269
4694
|
function toOxlintPattern(pattern) {
|
|
@@ -4275,11 +4700,11 @@ function readRootEslintIgnores(root) {
|
|
|
4275
4700
|
return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
|
|
4276
4701
|
}
|
|
4277
4702
|
function readEslintIgnores(cwd) {
|
|
4278
|
-
const config = ESLINT_CONFIG_FILES.map((name) =>
|
|
4703
|
+
const config = ESLINT_CONFIG_FILES.map((name) => join25(cwd, name)).find((path) => existsSync22(path));
|
|
4279
4704
|
if (!config) return { patterns: [], unresolved: [] };
|
|
4280
4705
|
let source;
|
|
4281
4706
|
try {
|
|
4282
|
-
source =
|
|
4707
|
+
source = readFileSync19(config, "utf8");
|
|
4283
4708
|
} catch {
|
|
4284
4709
|
return { patterns: [], unresolved: [] };
|
|
4285
4710
|
}
|
|
@@ -4380,8 +4805,8 @@ function lintPresetFor(preset) {
|
|
|
4380
4805
|
}
|
|
4381
4806
|
|
|
4382
4807
|
// src/roles/lint/rename-suppressions.ts
|
|
4383
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
4384
|
-
import { join as
|
|
4808
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync20, statSync as statSync3 } from "fs";
|
|
4809
|
+
import { join as join26, relative as relative4 } from "path";
|
|
4385
4810
|
var SOURCE_EXTENSIONS = [
|
|
4386
4811
|
".ts",
|
|
4387
4812
|
".tsx",
|
|
@@ -4428,10 +4853,10 @@ function* sourceFiles(dir) {
|
|
|
4428
4853
|
return;
|
|
4429
4854
|
}
|
|
4430
4855
|
for (const entry of entries) {
|
|
4431
|
-
const full =
|
|
4856
|
+
const full = join26(dir, entry);
|
|
4432
4857
|
let isDirectory;
|
|
4433
4858
|
try {
|
|
4434
|
-
isDirectory =
|
|
4859
|
+
isDirectory = statSync3(full).isDirectory();
|
|
4435
4860
|
} catch {
|
|
4436
4861
|
continue;
|
|
4437
4862
|
}
|
|
@@ -4448,7 +4873,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4448
4873
|
for (const file of sourceFiles(cwd)) {
|
|
4449
4874
|
let content;
|
|
4450
4875
|
try {
|
|
4451
|
-
content =
|
|
4876
|
+
content = readFileSync20(file, "utf8");
|
|
4452
4877
|
} catch {
|
|
4453
4878
|
continue;
|
|
4454
4879
|
}
|
|
@@ -4472,7 +4897,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4472
4897
|
}
|
|
4473
4898
|
if (hits.length === 0) return;
|
|
4474
4899
|
found.push({
|
|
4475
|
-
file:
|
|
4900
|
+
file: relative4(cwd, file),
|
|
4476
4901
|
line: index + 1,
|
|
4477
4902
|
from: line,
|
|
4478
4903
|
to: line.replace(rules, renamed.join(", ")),
|
|
@@ -4576,7 +5001,7 @@ function plan2(context) {
|
|
|
4576
5001
|
keys: removableDeps
|
|
4577
5002
|
});
|
|
4578
5003
|
}
|
|
4579
|
-
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) =>
|
|
5004
|
+
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync23(join27(context.cwd, name)));
|
|
4580
5005
|
for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
|
|
4581
5006
|
operations.push(
|
|
4582
5007
|
...nxTargetOperations({
|
|
@@ -4674,7 +5099,7 @@ function lintScripts(cwd) {
|
|
|
4674
5099
|
function committedExtends(cwd) {
|
|
4675
5100
|
try {
|
|
4676
5101
|
const parsed = parseJsonc(
|
|
4677
|
-
|
|
5102
|
+
readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4678
5103
|
LINT_CONFIG_FILE
|
|
4679
5104
|
);
|
|
4680
5105
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4685,7 +5110,7 @@ function committedExtends(cwd) {
|
|
|
4685
5110
|
function committedIgnorePatterns(cwd) {
|
|
4686
5111
|
try {
|
|
4687
5112
|
const parsed = parseJsonc(
|
|
4688
|
-
|
|
5113
|
+
readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4689
5114
|
LINT_CONFIG_FILE
|
|
4690
5115
|
);
|
|
4691
5116
|
return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4697,7 +5122,7 @@ function moduleEslintDependencies(cwd) {
|
|
|
4697
5122
|
const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
|
|
4698
5123
|
let manifest;
|
|
4699
5124
|
try {
|
|
4700
|
-
manifest = JSON.parse(
|
|
5125
|
+
manifest = JSON.parse(readFileSync21(join27(cwd, "package.json"), "utf8"));
|
|
4701
5126
|
} catch {
|
|
4702
5127
|
return [];
|
|
4703
5128
|
}
|
|
@@ -4831,23 +5256,23 @@ ${result.stderr ?? ""}`);
|
|
|
4831
5256
|
* build the developer can see, rather than a silent half-adoption they cannot.
|
|
4832
5257
|
*/
|
|
4833
5258
|
writeModuleBaseline(ctx, oxlint, env) {
|
|
4834
|
-
const configPath =
|
|
4835
|
-
const baselinePath =
|
|
4836
|
-
if (!
|
|
5259
|
+
const configPath = join28(ctx.cwd, LINT_CONFIG_FILE);
|
|
5260
|
+
const baselinePath = join28(ctx.cwd, LINT_BASELINE_FILE);
|
|
5261
|
+
if (!existsSync24(configPath)) return;
|
|
4837
5262
|
let config;
|
|
4838
5263
|
try {
|
|
4839
5264
|
config = parseJsonc(
|
|
4840
|
-
|
|
5265
|
+
readFileSync22(configPath, "utf8"),
|
|
4841
5266
|
LINT_CONFIG_FILE
|
|
4842
5267
|
);
|
|
4843
5268
|
} catch {
|
|
4844
5269
|
return;
|
|
4845
5270
|
}
|
|
4846
5271
|
const current = Array.isArray(config.extends) ? config.extends : [];
|
|
4847
|
-
const measurePath =
|
|
5272
|
+
const measurePath = join28(ctx.cwd, LINT_MEASURE_FILE);
|
|
4848
5273
|
let measured;
|
|
4849
5274
|
try {
|
|
4850
|
-
|
|
5275
|
+
writeFileSync3(
|
|
4851
5276
|
measurePath,
|
|
4852
5277
|
`${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
|
|
4853
5278
|
`
|
|
@@ -4865,7 +5290,7 @@ ${result.stderr ?? ""}`);
|
|
|
4865
5290
|
{ cwd: ctx.cwd, encoding: "utf8", env, maxBuffer: CAPTURE_MAX_BUFFER }
|
|
4866
5291
|
);
|
|
4867
5292
|
} finally {
|
|
4868
|
-
|
|
5293
|
+
rmSync2(measurePath, { force: true });
|
|
4869
5294
|
}
|
|
4870
5295
|
if (measured.error || !(measured.stdout ?? "").includes('"diagnostics"')) {
|
|
4871
5296
|
process.stderr.write(
|
|
@@ -4879,20 +5304,20 @@ ${result.stderr ?? ""}`);
|
|
|
4879
5304
|
const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
|
|
4880
5305
|
const next = extendsWithBaseline(current, holds.length > 0);
|
|
4881
5306
|
if (holds.length > 0) {
|
|
4882
|
-
|
|
5307
|
+
writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
|
|
4883
5308
|
process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
|
|
4884
5309
|
`);
|
|
4885
5310
|
} else {
|
|
4886
|
-
|
|
5311
|
+
rmSync2(baselinePath, { force: true });
|
|
4887
5312
|
}
|
|
4888
5313
|
if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
|
|
4889
5314
|
config.extends = next;
|
|
4890
|
-
|
|
5315
|
+
writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
|
|
4891
5316
|
`);
|
|
4892
5317
|
}
|
|
4893
5318
|
}
|
|
4894
5319
|
async run(ctx) {
|
|
4895
|
-
if (!
|
|
5320
|
+
if (!existsSync24(join28(ctx.cwd, LINT_CONFIG_FILE))) {
|
|
4896
5321
|
process.stderr.write(
|
|
4897
5322
|
`sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
|
|
4898
5323
|
`
|
|
@@ -4937,7 +5362,7 @@ ${result.stderr ?? ""}`);
|
|
|
4937
5362
|
valueFlags: OXLINT_VALUE_FLAGS
|
|
4938
5363
|
});
|
|
4939
5364
|
const targets = paths.length > 0 ? paths : ["."];
|
|
4940
|
-
const emptyScope = scopeFlags(paths);
|
|
5365
|
+
const emptyScope = scopeFlags(paths, passedOptions);
|
|
4941
5366
|
const lint = (extra) => {
|
|
4942
5367
|
const passed = [...extra, ...emptyScope, ...passedOptions];
|
|
4943
5368
|
const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
|
|
@@ -5040,7 +5465,7 @@ ${result.stderr ?? ""}`);
|
|
|
5040
5465
|
let stub;
|
|
5041
5466
|
try {
|
|
5042
5467
|
stub = parseJsonc(
|
|
5043
|
-
|
|
5468
|
+
readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5044
5469
|
LINT_CONFIG_FILE
|
|
5045
5470
|
);
|
|
5046
5471
|
} catch {
|
|
@@ -5050,7 +5475,7 @@ ${result.stderr ?? ""}`);
|
|
|
5050
5475
|
for (const entry of stub.extends ?? []) {
|
|
5051
5476
|
try {
|
|
5052
5477
|
const preset = parseJsonc(
|
|
5053
|
-
|
|
5478
|
+
readFileSync22(join28(cwd, entry), "utf8"),
|
|
5054
5479
|
entry
|
|
5055
5480
|
);
|
|
5056
5481
|
for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
|
|
@@ -5120,14 +5545,14 @@ ${result.stderr ?? ""}`);
|
|
|
5120
5545
|
let parsed;
|
|
5121
5546
|
try {
|
|
5122
5547
|
parsed = parseJsonc(
|
|
5123
|
-
|
|
5548
|
+
readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5124
5549
|
LINT_CONFIG_FILE
|
|
5125
5550
|
);
|
|
5126
5551
|
} catch {
|
|
5127
5552
|
return void 0;
|
|
5128
5553
|
}
|
|
5129
5554
|
const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
5130
|
-
return targets.find((target) => !
|
|
5555
|
+
return targets.find((target) => !existsSync24(join28(cwd, target)));
|
|
5131
5556
|
}
|
|
5132
5557
|
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
5133
5558
|
announceDisabled(preset) {
|
|
@@ -5157,11 +5582,355 @@ function registerLint() {
|
|
|
5157
5582
|
setDefaultRunner("lint", "oxlint");
|
|
5158
5583
|
}
|
|
5159
5584
|
|
|
5160
|
-
// src/roles/
|
|
5585
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5161
5586
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5162
|
-
import { existsSync as
|
|
5587
|
+
import { existsSync as existsSync26 } from "fs";
|
|
5588
|
+
import { join as join30 } from "path";
|
|
5589
|
+
|
|
5590
|
+
// src/roles/test/presets/toolchain.json
|
|
5591
|
+
var toolchain_default2 = {
|
|
5592
|
+
$comment: "The test toolchain sentinel owns. Same shape and same rule as the build one: a single table drives what `--init` removes from a module's package.json, which import specifiers move, and the name each default import is re-exported under, so the three cannot disagree.",
|
|
5593
|
+
owned: [
|
|
5594
|
+
{
|
|
5595
|
+
package: "vitest",
|
|
5596
|
+
specifier: "vitest/config",
|
|
5597
|
+
named: ["defineConfig", "mergeConfig"],
|
|
5598
|
+
types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
|
|
5599
|
+
why: "the runner itself, declared at the workspace ROOT today, which is what stops a module being testable on its own. Note the package is `vitest` and the specifier is `vitest/config`: the same split as nitro on the build side, and the reason one table holds both."
|
|
5600
|
+
},
|
|
5601
|
+
{
|
|
5602
|
+
package: "vite",
|
|
5603
|
+
specifier: "vite",
|
|
5604
|
+
named: ["loadEnv"],
|
|
5605
|
+
why: "`loadEnv` has no equivalent in `vitest/config`, and two of the six configs import it from vite to build their test env. Re-exported here rather than claimed: the BUILD role owns the `vite` package, and two roles owning one package is how a version conflict gets written down twice."
|
|
5606
|
+
}
|
|
5607
|
+
]
|
|
5608
|
+
};
|
|
5609
|
+
|
|
5610
|
+
// src/roles/test/preset-data.ts
|
|
5611
|
+
var OWNED_TEST_PACKAGES = toolchain_default2.owned;
|
|
5612
|
+
|
|
5613
|
+
// src/roles/test/config-policy.ts
|
|
5614
|
+
var TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
|
|
5615
|
+
var TEST_CONFIG_FILES = [
|
|
5616
|
+
"vitest.config.ts",
|
|
5617
|
+
"vitest.config.mts",
|
|
5618
|
+
"vitest.config.js",
|
|
5619
|
+
"vitest.config.mjs"
|
|
5620
|
+
];
|
|
5621
|
+
var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
|
|
5622
|
+
var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
|
|
5623
|
+
(entry) => entry.package !== "vite"
|
|
5624
|
+
).map((entry) => entry.package);
|
|
5625
|
+
var TEST_TOOLCHAIN = {
|
|
5626
|
+
presetSpecifier: TEST_PRESET_SPECIFIER,
|
|
5627
|
+
owned: TEST_OWNED_PACKAGES
|
|
5628
|
+
};
|
|
5629
|
+
var TEST_SCRIPT_NAME = "test";
|
|
5630
|
+
var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
|
|
5631
|
+
var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
|
|
5632
|
+
function rewriteVitestScript(command) {
|
|
5633
|
+
return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
|
|
5634
|
+
const args = tail.trim().split(/\s+/).filter(Boolean);
|
|
5635
|
+
const oneShot = args[0] === "run" || args.includes("--run");
|
|
5636
|
+
const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
|
|
5637
|
+
const passthrough = oneShot ? rest : ["--watch", ...rest];
|
|
5638
|
+
const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
|
|
5639
|
+
const trailing = /\s$/.test(tail) ? " " : "";
|
|
5640
|
+
return `${lead}sentinel --run --test${suffix}${trailing}`;
|
|
5641
|
+
});
|
|
5642
|
+
}
|
|
5643
|
+
function testTarget(options) {
|
|
5644
|
+
const { configFiles, outputs } = options;
|
|
5645
|
+
const cached = outputs !== void 0 && outputs.length > 0;
|
|
5646
|
+
return {
|
|
5647
|
+
[TEST_SCRIPT_NAME]: {
|
|
5648
|
+
cache: cached,
|
|
5649
|
+
inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
|
|
5650
|
+
...cached ? { outputs: [...outputs] } : {}
|
|
5651
|
+
}
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
|
|
5655
|
+
// src/roles/test/read-adoption.ts
|
|
5656
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
|
|
5657
|
+
import { join as join29 } from "path";
|
|
5658
|
+
var JEST_CONFIG_FILES = [
|
|
5659
|
+
"jest.config.ts",
|
|
5660
|
+
"jest.config.js",
|
|
5661
|
+
"jest.prisma.config.ts",
|
|
5662
|
+
"jest.functional.config.ts",
|
|
5663
|
+
"jest.integration-config.ts"
|
|
5664
|
+
];
|
|
5665
|
+
function testConfigFile(cwd) {
|
|
5666
|
+
return TEST_CONFIG_FILES.find((name) => existsSync25(join29(cwd, name)));
|
|
5667
|
+
}
|
|
5668
|
+
function jestConfigFiles(cwd) {
|
|
5669
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(cwd, name)));
|
|
5670
|
+
}
|
|
5671
|
+
function rootJestConfigFiles(cwd) {
|
|
5672
|
+
const root = findWorkspaceRoot(cwd);
|
|
5673
|
+
if (root === void 0 || root === cwd) return [];
|
|
5674
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(root, name)));
|
|
5675
|
+
}
|
|
5676
|
+
var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
|
|
5677
|
+
var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
|
|
5678
|
+
function hasTestFiles(cwd) {
|
|
5679
|
+
const stack = [cwd];
|
|
5680
|
+
while (stack.length > 0) {
|
|
5681
|
+
const dir = stack.pop();
|
|
5682
|
+
let entries;
|
|
5683
|
+
try {
|
|
5684
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
5685
|
+
} catch {
|
|
5686
|
+
continue;
|
|
5687
|
+
}
|
|
5688
|
+
for (const entry of entries) {
|
|
5689
|
+
if (entry.isDirectory()) {
|
|
5690
|
+
if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
|
|
5691
|
+
} else if (TEST_FILE.test(entry.name)) {
|
|
5692
|
+
return true;
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
return false;
|
|
5697
|
+
}
|
|
5698
|
+
function readTestConfigSource(cwd, fileName) {
|
|
5699
|
+
try {
|
|
5700
|
+
return readFileSync23(join29(cwd, fileName), "utf8");
|
|
5701
|
+
} catch {
|
|
5702
|
+
return "";
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
function importsPreset2(source) {
|
|
5706
|
+
const escaped = TEST_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5707
|
+
return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
|
|
5708
|
+
}
|
|
5709
|
+
function readTestAdoption(cwd) {
|
|
5710
|
+
const manifest = readProjectPackageJson(cwd);
|
|
5711
|
+
const jestConfigs = jestConfigFiles(cwd);
|
|
5712
|
+
const configFile = testConfigFile(cwd);
|
|
5713
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
5714
|
+
const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
|
|
5715
|
+
if (configFile === void 0) {
|
|
5716
|
+
const hasTests = hasTestFiles(cwd);
|
|
5717
|
+
const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
|
|
5718
|
+
return {
|
|
5719
|
+
configFile: null,
|
|
5720
|
+
preset: null,
|
|
5721
|
+
adopted: false,
|
|
5722
|
+
conformant: false,
|
|
5723
|
+
drift: [],
|
|
5724
|
+
unreadable: null,
|
|
5725
|
+
state: hasTests ? "jest" : "no-tests",
|
|
5726
|
+
runner: hasTests ? "jest" : "none",
|
|
5727
|
+
jestConfigs,
|
|
5728
|
+
inheritsRootConfig,
|
|
5729
|
+
ownDeclarations
|
|
5730
|
+
};
|
|
5731
|
+
}
|
|
5732
|
+
const source = readTestConfigSource(cwd, configFile);
|
|
5733
|
+
if (source === "") {
|
|
5734
|
+
return {
|
|
5735
|
+
configFile,
|
|
5736
|
+
preset: null,
|
|
5737
|
+
adopted: false,
|
|
5738
|
+
conformant: false,
|
|
5739
|
+
drift: [],
|
|
5740
|
+
unreadable: `${configFile} could not be read`,
|
|
5741
|
+
state: "vitest-own-toolchain",
|
|
5742
|
+
runner: "vitest",
|
|
5743
|
+
jestConfigs,
|
|
5744
|
+
inheritsRootConfig: false,
|
|
5745
|
+
// it has a config of its own, readable or not
|
|
5746
|
+
ownDeclarations
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
const adopted = importsPreset2(source);
|
|
5750
|
+
const script = moduleScripts(cwd)["test"];
|
|
5751
|
+
return {
|
|
5752
|
+
configFile,
|
|
5753
|
+
preset: adopted ? "react" : null,
|
|
5754
|
+
adopted,
|
|
5755
|
+
conformant: adopted && script !== void 0 && jestConfigs.length === 0,
|
|
5756
|
+
drift: [],
|
|
5757
|
+
unreadable: null,
|
|
5758
|
+
state: adopted ? "adopted" : "vitest-own-toolchain",
|
|
5759
|
+
runner: "vitest",
|
|
5760
|
+
jestConfigs,
|
|
5761
|
+
inheritsRootConfig: false,
|
|
5762
|
+
// local always wins: this module is out of the root's reach
|
|
5763
|
+
ownDeclarations
|
|
5764
|
+
};
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
// src/roles/test/plan.ts
|
|
5768
|
+
function ownedTestDependencies(cwd, declared) {
|
|
5769
|
+
return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
|
|
5770
|
+
["dependencies", entry.name],
|
|
5771
|
+
["devDependencies", entry.name]
|
|
5772
|
+
]).filter(([section]) => section !== void 0);
|
|
5773
|
+
}
|
|
5774
|
+
function plan3(context) {
|
|
5775
|
+
const configFile = testConfigFile(context.cwd);
|
|
5776
|
+
const adoption = readTestAdoption(context.cwd);
|
|
5777
|
+
if (configFile === void 0) {
|
|
5778
|
+
return {
|
|
5779
|
+
operations: [],
|
|
5780
|
+
skipped: adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}). Migrating it moves its test FILES as well as its config, which is the next step of this role and not this one.` : `this module has no test config and no test files, so there is nothing to point at sentinel. What it needs is its jest leftovers removed, which is a cleanup rather than an adoption.`
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
const operations = [];
|
|
5784
|
+
const notes = [];
|
|
5785
|
+
const rewrite = rewriteToolchainImports(
|
|
5786
|
+
TEST_TOOLCHAIN,
|
|
5787
|
+
readTestConfigSource(context.cwd, configFile),
|
|
5788
|
+
configFile
|
|
5789
|
+
);
|
|
5790
|
+
if (rewrite.kind === "blocked") {
|
|
5791
|
+
return {
|
|
5792
|
+
operations: [],
|
|
5793
|
+
skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
if (rewrite.kind === "rewritten") {
|
|
5797
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
5798
|
+
notes.push(
|
|
5799
|
+
`${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
5802
|
+
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
|
|
5803
|
+
const outputs = existing?.outputs ?? [];
|
|
5804
|
+
if (outputs.length === 0) {
|
|
5805
|
+
notes.push(
|
|
5806
|
+
`this target declares no \`outputs\`, so it is left UNCACHED. Caching a run that restores nothing is the defect where nx reports a hit and the coverage directory is empty; if this module writes coverage, declare where and the target can cache.`
|
|
5807
|
+
);
|
|
5808
|
+
}
|
|
5809
|
+
const siblingScripts = {};
|
|
5810
|
+
for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
|
|
5811
|
+
if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
|
|
5812
|
+
const rewritten = rewriteVitestScript(command);
|
|
5813
|
+
if (rewritten !== command) siblingScripts[name] = rewritten;
|
|
5814
|
+
}
|
|
5815
|
+
if (Object.keys(siblingScripts).length > 0) {
|
|
5816
|
+
notes.push(
|
|
5817
|
+
`rewrote ${Object.keys(siblingScripts).join(", ")} to run through sentinel: they called vitest directly, and the dependency they relied on is removed below. A watch script keeps watching, through \`-- --watch\`.`
|
|
5818
|
+
);
|
|
5819
|
+
}
|
|
5820
|
+
operations.push(
|
|
5821
|
+
manifestOperation(context.cwd, {
|
|
5822
|
+
[TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
|
|
5823
|
+
...siblingScripts
|
|
5824
|
+
})
|
|
5825
|
+
);
|
|
5826
|
+
operations.push(
|
|
5827
|
+
...nxTargetOperations({
|
|
5828
|
+
cwd: context.cwd,
|
|
5829
|
+
targets: testTarget({ configFiles: [configFile], outputs })
|
|
5830
|
+
})
|
|
5831
|
+
);
|
|
5832
|
+
const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
|
|
5833
|
+
if (owned.length > 0) {
|
|
5834
|
+
operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
|
|
5835
|
+
notes.push(
|
|
5836
|
+
`removed ${[...new Set(owned.map(([, name]) => name))].join(", ")} from this module: sentinel owns the runner now, and two copies of Vitest in one run give the plugins a different Vite than the one running them.`
|
|
5837
|
+
);
|
|
5838
|
+
}
|
|
5839
|
+
if (adoption.jestConfigs.length > 0) {
|
|
5840
|
+
notes.push(
|
|
5841
|
+
`${adoption.jestConfigs.join(", ")} is still here beside the Vitest config. It is not this step's to delete, since something may still run it, but the workspace cannot drop jest while it exists.`
|
|
5842
|
+
);
|
|
5843
|
+
}
|
|
5844
|
+
return { operations, notes };
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5848
|
+
function resolveVitest(cwd) {
|
|
5849
|
+
const fromSentinel = join30(
|
|
5850
|
+
new URL("../../../../../node_modules/.bin/vitest", import.meta.url).pathname
|
|
5851
|
+
);
|
|
5852
|
+
if (existsSync26(fromSentinel)) return { bin: fromSentinel, origin: "sentinel" };
|
|
5853
|
+
const fromModule = join30(cwd, "node_modules", ".bin", "vitest");
|
|
5854
|
+
return existsSync26(fromModule) ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
|
|
5855
|
+
}
|
|
5856
|
+
var VitestAdapter = class extends BaseAdapter {
|
|
5857
|
+
target = "test";
|
|
5858
|
+
runner = "vitest";
|
|
5859
|
+
appliesTo(_preset) {
|
|
5860
|
+
return true;
|
|
5861
|
+
}
|
|
5862
|
+
plan(context) {
|
|
5863
|
+
return plan3(context);
|
|
5864
|
+
}
|
|
5865
|
+
async status(ctx) {
|
|
5866
|
+
return Promise.resolve(readTestAdoption(ctx.cwd));
|
|
5867
|
+
}
|
|
5868
|
+
async inspect(ctx) {
|
|
5869
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
5870
|
+
return Promise.resolve({
|
|
5871
|
+
state: adoption.state,
|
|
5872
|
+
runner: adoption.runner,
|
|
5873
|
+
configFile: adoption.configFile,
|
|
5874
|
+
jestConfigs: adoption.jestConfigs,
|
|
5875
|
+
adopted: adoption.adopted,
|
|
5876
|
+
conformant: adoption.conformant,
|
|
5877
|
+
drift: adoption.drift,
|
|
5878
|
+
unreadable: adoption.unreadable
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5881
|
+
/**
|
|
5882
|
+
* Run the module's tests.
|
|
5883
|
+
*
|
|
5884
|
+
* A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
|
|
5885
|
+
* with no named target sweeps the workspace, so failing here would exit non-zero on the 107
|
|
5886
|
+
* modules still on jest, which says nothing anyone can act on.
|
|
5887
|
+
*/
|
|
5888
|
+
async run(ctx) {
|
|
5889
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
5890
|
+
if (!adoption.adopted) {
|
|
5891
|
+
this.say(
|
|
5892
|
+
adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}); run \`sentinel --init --test\` to migrate it.` : adoption.state === "no-tests" ? `this module has no tests.` : `no sentinel preset in this module's Vitest config; run \`sentinel --init --test\` to adopt.`
|
|
5893
|
+
);
|
|
5894
|
+
return Promise.resolve({ ok: true, code: 0 });
|
|
5895
|
+
}
|
|
5896
|
+
const { bin, origin } = resolveVitest(ctx.cwd);
|
|
5897
|
+
if (bin === void 0) {
|
|
5898
|
+
this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
|
|
5899
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
5900
|
+
}
|
|
5901
|
+
if (origin === "module") {
|
|
5902
|
+
this.say(
|
|
5903
|
+
`using the MODULE's vitest, not sentinel's. An adopted config gets its plugins from sentinel, and two copies fail in ways that never mention a version. Remove vitest from this module's package.json.`
|
|
5904
|
+
);
|
|
5905
|
+
}
|
|
5906
|
+
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
|
|
5907
|
+
const watching = options.includes("--watch") || options.includes("-w");
|
|
5908
|
+
const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
|
|
5909
|
+
const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
|
|
5910
|
+
if (result.error) {
|
|
5911
|
+
this.say(`could not run vitest (${result.error.message})`);
|
|
5912
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
5913
|
+
}
|
|
5914
|
+
const code = result.status ?? 1;
|
|
5915
|
+
return Promise.resolve({ ok: code === 0, code });
|
|
5916
|
+
}
|
|
5917
|
+
say(message) {
|
|
5918
|
+
process.stderr.write(`sentinel test(vitest): ${message}
|
|
5919
|
+
`);
|
|
5920
|
+
}
|
|
5921
|
+
};
|
|
5922
|
+
|
|
5923
|
+
// src/roles/test/register.ts
|
|
5924
|
+
function registerTest() {
|
|
5925
|
+
register(new VitestAdapter());
|
|
5926
|
+
setDefaultRunner("test", "vitest");
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5929
|
+
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
5930
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
5931
|
+
import { existsSync as existsSync27, readFileSync as readFileSync25 } from "fs";
|
|
5163
5932
|
import { createRequire as createRequire4 } from "module";
|
|
5164
|
-
import { join as
|
|
5933
|
+
import { join as join32 } from "path";
|
|
5165
5934
|
|
|
5166
5935
|
// src/roles/typescript/presets/nest.json
|
|
5167
5936
|
var nest_default2 = {
|
|
@@ -5342,8 +6111,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
5342
6111
|
}
|
|
5343
6112
|
|
|
5344
6113
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
5345
|
-
import { readFileSync as
|
|
5346
|
-
import { join as
|
|
6114
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
6115
|
+
import { join as join31 } from "path";
|
|
5347
6116
|
|
|
5348
6117
|
// src/roles/typescript/typecheck-script.ts
|
|
5349
6118
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -5436,7 +6205,7 @@ function planAdoption(context) {
|
|
|
5436
6205
|
};
|
|
5437
6206
|
}
|
|
5438
6207
|
const existing = parseJsonc(
|
|
5439
|
-
|
|
6208
|
+
readFileSync24(join31(context.cwd, target.path), "utf8"),
|
|
5440
6209
|
target.path
|
|
5441
6210
|
);
|
|
5442
6211
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -5574,7 +6343,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5574
6343
|
let chain;
|
|
5575
6344
|
try {
|
|
5576
6345
|
const parsed = parseJsonc(
|
|
5577
|
-
|
|
6346
|
+
readFileSync25(join32(cwd, target.path), "utf8"),
|
|
5578
6347
|
target.path
|
|
5579
6348
|
);
|
|
5580
6349
|
chain = parsed.extends;
|
|
@@ -5587,7 +6356,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5587
6356
|
);
|
|
5588
6357
|
if (preset === void 0) return void 0;
|
|
5589
6358
|
try {
|
|
5590
|
-
createRequire4(
|
|
6359
|
+
createRequire4(join32(cwd, "noop.js")).resolve(preset);
|
|
5591
6360
|
return void 0;
|
|
5592
6361
|
} catch {
|
|
5593
6362
|
return preset;
|
|
@@ -5628,7 +6397,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5628
6397
|
return { ok: false, code: 1 };
|
|
5629
6398
|
}
|
|
5630
6399
|
if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
|
|
5631
|
-
const result =
|
|
6400
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
|
|
5632
6401
|
if (result.error) {
|
|
5633
6402
|
process.stderr.write(
|
|
5634
6403
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5663,7 +6432,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5663
6432
|
let worst = 0;
|
|
5664
6433
|
for (const project of this.referencedProjects(ctx.cwd, config)) {
|
|
5665
6434
|
const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
|
|
5666
|
-
const result =
|
|
6435
|
+
const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
|
|
5667
6436
|
if (result.error) {
|
|
5668
6437
|
process.stderr.write(
|
|
5669
6438
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5683,7 +6452,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5683
6452
|
referencedProjects(cwd, config) {
|
|
5684
6453
|
try {
|
|
5685
6454
|
const parsed = parseJsonc(
|
|
5686
|
-
|
|
6455
|
+
readFileSync25(join32(cwd, config), "utf8"),
|
|
5687
6456
|
config
|
|
5688
6457
|
);
|
|
5689
6458
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -5699,7 +6468,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5699
6468
|
* check.
|
|
5700
6469
|
*/
|
|
5701
6470
|
typecheckTarget(cwd) {
|
|
5702
|
-
if (
|
|
6471
|
+
if (existsSync27(join32(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
5703
6472
|
const target = resolveTsconfigTarget(cwd);
|
|
5704
6473
|
return target.reason === "none" ? null : target.path;
|
|
5705
6474
|
}
|
|
@@ -5752,7 +6521,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5752
6521
|
return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
|
|
5753
6522
|
}
|
|
5754
6523
|
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
5755
|
-
const result =
|
|
6524
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
5756
6525
|
if (result.error) {
|
|
5757
6526
|
process.stderr.write(
|
|
5758
6527
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5795,7 +6564,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5795
6564
|
* errors in the output mean the rule must be on.
|
|
5796
6565
|
*/
|
|
5797
6566
|
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
5798
|
-
const shown =
|
|
6567
|
+
const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
5799
6568
|
if (shown.status === 0 && shown.stdout) {
|
|
5800
6569
|
try {
|
|
5801
6570
|
const co = parseJsonc(
|
|
@@ -5822,6 +6591,7 @@ function registerAdapters() {
|
|
|
5822
6591
|
registerLint();
|
|
5823
6592
|
registerFormat();
|
|
5824
6593
|
registerBuild();
|
|
6594
|
+
registerTest();
|
|
5825
6595
|
}
|
|
5826
6596
|
|
|
5827
6597
|
// src/core/detect-framework.ts
|
|
@@ -5903,7 +6673,7 @@ function replaceLines(current, replacements) {
|
|
|
5903
6673
|
}
|
|
5904
6674
|
|
|
5905
6675
|
// src/core/apply-plan.ts
|
|
5906
|
-
import { existsSync as
|
|
6676
|
+
import { existsSync as existsSync28, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5907
6677
|
import { resolve as resolve4, sep } from "path";
|
|
5908
6678
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
5909
6679
|
|
|
@@ -5922,7 +6692,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
5922
6692
|
return absolutePath;
|
|
5923
6693
|
}
|
|
5924
6694
|
function readIfExists(absolutePath) {
|
|
5925
|
-
return
|
|
6695
|
+
return existsSync28(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
|
|
5926
6696
|
}
|
|
5927
6697
|
function* leaves(value, prefix = []) {
|
|
5928
6698
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6003,9 +6773,9 @@ function applyOperationTo(current, operation) {
|
|
|
6003
6773
|
}
|
|
6004
6774
|
}
|
|
6005
6775
|
}
|
|
6006
|
-
function preparePlan(cwd,
|
|
6776
|
+
function preparePlan(cwd, plan4) {
|
|
6007
6777
|
const prepared = /* @__PURE__ */ new Map();
|
|
6008
|
-
for (const operation of
|
|
6778
|
+
for (const operation of plan4.operations) {
|
|
6009
6779
|
const absolutePath = resolveWithinRoot(cwd, operation.path);
|
|
6010
6780
|
const existing = prepared.get(operation.path);
|
|
6011
6781
|
const before = existing?.before ?? readIfExists(absolutePath) ?? "";
|
|
@@ -6024,14 +6794,14 @@ function preparePlan(cwd, plan3) {
|
|
|
6024
6794
|
}
|
|
6025
6795
|
function writeFileAtomic(absolutePath, contents) {
|
|
6026
6796
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
6027
|
-
|
|
6797
|
+
writeFileSync4(tempPath, contents);
|
|
6028
6798
|
renameSync(tempPath, absolutePath);
|
|
6029
6799
|
}
|
|
6030
|
-
function applyPlan(cwd,
|
|
6031
|
-
const changed = preparePlan(cwd,
|
|
6800
|
+
function applyPlan(cwd, plan4) {
|
|
6801
|
+
const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
|
|
6032
6802
|
for (const file of changed) {
|
|
6033
6803
|
if (file.deleted) {
|
|
6034
|
-
|
|
6804
|
+
rmSync3(file.absolutePath, { force: true });
|
|
6035
6805
|
continue;
|
|
6036
6806
|
}
|
|
6037
6807
|
writeFileAtomic(file.absolutePath, file.after);
|
|
@@ -6040,8 +6810,8 @@ function applyPlan(cwd, plan3) {
|
|
|
6040
6810
|
}
|
|
6041
6811
|
|
|
6042
6812
|
// src/core/config/preset-evidence.ts
|
|
6043
|
-
import { existsSync as
|
|
6044
|
-
import { join as
|
|
6813
|
+
import { existsSync as existsSync29, readdirSync as readdirSync4, readFileSync as readFileSync27 } from "fs";
|
|
6814
|
+
import { join as join33 } from "path";
|
|
6045
6815
|
var PATH_SIGNALS = [
|
|
6046
6816
|
{
|
|
6047
6817
|
preset: "nest",
|
|
@@ -6055,11 +6825,11 @@ var DEPENDENCY_SIGNALS = [
|
|
|
6055
6825
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
6056
6826
|
];
|
|
6057
6827
|
function dependencyNames(cwd) {
|
|
6058
|
-
const path =
|
|
6059
|
-
if (!
|
|
6828
|
+
const path = join33(cwd, "package.json");
|
|
6829
|
+
if (!existsSync29(path)) return [];
|
|
6060
6830
|
try {
|
|
6061
6831
|
const manifest = parseJsonc(
|
|
6062
|
-
|
|
6832
|
+
readFileSync27(path, "utf8"),
|
|
6063
6833
|
path
|
|
6064
6834
|
);
|
|
6065
6835
|
return [
|
|
@@ -6073,7 +6843,7 @@ function dependencyNames(cwd) {
|
|
|
6073
6843
|
function declaresJsx(cwd) {
|
|
6074
6844
|
let entries;
|
|
6075
6845
|
try {
|
|
6076
|
-
entries =
|
|
6846
|
+
entries = readdirSync4(cwd).filter(
|
|
6077
6847
|
(name) => name.startsWith("tsconfig") && name.endsWith(".json")
|
|
6078
6848
|
);
|
|
6079
6849
|
} catch {
|
|
@@ -6082,7 +6852,7 @@ function declaresJsx(cwd) {
|
|
|
6082
6852
|
for (const name of entries) {
|
|
6083
6853
|
try {
|
|
6084
6854
|
const config = parseJsonc(
|
|
6085
|
-
|
|
6855
|
+
readFileSync27(join33(cwd, name), "utf8"),
|
|
6086
6856
|
name
|
|
6087
6857
|
);
|
|
6088
6858
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -6133,14 +6903,14 @@ function resolveFlavour(opts) {
|
|
|
6133
6903
|
}
|
|
6134
6904
|
return detection.preset;
|
|
6135
6905
|
}
|
|
6136
|
-
function previewPlan(opts,
|
|
6137
|
-
const changed = preparePlan(opts.cwd,
|
|
6906
|
+
function previewPlan(opts, plan4) {
|
|
6907
|
+
const changed = preparePlan(opts.cwd, plan4).filter((file) => file.before !== file.after);
|
|
6138
6908
|
if (opts.json) {
|
|
6139
6909
|
process.stdout.write(
|
|
6140
6910
|
JSON.stringify(
|
|
6141
6911
|
{
|
|
6142
6912
|
dryRun: true,
|
|
6143
|
-
notes:
|
|
6913
|
+
notes: plan4.notes ?? [],
|
|
6144
6914
|
files: changed.map(({ path, before, after, deleted }) => ({
|
|
6145
6915
|
path,
|
|
6146
6916
|
action: deleted ? "delete" : before.length === 0 ? "create" : "update",
|
|
@@ -6155,7 +6925,7 @@ function previewPlan(opts, plan3) {
|
|
|
6155
6925
|
return 0;
|
|
6156
6926
|
}
|
|
6157
6927
|
process.stderr.write(" dry run: no files written\n");
|
|
6158
|
-
for (const note of
|
|
6928
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6159
6929
|
`);
|
|
6160
6930
|
if (changed.length === 0) {
|
|
6161
6931
|
process.stderr.write(" nothing to change\n");
|
|
@@ -6199,25 +6969,25 @@ async function dispatch(opts) {
|
|
|
6199
6969
|
}
|
|
6200
6970
|
}
|
|
6201
6971
|
const context = { cwd: opts.cwd, preset: effective };
|
|
6202
|
-
const
|
|
6203
|
-
if (
|
|
6204
|
-
process.stderr.write(`sentinel (${opts.target}): ${
|
|
6972
|
+
const plan4 = await adapter.plan(context);
|
|
6973
|
+
if (plan4.blocked) {
|
|
6974
|
+
process.stderr.write(`sentinel (${opts.target}): ${plan4.blocked}
|
|
6205
6975
|
`);
|
|
6206
6976
|
return 1;
|
|
6207
6977
|
}
|
|
6208
|
-
if (
|
|
6209
|
-
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${
|
|
6978
|
+
if (plan4.skipped) {
|
|
6979
|
+
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan4.skipped}`)}
|
|
6210
6980
|
`);
|
|
6211
6981
|
return 0;
|
|
6212
6982
|
}
|
|
6213
6983
|
if (opts.dryRun) {
|
|
6214
|
-
return previewPlan(opts,
|
|
6984
|
+
return previewPlan(opts, plan4);
|
|
6215
6985
|
}
|
|
6216
|
-
for (const change of applyPlan(opts.cwd,
|
|
6986
|
+
for (const change of applyPlan(opts.cwd, plan4)) {
|
|
6217
6987
|
process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
|
|
6218
6988
|
`);
|
|
6219
6989
|
}
|
|
6220
|
-
for (const note of
|
|
6990
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6221
6991
|
`);
|
|
6222
6992
|
if (adapter.afterInit) {
|
|
6223
6993
|
process.stderr.write(` fixing what ${opts.target} can fix automatically...
|
|
@@ -6241,24 +7011,25 @@ export {
|
|
|
6241
7011
|
declaredPresetFor,
|
|
6242
7012
|
availableTargets,
|
|
6243
7013
|
resolve,
|
|
6244
|
-
BaseAdapter,
|
|
6245
|
-
resolveBin,
|
|
6246
7014
|
readOwnVersion,
|
|
6247
7015
|
readProjectPackageJson,
|
|
6248
7016
|
moduleName,
|
|
7017
|
+
buildConfigFile,
|
|
7018
|
+
BaseAdapter,
|
|
7019
|
+
resolveBin,
|
|
7020
|
+
WORKSPACE_ROOT_MARKER,
|
|
7021
|
+
findWorkspaceRoot,
|
|
7022
|
+
ensureWorkspacePrep,
|
|
7023
|
+
inspectWorkspacePrep,
|
|
6249
7024
|
VERBS,
|
|
6250
7025
|
TARGETS,
|
|
6251
7026
|
LONG_RUNNING_TARGETS,
|
|
6252
7027
|
SWEEPABLE_TARGETS,
|
|
6253
7028
|
PRESET_NAMES,
|
|
6254
|
-
WORKSPACE_ROOT_MARKER,
|
|
6255
|
-
findWorkspaceRoot,
|
|
6256
|
-
ensureWorkspacePrep,
|
|
6257
|
-
inspectWorkspacePrep,
|
|
6258
7029
|
palette,
|
|
6259
7030
|
registerAdapters,
|
|
6260
7031
|
describeFramework,
|
|
6261
7032
|
detectFramework,
|
|
6262
7033
|
dispatch
|
|
6263
7034
|
};
|
|
6264
|
-
//# sourceMappingURL=chunk-
|
|
7035
|
+
//# sourceMappingURL=chunk-37Y7RALA.js.map
|