@hublo/sentinel 1.2.0-alpha.3 → 1.2.0-alpha.31
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-PLZ45MWU.js} +1545 -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,27 @@ 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, semi) {
|
|
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(", ");
|
|
1042
|
+
const end = semi ? ";" : "";
|
|
617
1043
|
const values = bindings.filter((b) => !b.typeOnly);
|
|
618
1044
|
const types = bindings.filter((b) => b.typeOnly);
|
|
619
1045
|
const lines = [];
|
|
620
|
-
if (values.length > 0) lines.push(`import { ${render(values)} } from '${
|
|
1046
|
+
if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'${end}`);
|
|
621
1047
|
if (types.length > 0) {
|
|
622
|
-
lines.push(`import type { ${render(types)} } from '${
|
|
1048
|
+
lines.push(`import type { ${render(types)} } from '${presetSpecifier}'${end}`);
|
|
623
1049
|
}
|
|
624
1050
|
return lines.join("\n");
|
|
625
1051
|
}
|
|
626
|
-
function
|
|
1052
|
+
function rewriteToolchainImports(toolchain, source, fileName) {
|
|
1053
|
+
const moved = movedSpecifiers(toolchain);
|
|
1054
|
+
const defaults = defaultExportNames(toolchain);
|
|
627
1055
|
const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
|
|
628
1056
|
if (errors.length > 0) {
|
|
629
1057
|
return {
|
|
@@ -634,10 +1062,10 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
634
1062
|
const imports = program.body.filter(
|
|
635
1063
|
(node) => node.type === "ImportDeclaration"
|
|
636
1064
|
);
|
|
637
|
-
if (imports.some((node) => node.source.value ===
|
|
1065
|
+
if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
|
|
638
1066
|
return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
|
|
639
1067
|
}
|
|
640
|
-
const moving = imports.filter((node) =>
|
|
1068
|
+
const moving = imports.filter((node) => moved.has(node.source.value));
|
|
641
1069
|
if (moving.length === 0) {
|
|
642
1070
|
return {
|
|
643
1071
|
kind: "unchanged",
|
|
@@ -654,7 +1082,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
654
1082
|
for (const spec of specifiers) {
|
|
655
1083
|
const typeOnly = node.importKind === "type" || spec.importKind === "type";
|
|
656
1084
|
if (spec.type === "ImportDefaultSpecifier") {
|
|
657
|
-
const exported =
|
|
1085
|
+
const exported = defaults[from];
|
|
658
1086
|
if (!exported) {
|
|
659
1087
|
return {
|
|
660
1088
|
kind: "blocked",
|
|
@@ -686,7 +1114,8 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
686
1114
|
const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
|
|
687
1115
|
out = out.slice(0, span.start) + out.slice(span.end + after);
|
|
688
1116
|
}
|
|
689
|
-
|
|
1117
|
+
const semi = source.slice(first.start, first.end).trimEnd().endsWith(";");
|
|
1118
|
+
out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier, semi) + out.slice(first.end);
|
|
690
1119
|
return {
|
|
691
1120
|
kind: "rewritten",
|
|
692
1121
|
source: out,
|
|
@@ -694,7 +1123,19 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
|
694
1123
|
};
|
|
695
1124
|
}
|
|
696
1125
|
|
|
1126
|
+
// src/roles/build/rewrite-imports.ts
|
|
1127
|
+
var BUILD_TOOLCHAIN = {
|
|
1128
|
+
presetSpecifier: BUILD_PRESET_SPECIFIER,
|
|
1129
|
+
owned: OWNED_PACKAGES
|
|
1130
|
+
};
|
|
1131
|
+
function rewriteBuildImports(source, fileName = "vite.config.ts") {
|
|
1132
|
+
return rewriteToolchainImports(BUILD_TOOLCHAIN, source, fileName);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
697
1135
|
// src/roles/build/plan.ts
|
|
1136
|
+
function candidateOutputs() {
|
|
1137
|
+
return BUILD_OUTPUT_CANDIDATES.map((name) => `{projectRoot}/${name}`);
|
|
1138
|
+
}
|
|
698
1139
|
function ownedBuildDependencies(cwd) {
|
|
699
1140
|
const manifest = readProjectPackageJson(cwd);
|
|
700
1141
|
const keys = [];
|
|
@@ -726,6 +1167,8 @@ function buildScripts(cwd) {
|
|
|
726
1167
|
const own2 = moduleName(cwd);
|
|
727
1168
|
scripts[DEV_SCRIPT_NAME] = composeDevScript(
|
|
728
1169
|
existingDev,
|
|
1170
|
+
// No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
|
|
1171
|
+
// sides produced `-- --`, handing Vite an argument nobody wrote.
|
|
729
1172
|
`${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
|
|
730
1173
|
);
|
|
731
1174
|
}
|
|
@@ -740,54 +1183,73 @@ function plan(context) {
|
|
|
740
1183
|
if (unmet.length > 0) {
|
|
741
1184
|
return { operations: [], skipped: describeUnmet(context.preset, unmet) };
|
|
742
1185
|
}
|
|
743
|
-
|
|
1186
|
+
const declaredPreset2 = declaredBuildPreset(context.cwd);
|
|
1187
|
+
const configFile = buildConfigFile(context.cwd);
|
|
1188
|
+
if (declaredPreset2 === void 0 && configFile !== void 0) {
|
|
1189
|
+
return {
|
|
1190
|
+
operations: [],
|
|
1191
|
+
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.`
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
const webpackTarget = webpackBuildTarget(context.cwd);
|
|
1195
|
+
if (declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0) {
|
|
1196
|
+
return planNestService(context, webpackTarget);
|
|
1197
|
+
}
|
|
1198
|
+
if (configFile === void 0) {
|
|
744
1199
|
return {
|
|
745
1200
|
operations: [],
|
|
746
|
-
skipped: `this module
|
|
1201
|
+
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
1202
|
};
|
|
748
1203
|
}
|
|
1204
|
+
return planReactApp(context, configFile);
|
|
1205
|
+
}
|
|
1206
|
+
function planReactApp(context, configFile) {
|
|
749
1207
|
const notes = [];
|
|
750
1208
|
const operations = [];
|
|
751
|
-
const
|
|
752
|
-
if (
|
|
1209
|
+
const esm = esmReadiness(configFile, readProjectPackageJson(context.cwd).type);
|
|
1210
|
+
if (esm.kind === "blocked") return { operations: [], skipped: esm.why };
|
|
1211
|
+
const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
|
|
1212
|
+
if (rewrite.kind === "blocked") {
|
|
753
1213
|
return {
|
|
754
1214
|
operations: [],
|
|
755
|
-
skipped:
|
|
1215
|
+
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
1216
|
};
|
|
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") {
|
|
1217
|
+
}
|
|
1218
|
+
if (rewrite.kind === "rewritten") {
|
|
1219
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
1220
|
+
}
|
|
1221
|
+
for (const name of COMPANION_CONFIG_FILES) {
|
|
1222
|
+
const source = readConfigSource(context.cwd, name);
|
|
1223
|
+
if (source === "") continue;
|
|
1224
|
+
const companion = rewriteBuildImports(source, name);
|
|
1225
|
+
if (companion.kind === "rewritten") {
|
|
1226
|
+
operations.push({ kind: "write", path: name, contents: companion.source });
|
|
1227
|
+
notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
|
|
1228
|
+
} else if (companion.kind === "blocked") {
|
|
784
1229
|
notes.push(
|
|
785
|
-
`${
|
|
1230
|
+
`${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
1231
|
);
|
|
787
1232
|
}
|
|
788
1233
|
}
|
|
789
|
-
|
|
790
|
-
|
|
1234
|
+
if (rewrite.kind === "rewritten") {
|
|
1235
|
+
notes.push(
|
|
1236
|
+
`${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.`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
const scripts = buildScripts(context.cwd);
|
|
1240
|
+
operations.push(manifestOperation(context.cwd, scripts));
|
|
1241
|
+
operations.push(
|
|
1242
|
+
...nxTargetOperations({
|
|
1243
|
+
cwd: context.cwd,
|
|
1244
|
+
targets: buildTarget({
|
|
1245
|
+
moduleName: adoptedPackageName(context.cwd),
|
|
1246
|
+
outputs: candidateOutputs()
|
|
1247
|
+
})
|
|
1248
|
+
})
|
|
1249
|
+
);
|
|
1250
|
+
const dev = devTargetOperations(context.cwd, scripts[DEV_SCRIPT_NAME]);
|
|
1251
|
+
operations.push(...dev.operations);
|
|
1252
|
+
if (dev.note !== void 0) notes.push(dev.note);
|
|
791
1253
|
const owned = ownedBuildDependencies(context.cwd);
|
|
792
1254
|
if (owned.length > 0) {
|
|
793
1255
|
operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
|
|
@@ -795,12 +1257,53 @@ function plan(context) {
|
|
|
795
1257
|
`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
1258
|
);
|
|
797
1259
|
}
|
|
1260
|
+
const formatted = readFormatAdoption(context.cwd).adopted;
|
|
1261
|
+
notes.push(
|
|
1262
|
+
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.`
|
|
1263
|
+
);
|
|
1264
|
+
notes.push(
|
|
1265
|
+
`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.`
|
|
1266
|
+
);
|
|
798
1267
|
return { operations, notes };
|
|
799
1268
|
}
|
|
1269
|
+
function planNestService(context, webpackTarget) {
|
|
1270
|
+
const outputs = webpackTarget === void 0 ? nestOutputsFromConfig(context.cwd) : nestBuildOutputs(webpackTarget);
|
|
1271
|
+
if (outputs === void 0) {
|
|
1272
|
+
return {
|
|
1273
|
+
operations: [],
|
|
1274
|
+
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.`
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
const metadata = [
|
|
1278
|
+
manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND }),
|
|
1279
|
+
...nxTargetOperations({
|
|
1280
|
+
cwd: context.cwd,
|
|
1281
|
+
targets: buildTarget({
|
|
1282
|
+
moduleName: adoptedPackageName(context.cwd),
|
|
1283
|
+
configFiles: [NEST_CONFIG_FILE],
|
|
1284
|
+
outputs
|
|
1285
|
+
})
|
|
1286
|
+
})
|
|
1287
|
+
];
|
|
1288
|
+
if (webpackTarget === void 0) return { operations: metadata };
|
|
1289
|
+
const refusal = refusalFor(context.cwd, webpackTarget);
|
|
1290
|
+
if (refusal !== void 0) return { operations: [], blocked: refusal };
|
|
1291
|
+
const dropped = Object.keys(webpackTarget.configurations ?? {});
|
|
1292
|
+
return {
|
|
1293
|
+
operations: [...nestAdoptionOperations(context.cwd, webpackTarget), ...metadata],
|
|
1294
|
+
notes: [
|
|
1295
|
+
`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.`,
|
|
1296
|
+
...dropped.length > 0 ? [
|
|
1297
|
+
`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.`
|
|
1298
|
+
] : [],
|
|
1299
|
+
`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.`
|
|
1300
|
+
]
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
800
1303
|
function readConfigSource(cwd, configFile) {
|
|
801
|
-
if (!
|
|
1304
|
+
if (!existsSync11(join11(cwd, configFile))) return "";
|
|
802
1305
|
try {
|
|
803
|
-
return
|
|
1306
|
+
return readFileSync9(join11(cwd, configFile), "utf8");
|
|
804
1307
|
} catch {
|
|
805
1308
|
return "";
|
|
806
1309
|
}
|
|
@@ -840,11 +1343,17 @@ var ViteRoleAdapter = class extends BaseAdapter {
|
|
|
840
1343
|
* resolution let it run at all — filtering here would give "no adapter for preset svelte",
|
|
841
1344
|
* which tells nobody what to do about it.
|
|
842
1345
|
*
|
|
843
|
-
* Nest
|
|
844
|
-
*
|
|
1346
|
+
* Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
|
|
1347
|
+
* grounds that "there is nothing to bundle", which was never true: those 38 services bundle
|
|
1348
|
+
* with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
|
|
1349
|
+
* the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
|
|
1350
|
+
* migration generator refuses every one of them because they use `@nx/js:node`
|
|
1351
|
+
* (nrwl/nx#36389). So the way out runs through here.
|
|
1352
|
+
*
|
|
1353
|
+
* `node` and `tools` remain genuinely declined: nothing under them bundles at all.
|
|
845
1354
|
*/
|
|
846
1355
|
appliesTo(preset) {
|
|
847
|
-
return preset === "react" || preset === "svelte";
|
|
1356
|
+
return preset === "react" || preset === "svelte" || preset === "nest";
|
|
848
1357
|
}
|
|
849
1358
|
/**
|
|
850
1359
|
* `react`, read from the module's Vite config rather than from its dependencies.
|
|
@@ -935,40 +1444,252 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
|
|
|
935
1444
|
return "dev";
|
|
936
1445
|
}
|
|
937
1446
|
/**
|
|
938
|
-
*
|
|
1447
|
+
* `--init --dev` does the same thing as `--init --build`, and now says so.
|
|
1448
|
+
*
|
|
1449
|
+
* The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
|
|
1450
|
+
* because adopting one without the other leaves the module half-migrated. What was missing
|
|
1451
|
+
* was only the telling: the run printed a message about the build and nothing about `serve`,
|
|
1452
|
+
* so a developer could believe the dev server had been adopted on its own. Refusing was the
|
|
1453
|
+
* wrong correction, it added friction to a command that already produced the right result.
|
|
1454
|
+
*/
|
|
1455
|
+
plan(context) {
|
|
1456
|
+
if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
|
|
1457
|
+
return {
|
|
1458
|
+
operations: [],
|
|
1459
|
+
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.`
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
const planned = super.plan(context);
|
|
1463
|
+
if (planned.operations.length === 0) return planned;
|
|
1464
|
+
return {
|
|
1465
|
+
...planned,
|
|
1466
|
+
notes: [
|
|
1467
|
+
...planned.notes ?? [],
|
|
1468
|
+
"--init --dev adopts the build too: one plan writes both `build` and `serve`."
|
|
1469
|
+
]
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Start the dev server. It does not return until stopped, so there is no verdict to report
|
|
1474
|
+
* beyond the exit code the developer's own Ctrl-C produces.
|
|
939
1475
|
*
|
|
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`).
|
|
1476
|
+
* No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
|
|
1477
|
+
* is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
|
|
1478
|
+
* running it again would race the watcher that is about to own the file.
|
|
946
1479
|
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
1480
|
+
* No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
|
|
1481
|
+
* bare form is the one every version accepts.
|
|
949
1482
|
*/
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1483
|
+
async run(ctx) {
|
|
1484
|
+
const ready = this.prepare(ctx);
|
|
1485
|
+
if (ready.kind === "stop") return ready.result;
|
|
1486
|
+
return this.spawnVite(ctx, ready.vite, [], 0);
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
// src/roles/build/declared-outputs.ts
|
|
1491
|
+
import { existsSync as existsSync13, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1492
|
+
import { join as join13 } from "path";
|
|
1493
|
+
|
|
1494
|
+
// src/core/workspace-prep.ts
|
|
1495
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync } from "fs";
|
|
1496
|
+
import { dirname as dirname3, join as join12, relative as relative2 } from "path";
|
|
1497
|
+
|
|
1498
|
+
// src/core/settings.ts
|
|
1499
|
+
var WORKSPACE_ROOT_MARKER = "nx.json";
|
|
1500
|
+
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
1501
|
+
|
|
1502
|
+
// src/core/workspace-prep.ts
|
|
1503
|
+
var OVERRIDE_KEY = "i18next>typescript";
|
|
1504
|
+
var NATIVE_TS_ALIAS = "@typescript/native";
|
|
1505
|
+
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
1506
|
+
var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
|
|
1507
|
+
var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
|
|
1508
|
+
function findWorkspaceRoot(startDir) {
|
|
1509
|
+
let dir = startDir;
|
|
1510
|
+
for (; ; ) {
|
|
1511
|
+
if (existsSync12(join12(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
1512
|
+
const parent = dirname3(dir);
|
|
1513
|
+
if (parent === dir) return void 0;
|
|
1514
|
+
dir = parent;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function declaredNativeTs(pkg) {
|
|
1518
|
+
const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
|
|
1519
|
+
if (!spec) return void 0;
|
|
1520
|
+
const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
|
|
1521
|
+
return version || void 0;
|
|
1522
|
+
}
|
|
1523
|
+
function ensureI18nextSingleton(root, dryRun) {
|
|
1524
|
+
const pkgPath = join12(root, "package.json");
|
|
1525
|
+
if (!existsSync12(pkgPath)) return void 0;
|
|
1526
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
1527
|
+
const want = declaredNativeTs(pkg);
|
|
1528
|
+
if (!want) return void 0;
|
|
1529
|
+
const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
1530
|
+
if (have === want) return void 0;
|
|
1531
|
+
if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
|
|
1532
|
+
pkg.pnpm ??= {};
|
|
1533
|
+
pkg.pnpm.overrides ??= {};
|
|
1534
|
+
pkg.pnpm.overrides[OVERRIDE_KEY] = want;
|
|
1535
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1536
|
+
return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
|
|
1537
|
+
}
|
|
1538
|
+
function ensureReleaseAgeAllowList(root, dryRun) {
|
|
1539
|
+
const yamlPath = join12(root, WORKSPACE_YAML);
|
|
1540
|
+
if (!existsSync12(yamlPath)) return void 0;
|
|
1541
|
+
const own = readOwnPackage().name;
|
|
1542
|
+
const lines = readFileSync10(yamlPath, "utf8").split("\n");
|
|
1543
|
+
const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
|
|
1544
|
+
if (keyIdx === -1) return void 0;
|
|
1545
|
+
let lastItemIdx = keyIdx;
|
|
1546
|
+
let indent = " ";
|
|
1547
|
+
for (let i = keyIdx + 1; i < lines.length; i++) {
|
|
1548
|
+
const match = lines[i]?.match(LIST_ITEM);
|
|
1549
|
+
if (!match) break;
|
|
1550
|
+
indent = match[1] ?? indent;
|
|
1551
|
+
lastItemIdx = i;
|
|
1552
|
+
if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
|
|
1553
|
+
}
|
|
1554
|
+
if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
|
|
1555
|
+
lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
|
|
1556
|
+
writeFileSync(yamlPath, lines.join("\n"));
|
|
1557
|
+
return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
|
|
1558
|
+
}
|
|
1559
|
+
var PRETTIER_IGNORE = ".prettierignore";
|
|
1560
|
+
var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
|
|
1561
|
+
var ROOT_PRETTIER_CONFIGS = [
|
|
1562
|
+
".prettierrc",
|
|
1563
|
+
".prettierrc.json",
|
|
1564
|
+
".prettierrc.json5",
|
|
1565
|
+
".prettierrc.yml",
|
|
1566
|
+
".prettierrc.yaml",
|
|
1567
|
+
".prettierrc.js",
|
|
1568
|
+
".prettierrc.cjs",
|
|
1569
|
+
".prettierrc.mjs",
|
|
1570
|
+
"prettier.config.js",
|
|
1571
|
+
"prettier.config.cjs",
|
|
1572
|
+
"prettier.config.mjs"
|
|
1573
|
+
];
|
|
1574
|
+
function ensureFormatterExclusion(root, moduleDir, dryRun) {
|
|
1575
|
+
const rel = relative2(root, moduleDir).replaceAll("\\", "/");
|
|
1576
|
+
if (rel === "" || rel.startsWith("..")) return void 0;
|
|
1577
|
+
const ignorePath = join12(root, PRETTIER_IGNORE);
|
|
1578
|
+
const hasPrettier = existsSync12(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync12(join12(root, name)));
|
|
1579
|
+
if (!hasPrettier) return void 0;
|
|
1580
|
+
const pattern = `/${rel}/`;
|
|
1581
|
+
const existing = existsSync12(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
|
|
1582
|
+
const lines = existing.split("\n");
|
|
1583
|
+
if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
|
|
1584
|
+
return void 0;
|
|
1585
|
+
}
|
|
1586
|
+
if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
|
|
1587
|
+
const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
|
|
1588
|
+
if (headerIdx === -1) {
|
|
1589
|
+
const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
|
|
1590
|
+
|
|
1591
|
+
`;
|
|
1592
|
+
writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
|
|
1593
|
+
${pattern}
|
|
1594
|
+
`);
|
|
1595
|
+
} else {
|
|
1596
|
+
let end = headerIdx + 1;
|
|
1597
|
+
while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
|
|
1598
|
+
const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
|
|
1599
|
+
writeFileSync(
|
|
1600
|
+
ignorePath,
|
|
1601
|
+
[...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
|
|
953
1602
|
);
|
|
954
1603
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1604
|
+
return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
|
|
1605
|
+
}
|
|
1606
|
+
function ensureWorkspacePrep(opts) {
|
|
1607
|
+
const dryRun = Boolean(opts.dryRun);
|
|
1608
|
+
return [
|
|
1609
|
+
ensureI18nextSingleton(opts.root, dryRun),
|
|
1610
|
+
ensureReleaseAgeAllowList(opts.root, dryRun),
|
|
1611
|
+
opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
|
|
1612
|
+
].filter((message) => message !== void 0);
|
|
1613
|
+
}
|
|
1614
|
+
function inspectWorkspacePrep(root) {
|
|
1615
|
+
const entries = [];
|
|
1616
|
+
let pkg = {};
|
|
1617
|
+
const pkgPath = join12(root, "package.json");
|
|
1618
|
+
if (existsSync12(pkgPath)) {
|
|
1619
|
+
try {
|
|
1620
|
+
pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
1621
|
+
} catch {
|
|
1622
|
+
pkg = {};
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
1626
|
+
if (pinned !== void 0) {
|
|
1627
|
+
const stillForks = declaredNativeTs(pkg);
|
|
1628
|
+
entries.push({
|
|
1629
|
+
rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
|
|
1630
|
+
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.`
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
const own = readOwnPackage().name;
|
|
1634
|
+
const yamlPath = join12(root, WORKSPACE_YAML);
|
|
1635
|
+
if (existsSync12(yamlPath)) {
|
|
1636
|
+
const yaml = readFileSync10(yamlPath, "utf8");
|
|
1637
|
+
const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
|
|
1638
|
+
if (listed) {
|
|
1639
|
+
const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
|
|
1640
|
+
entries.push({
|
|
1641
|
+
rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
|
|
1642
|
+
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.`
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return entries;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/roles/build/declared-outputs.ts
|
|
1650
|
+
function declaredBuildTarget(cwd) {
|
|
1651
|
+
return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
|
|
1652
|
+
}
|
|
1653
|
+
function resolveOutputToken(token, cwd) {
|
|
1654
|
+
if (token.startsWith("{projectRoot}/")) return join13(cwd, token.slice("{projectRoot}/".length));
|
|
1655
|
+
if (token.startsWith("{workspaceRoot}/")) {
|
|
1656
|
+
const root = findWorkspaceRoot(cwd);
|
|
1657
|
+
return root === void 0 ? void 0 : join13(root, token.slice("{workspaceRoot}/".length));
|
|
1658
|
+
}
|
|
1659
|
+
return join13(cwd, token);
|
|
1660
|
+
}
|
|
1661
|
+
function buildStamp(cwd) {
|
|
1662
|
+
const marker = join13(cwd, `.sentinel-build-stamp-${process.pid}`);
|
|
1663
|
+
try {
|
|
1664
|
+
writeFileSync2(marker, "");
|
|
1665
|
+
const stamp = statSync(marker).mtimeMs;
|
|
1666
|
+
rmSync(marker, { force: true });
|
|
1667
|
+
return stamp;
|
|
1668
|
+
} catch {
|
|
1669
|
+
return Date.now();
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function writtenSince(path, startedAt) {
|
|
1673
|
+
if (!existsSync13(path)) return false;
|
|
1674
|
+
try {
|
|
1675
|
+
return statSync(path).mtimeMs >= startedAt;
|
|
1676
|
+
} catch {
|
|
1677
|
+
return true;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function outputsProblem(cwd, startedAt) {
|
|
1681
|
+
const target = declaredBuildTarget(cwd);
|
|
1682
|
+
if (target === void 0) return void 0;
|
|
1683
|
+
const declared = target.outputs ?? [];
|
|
1684
|
+
if (declared.length === 0) {
|
|
1685
|
+
if (target.cache !== true) return void 0;
|
|
1686
|
+
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
1687
|
}
|
|
971
|
-
};
|
|
1688
|
+
const resolved = declared.map((token) => ({ token, path: resolveOutputToken(token, cwd) })).filter((entry) => entry.path !== void 0);
|
|
1689
|
+
if (resolved.length === 0) return void 0;
|
|
1690
|
+
if (resolved.some((entry) => writtenSince(entry.path, startedAt))) return void 0;
|
|
1691
|
+
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.`;
|
|
1692
|
+
}
|
|
972
1693
|
|
|
973
1694
|
// src/roles/build/prerequisite.ts
|
|
974
1695
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1018,7 +1739,15 @@ var ViteAdapter = class extends ViteRoleAdapter {
|
|
|
1018
1739
|
);
|
|
1019
1740
|
return { ok: false, code: prerequisite.code };
|
|
1020
1741
|
}
|
|
1021
|
-
|
|
1742
|
+
const startedAt = buildStamp(ctx.cwd);
|
|
1743
|
+
const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
|
|
1744
|
+
if (!result.ok) return result;
|
|
1745
|
+
const problem = outputsProblem(ctx.cwd, startedAt);
|
|
1746
|
+
if (problem !== void 0) {
|
|
1747
|
+
this.say(problem);
|
|
1748
|
+
return { ok: false, code: 1 };
|
|
1749
|
+
}
|
|
1750
|
+
return result;
|
|
1022
1751
|
}
|
|
1023
1752
|
/**
|
|
1024
1753
|
* What this module builds with, WITHOUT loading its config.
|
|
@@ -1090,12 +1819,12 @@ function registerBuild() {
|
|
|
1090
1819
|
|
|
1091
1820
|
// src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
|
|
1092
1821
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1093
|
-
import { existsSync as
|
|
1094
|
-
import { join as
|
|
1822
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
1823
|
+
import { join as join18 } from "path";
|
|
1095
1824
|
|
|
1096
1825
|
// src/core/config/has-source.ts
|
|
1097
1826
|
import { readdirSync } from "fs";
|
|
1098
|
-
import { extname, join as
|
|
1827
|
+
import { extname, join as join14, relative as relative3 } from "path";
|
|
1099
1828
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1100
1829
|
"node_modules",
|
|
1101
1830
|
"dist",
|
|
@@ -1146,7 +1875,7 @@ function hasSourceFiles(cwd, extensions) {
|
|
|
1146
1875
|
}
|
|
1147
1876
|
for (const entry of entries) {
|
|
1148
1877
|
if (entry.isDirectory()) {
|
|
1149
|
-
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(
|
|
1878
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join14(dir, entry.name));
|
|
1150
1879
|
continue;
|
|
1151
1880
|
}
|
|
1152
1881
|
if (wanted.has(extname(entry.name))) return true;
|
|
@@ -1167,12 +1896,12 @@ function listSourceFiles(cwd, extensions) {
|
|
|
1167
1896
|
continue;
|
|
1168
1897
|
}
|
|
1169
1898
|
for (const entry of entries) {
|
|
1170
|
-
const full =
|
|
1899
|
+
const full = join14(dir, entry.name);
|
|
1171
1900
|
if (entry.isDirectory()) {
|
|
1172
1901
|
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
|
|
1173
1902
|
continue;
|
|
1174
1903
|
}
|
|
1175
|
-
if (wanted.has(extname(entry.name))) found.push(
|
|
1904
|
+
if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
|
|
1176
1905
|
}
|
|
1177
1906
|
}
|
|
1178
1907
|
return found;
|
|
@@ -1180,207 +1909,9 @@ function listSourceFiles(cwd, extensions) {
|
|
|
1180
1909
|
|
|
1181
1910
|
// src/core/named-scope.ts
|
|
1182
1911
|
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
|
-
};
|
|
1912
|
+
function scopeFlags(paths, options = []) {
|
|
1913
|
+
if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
|
|
1914
|
+
return [TOLERATE_EMPTY_FLAG];
|
|
1384
1915
|
}
|
|
1385
1916
|
|
|
1386
1917
|
// src/roles/format/format-script.ts
|
|
@@ -1411,8 +1942,8 @@ function writesWhenRewritten(name, command) {
|
|
|
1411
1942
|
var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
|
|
1412
1943
|
|
|
1413
1944
|
// src/roles/format/inherited-ignores.ts
|
|
1414
|
-
import { existsSync as
|
|
1415
|
-
import { join as
|
|
1945
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
|
|
1946
|
+
import { join as join15 } from "path";
|
|
1416
1947
|
var ROOT_IGNORE_FILE = ".prettierignore";
|
|
1417
1948
|
function isPattern(line) {
|
|
1418
1949
|
const trimmed = line.trim();
|
|
@@ -1427,11 +1958,11 @@ function toModulePattern(pattern) {
|
|
|
1427
1958
|
}
|
|
1428
1959
|
function inheritedIgnorePatterns(workspaceRoot) {
|
|
1429
1960
|
if (!workspaceRoot) return [];
|
|
1430
|
-
const path =
|
|
1431
|
-
if (!
|
|
1961
|
+
const path = join15(workspaceRoot, ROOT_IGNORE_FILE);
|
|
1962
|
+
if (!existsSync14(path)) return [];
|
|
1432
1963
|
let contents;
|
|
1433
1964
|
try {
|
|
1434
|
-
contents =
|
|
1965
|
+
contents = readFileSync11(path, "utf8");
|
|
1435
1966
|
} catch {
|
|
1436
1967
|
return [];
|
|
1437
1968
|
}
|
|
@@ -1453,78 +1984,36 @@ function unparseableFiles(output) {
|
|
|
1453
1984
|
const location = LOCATION.exec(raw);
|
|
1454
1985
|
if (location && pending) {
|
|
1455
1986
|
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);
|
|
1987
|
+
file: location[1] ?? "",
|
|
1988
|
+
line: Number(location[2] ?? 0),
|
|
1989
|
+
message: pending
|
|
1990
|
+
});
|
|
1991
|
+
pending = void 0;
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const message = MESSAGE.exec(raw);
|
|
1995
|
+
if (message) pending = message[1];
|
|
1996
|
+
}
|
|
1997
|
+
return diagnostics;
|
|
1506
1998
|
}
|
|
1507
|
-
function
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
return
|
|
1999
|
+
function oxfmtSaid(output) {
|
|
2000
|
+
const lines = plain(output).split("\n");
|
|
2001
|
+
const diagnostic = lines.find((line) => MESSAGE.test(line));
|
|
2002
|
+
if (diagnostic) return MESSAGE.exec(diagnostic)?.[1] ?? diagnostic.trim();
|
|
2003
|
+
return [...lines].reverse().find((line) => line.trim().length > 0)?.trim() ?? "no output";
|
|
1511
2004
|
}
|
|
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;
|
|
2005
|
+
function needsSvelteCompiler(output) {
|
|
2006
|
+
return /svelte\/compiler/.test(output);
|
|
1518
2007
|
}
|
|
1519
2008
|
|
|
1520
2009
|
// src/roles/format/prettier-config.ts
|
|
1521
|
-
import { existsSync as
|
|
1522
|
-
import { join as
|
|
2010
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
2011
|
+
import { join as join17 } from "path";
|
|
1523
2012
|
|
|
1524
2013
|
// src/roles/format/resolve-oxfmt.ts
|
|
1525
|
-
import { readFileSync as
|
|
2014
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
1526
2015
|
import { createRequire as createRequire2 } from "module";
|
|
1527
|
-
import { dirname as dirname4, join as
|
|
2016
|
+
import { dirname as dirname4, join as join16 } from "path";
|
|
1528
2017
|
function resolveOxfmt(cwd) {
|
|
1529
2018
|
return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
|
|
1530
2019
|
}
|
|
@@ -1553,8 +2042,8 @@ function configSchema() {
|
|
|
1553
2042
|
function readConfigSchema() {
|
|
1554
2043
|
try {
|
|
1555
2044
|
const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
|
|
1556
|
-
const schemaPath =
|
|
1557
|
-
return JSON.parse(
|
|
2045
|
+
const schemaPath = join16(dirname4(manifest), "configuration_schema.json");
|
|
2046
|
+
return JSON.parse(readFileSync12(schemaPath, "utf8"));
|
|
1558
2047
|
} catch {
|
|
1559
2048
|
return void 0;
|
|
1560
2049
|
}
|
|
@@ -1585,14 +2074,14 @@ function toOxfmtOverrides(value) {
|
|
|
1585
2074
|
return { overrides, unresolved };
|
|
1586
2075
|
}
|
|
1587
2076
|
function readPrettierSettings(cwd) {
|
|
1588
|
-
const file = PRETTIER_CONFIG_FILES.find((name) =>
|
|
2077
|
+
const file = PRETTIER_CONFIG_FILES.find((name) => existsSync15(join17(cwd, name)));
|
|
1589
2078
|
if (!file) return { options: {}, unresolved: [] };
|
|
1590
2079
|
if (/\.(js|cjs|mjs)$/.test(file)) {
|
|
1591
2080
|
return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
|
|
1592
2081
|
}
|
|
1593
2082
|
let parsed;
|
|
1594
2083
|
try {
|
|
1595
|
-
parsed = parseJsonc(
|
|
2084
|
+
parsed = parseJsonc(readFileSync13(join17(cwd, file), "utf8"), file);
|
|
1596
2085
|
} catch {
|
|
1597
2086
|
return { options: {}, file, unresolved: [`${file} could not be parsed`] };
|
|
1598
2087
|
}
|
|
@@ -1621,71 +2110,6 @@ function readPrettierSettings(cwd) {
|
|
|
1621
2110
|
return { options, file, unresolved };
|
|
1622
2111
|
}
|
|
1623
2112
|
|
|
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
2113
|
// src/roles/format/unenforced.ts
|
|
1690
2114
|
function unenforcedFormatOptions(options) {
|
|
1691
2115
|
const gaps = [];
|
|
@@ -1768,7 +2192,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1768
2192
|
manifestOperation(context.cwd, this.formatScripts(context.cwd))
|
|
1769
2193
|
];
|
|
1770
2194
|
const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
|
|
1771
|
-
(name) =>
|
|
2195
|
+
(name) => existsSync16(join18(context.cwd, name))
|
|
1772
2196
|
);
|
|
1773
2197
|
for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
|
|
1774
2198
|
const removableDeps = this.modulePrettierDependencies(context.cwd);
|
|
@@ -1892,7 +2316,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1892
2316
|
});
|
|
1893
2317
|
const result = spawnSync3(
|
|
1894
2318
|
oxfmt,
|
|
1895
|
-
[mode, ...scopeFlags(paths), ...options, ...paths.length > 0 ? paths : ["."]],
|
|
2319
|
+
[mode, ...scopeFlags(paths, options), ...options, ...paths.length > 0 ? paths : ["."]],
|
|
1896
2320
|
{
|
|
1897
2321
|
cwd: ctx.cwd,
|
|
1898
2322
|
stdio: "inherit"
|
|
@@ -2008,7 +2432,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
2008
2432
|
modulePrettierDependencies(cwd) {
|
|
2009
2433
|
const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
|
|
2010
2434
|
try {
|
|
2011
|
-
const manifest = JSON.parse(
|
|
2435
|
+
const manifest = JSON.parse(readFileSync14(join18(cwd, "package.json"), "utf8"));
|
|
2012
2436
|
return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
|
|
2013
2437
|
} catch {
|
|
2014
2438
|
return [];
|
|
@@ -2090,8 +2514,8 @@ function registerFormat() {
|
|
|
2090
2514
|
|
|
2091
2515
|
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
2092
2516
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2093
|
-
import { existsSync as
|
|
2094
|
-
import { join as
|
|
2517
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2518
|
+
import { join as join28 } from "path";
|
|
2095
2519
|
|
|
2096
2520
|
// src/core/config/deferred-rules.ts
|
|
2097
2521
|
function deferredRuleNames(rules) {
|
|
@@ -2099,8 +2523,8 @@ function deferredRuleNames(rules) {
|
|
|
2099
2523
|
}
|
|
2100
2524
|
|
|
2101
2525
|
// src/core/fix-report.ts
|
|
2102
|
-
import { statSync } from "fs";
|
|
2103
|
-
import { join as
|
|
2526
|
+
import { statSync as statSync2 } from "fs";
|
|
2527
|
+
import { join as join19 } from "path";
|
|
2104
2528
|
var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
|
|
2105
2529
|
function readFailure(output) {
|
|
2106
2530
|
const lines = output.split("\n").map((line2) => line2.trim());
|
|
@@ -2111,7 +2535,7 @@ function readFailure(output) {
|
|
|
2111
2535
|
}
|
|
2112
2536
|
function fingerprint(path) {
|
|
2113
2537
|
try {
|
|
2114
|
-
const stat =
|
|
2538
|
+
const stat = statSync2(path);
|
|
2115
2539
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
2116
2540
|
} catch {
|
|
2117
2541
|
return void 0;
|
|
@@ -2120,7 +2544,7 @@ function fingerprint(path) {
|
|
|
2120
2544
|
function snapshotFiles(cwd, extensions) {
|
|
2121
2545
|
const snapshot = /* @__PURE__ */ new Map();
|
|
2122
2546
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2123
|
-
const mark = fingerprint(
|
|
2547
|
+
const mark = fingerprint(join19(cwd, file));
|
|
2124
2548
|
if (mark !== void 0) snapshot.set(file, mark);
|
|
2125
2549
|
}
|
|
2126
2550
|
return snapshot;
|
|
@@ -2128,7 +2552,7 @@ function snapshotFiles(cwd, extensions) {
|
|
|
2128
2552
|
function changedSince(cwd, before, extensions) {
|
|
2129
2553
|
const changed = [];
|
|
2130
2554
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2131
|
-
const now = fingerprint(
|
|
2555
|
+
const now = fingerprint(join19(cwd, file));
|
|
2132
2556
|
if (now === void 0) continue;
|
|
2133
2557
|
if (before.get(file) !== now) changed.push(file);
|
|
2134
2558
|
}
|
|
@@ -2147,8 +2571,8 @@ function describeFixOutcome(outcome, toolLabel) {
|
|
|
2147
2571
|
}
|
|
2148
2572
|
|
|
2149
2573
|
// src/roles/lint/module-baseline.ts
|
|
2150
|
-
import { existsSync as
|
|
2151
|
-
import { join as
|
|
2574
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
2575
|
+
import { join as join20 } from "path";
|
|
2152
2576
|
var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
|
|
2153
2577
|
var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
|
|
2154
2578
|
var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
|
|
@@ -2192,11 +2616,11 @@ function describeHolds(holds) {
|
|
|
2192
2616
|
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
2617
|
}
|
|
2194
2618
|
function heldRules(cwd) {
|
|
2195
|
-
const path =
|
|
2196
|
-
if (!
|
|
2619
|
+
const path = join20(cwd, LINT_BASELINE_FILE);
|
|
2620
|
+
if (!existsSync17(path)) return [];
|
|
2197
2621
|
try {
|
|
2198
2622
|
const parsed = parseJsonc(
|
|
2199
|
-
|
|
2623
|
+
readFileSync15(path, "utf8"),
|
|
2200
2624
|
LINT_BASELINE_FILE
|
|
2201
2625
|
);
|
|
2202
2626
|
return Object.keys(parsed.rules ?? {});
|
|
@@ -2627,7 +3051,8 @@ var react_lib_default = {
|
|
|
2627
3051
|
{
|
|
2628
3052
|
name: "storybook",
|
|
2629
3053
|
specifier: "eslint-plugin-storybook"
|
|
2630
|
-
}
|
|
3054
|
+
},
|
|
3055
|
+
"../plugins/hublo.js"
|
|
2631
3056
|
],
|
|
2632
3057
|
settings: null,
|
|
2633
3058
|
renames: {
|
|
@@ -2636,6 +3061,7 @@ var react_lib_default = {
|
|
|
2636
3061
|
rules: {
|
|
2637
3062
|
curly: ["error", "all"],
|
|
2638
3063
|
"for-direction": "error",
|
|
3064
|
+
"hublo/no-regex-lookbehind": "error",
|
|
2639
3065
|
"jest-dom/prefer-checked": "error",
|
|
2640
3066
|
"jest-dom/prefer-empty": "error",
|
|
2641
3067
|
"jest-dom/prefer-enabled-disabled": "error",
|
|
@@ -2987,7 +3413,7 @@ var react_lib_default = {
|
|
|
2987
3413
|
],
|
|
2988
3414
|
uncovered: {
|
|
2989
3415
|
"no-octal": "no oxlint equivalent",
|
|
2990
|
-
"no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead,
|
|
3416
|
+
"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
3417
|
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
3418
|
}
|
|
2993
3419
|
};
|
|
@@ -3051,6 +3477,7 @@ var react_default = {
|
|
|
3051
3477
|
severity: "off",
|
|
3052
3478
|
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
3479
|
},
|
|
3480
|
+
"hublo/no-regex-lookbehind": "error",
|
|
3054
3481
|
"jest/expect-expect": {
|
|
3055
3482
|
severity: "off",
|
|
3056
3483
|
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 +3964,7 @@ var react_default = {
|
|
|
3537
3964
|
uncovered: {
|
|
3538
3965
|
"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
3966
|
"no-octal": "no oxlint equivalent",
|
|
3540
|
-
"no-restricted-syntax": "no oxlint equivalent as configuration, so the
|
|
3967
|
+
"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
3968
|
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
3969
|
}
|
|
3543
3970
|
};
|
|
@@ -4019,14 +4446,14 @@ function downgradedRulesFor(preset) {
|
|
|
4019
4446
|
}
|
|
4020
4447
|
|
|
4021
4448
|
// src/roles/lint/extra-layers.ts
|
|
4022
|
-
import { existsSync as
|
|
4023
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4449
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
4450
|
+
import { isAbsolute as isAbsolute2, join as join21, resolve as resolve3 } from "path";
|
|
4024
4451
|
function ruleCount(cwd, specifier) {
|
|
4025
4452
|
const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
|
|
4026
|
-
if (!
|
|
4453
|
+
if (!existsSync18(path)) return void 0;
|
|
4027
4454
|
try {
|
|
4028
4455
|
const parsed = parseJsonc(
|
|
4029
|
-
|
|
4456
|
+
readFileSync16(path, "utf8"),
|
|
4030
4457
|
specifier
|
|
4031
4458
|
);
|
|
4032
4459
|
return Object.keys(parsed.rules ?? {}).length;
|
|
@@ -4046,7 +4473,7 @@ function extraLayers(cwd, extendsList) {
|
|
|
4046
4473
|
function committedExtendsList(cwd, configFile) {
|
|
4047
4474
|
try {
|
|
4048
4475
|
const parsed = parseJsonc(
|
|
4049
|
-
|
|
4476
|
+
readFileSync16(join21(cwd, configFile), "utf8"),
|
|
4050
4477
|
configFile
|
|
4051
4478
|
);
|
|
4052
4479
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4105,8 +4532,8 @@ function errorCountsByConfigRule(stdout) {
|
|
|
4105
4532
|
}
|
|
4106
4533
|
|
|
4107
4534
|
// src/core/config/read-adoption.ts
|
|
4108
|
-
import { existsSync as
|
|
4109
|
-
import { join as
|
|
4535
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
|
|
4536
|
+
import { join as join23 } from "path";
|
|
4110
4537
|
|
|
4111
4538
|
// src/core/config/owned-keys.ts
|
|
4112
4539
|
function presetOwnedKeys(config, permitted, presetSets) {
|
|
@@ -4121,12 +4548,12 @@ function localOnlyKeys(config, permitted, presetSets) {
|
|
|
4121
4548
|
}
|
|
4122
4549
|
|
|
4123
4550
|
// src/core/config/resolve-config-target.ts
|
|
4124
|
-
import { existsSync as
|
|
4125
|
-
import { join as
|
|
4551
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
|
|
4552
|
+
import { join as join22 } from "path";
|
|
4126
4553
|
function readExtends(absolutePath) {
|
|
4127
4554
|
let parsed;
|
|
4128
4555
|
try {
|
|
4129
|
-
parsed = parseJsonc(
|
|
4556
|
+
parsed = parseJsonc(readFileSync17(absolutePath, "utf8"), absolutePath);
|
|
4130
4557
|
} catch {
|
|
4131
4558
|
return [];
|
|
4132
4559
|
}
|
|
@@ -4140,8 +4567,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
|
|
|
4140
4567
|
let existing;
|
|
4141
4568
|
let existingExtendsSomething = false;
|
|
4142
4569
|
for (const candidate of candidates) {
|
|
4143
|
-
const absolutePath =
|
|
4144
|
-
if (!
|
|
4570
|
+
const absolutePath = join22(moduleDir, candidate);
|
|
4571
|
+
if (!existsSync19(absolutePath)) continue;
|
|
4145
4572
|
const chain = readExtends(absolutePath);
|
|
4146
4573
|
if (existing === void 0) {
|
|
4147
4574
|
existing = candidate;
|
|
@@ -4174,12 +4601,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
|
|
|
4174
4601
|
});
|
|
4175
4602
|
function readAdoption(cwd, options) {
|
|
4176
4603
|
const target = resolveConfigTarget(cwd, options);
|
|
4177
|
-
if (target.reason === "none" || !
|
|
4604
|
+
if (target.reason === "none" || !existsSync20(join23(cwd, target.path))) {
|
|
4178
4605
|
return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
|
|
4179
4606
|
}
|
|
4180
4607
|
let parsed;
|
|
4181
4608
|
try {
|
|
4182
|
-
parsed = parseJsonc(
|
|
4609
|
+
parsed = parseJsonc(readFileSync18(join23(cwd, target.path), "utf8"), target.path);
|
|
4183
4610
|
} catch (error) {
|
|
4184
4611
|
const reason = error instanceof Error ? error.message : String(error);
|
|
4185
4612
|
return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
|
|
@@ -4213,9 +4640,9 @@ function readLintAdoption(cwd) {
|
|
|
4213
4640
|
}
|
|
4214
4641
|
|
|
4215
4642
|
// src/roles/lint/resolve-oxlint.ts
|
|
4216
|
-
import { existsSync as
|
|
4643
|
+
import { existsSync as existsSync21 } from "fs";
|
|
4217
4644
|
import { createRequire as createRequire3 } from "module";
|
|
4218
|
-
import { delimiter as delimiter2, dirname as dirname5, join as
|
|
4645
|
+
import { delimiter as delimiter2, dirname as dirname5, join as join24 } from "path";
|
|
4219
4646
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4220
4647
|
var PACKAGE_OF = {
|
|
4221
4648
|
oxlint: "oxlint",
|
|
@@ -4240,30 +4667,30 @@ function tsgolintShim(cwd) {
|
|
|
4240
4667
|
for (const owner of ["oxlint-tsgolint", "oxlint"]) {
|
|
4241
4668
|
try {
|
|
4242
4669
|
const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
|
|
4243
|
-
candidates.push(
|
|
4244
|
-
candidates.push(
|
|
4670
|
+
candidates.push(join24(packageDir, "node_modules", ".bin", "tsgolint"));
|
|
4671
|
+
candidates.push(join24(packageDir, "..", ".bin", "tsgolint"));
|
|
4245
4672
|
} catch {
|
|
4246
4673
|
}
|
|
4247
4674
|
}
|
|
4248
4675
|
try {
|
|
4249
4676
|
const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
|
|
4250
|
-
candidates.push(
|
|
4677
|
+
candidates.push(join24(ownRoot, "node_modules", ".bin", "tsgolint"));
|
|
4251
4678
|
} catch {
|
|
4252
4679
|
candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
|
|
4253
4680
|
}
|
|
4254
|
-
return candidates.find((candidate) => candidate !== "" &&
|
|
4681
|
+
return candidates.find((candidate) => candidate !== "" && existsSync21(candidate));
|
|
4255
4682
|
}
|
|
4256
4683
|
function oxlintSearchPath(cwd) {
|
|
4257
4684
|
return binSearchPath(cwd);
|
|
4258
4685
|
}
|
|
4259
4686
|
|
|
4260
4687
|
// src/roles/lint/adapters/oxlint/plan.ts
|
|
4261
|
-
import { existsSync as
|
|
4262
|
-
import { join as
|
|
4688
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
|
|
4689
|
+
import { join as join27 } from "path";
|
|
4263
4690
|
|
|
4264
4691
|
// src/roles/lint/eslint-ignores.ts
|
|
4265
|
-
import { existsSync as
|
|
4266
|
-
import { join as
|
|
4692
|
+
import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
|
|
4693
|
+
import { join as join25 } from "path";
|
|
4267
4694
|
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4268
4695
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
4269
4696
|
function toOxlintPattern(pattern) {
|
|
@@ -4275,11 +4702,11 @@ function readRootEslintIgnores(root) {
|
|
|
4275
4702
|
return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
|
|
4276
4703
|
}
|
|
4277
4704
|
function readEslintIgnores(cwd) {
|
|
4278
|
-
const config = ESLINT_CONFIG_FILES.map((name) =>
|
|
4705
|
+
const config = ESLINT_CONFIG_FILES.map((name) => join25(cwd, name)).find((path) => existsSync22(path));
|
|
4279
4706
|
if (!config) return { patterns: [], unresolved: [] };
|
|
4280
4707
|
let source;
|
|
4281
4708
|
try {
|
|
4282
|
-
source =
|
|
4709
|
+
source = readFileSync19(config, "utf8");
|
|
4283
4710
|
} catch {
|
|
4284
4711
|
return { patterns: [], unresolved: [] };
|
|
4285
4712
|
}
|
|
@@ -4380,8 +4807,8 @@ function lintPresetFor(preset) {
|
|
|
4380
4807
|
}
|
|
4381
4808
|
|
|
4382
4809
|
// src/roles/lint/rename-suppressions.ts
|
|
4383
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
4384
|
-
import { join as
|
|
4810
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync20, statSync as statSync3 } from "fs";
|
|
4811
|
+
import { join as join26, relative as relative4 } from "path";
|
|
4385
4812
|
var SOURCE_EXTENSIONS = [
|
|
4386
4813
|
".ts",
|
|
4387
4814
|
".tsx",
|
|
@@ -4428,10 +4855,10 @@ function* sourceFiles(dir) {
|
|
|
4428
4855
|
return;
|
|
4429
4856
|
}
|
|
4430
4857
|
for (const entry of entries) {
|
|
4431
|
-
const full =
|
|
4858
|
+
const full = join26(dir, entry);
|
|
4432
4859
|
let isDirectory;
|
|
4433
4860
|
try {
|
|
4434
|
-
isDirectory =
|
|
4861
|
+
isDirectory = statSync3(full).isDirectory();
|
|
4435
4862
|
} catch {
|
|
4436
4863
|
continue;
|
|
4437
4864
|
}
|
|
@@ -4448,7 +4875,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4448
4875
|
for (const file of sourceFiles(cwd)) {
|
|
4449
4876
|
let content;
|
|
4450
4877
|
try {
|
|
4451
|
-
content =
|
|
4878
|
+
content = readFileSync20(file, "utf8");
|
|
4452
4879
|
} catch {
|
|
4453
4880
|
continue;
|
|
4454
4881
|
}
|
|
@@ -4472,7 +4899,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4472
4899
|
}
|
|
4473
4900
|
if (hits.length === 0) return;
|
|
4474
4901
|
found.push({
|
|
4475
|
-
file:
|
|
4902
|
+
file: relative4(cwd, file),
|
|
4476
4903
|
line: index + 1,
|
|
4477
4904
|
from: line,
|
|
4478
4905
|
to: line.replace(rules, renamed.join(", ")),
|
|
@@ -4576,7 +5003,7 @@ function plan2(context) {
|
|
|
4576
5003
|
keys: removableDeps
|
|
4577
5004
|
});
|
|
4578
5005
|
}
|
|
4579
|
-
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) =>
|
|
5006
|
+
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync23(join27(context.cwd, name)));
|
|
4580
5007
|
for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
|
|
4581
5008
|
operations.push(
|
|
4582
5009
|
...nxTargetOperations({
|
|
@@ -4674,7 +5101,7 @@ function lintScripts(cwd) {
|
|
|
4674
5101
|
function committedExtends(cwd) {
|
|
4675
5102
|
try {
|
|
4676
5103
|
const parsed = parseJsonc(
|
|
4677
|
-
|
|
5104
|
+
readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4678
5105
|
LINT_CONFIG_FILE
|
|
4679
5106
|
);
|
|
4680
5107
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4685,7 +5112,7 @@ function committedExtends(cwd) {
|
|
|
4685
5112
|
function committedIgnorePatterns(cwd) {
|
|
4686
5113
|
try {
|
|
4687
5114
|
const parsed = parseJsonc(
|
|
4688
|
-
|
|
5115
|
+
readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
4689
5116
|
LINT_CONFIG_FILE
|
|
4690
5117
|
);
|
|
4691
5118
|
return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4697,7 +5124,7 @@ function moduleEslintDependencies(cwd) {
|
|
|
4697
5124
|
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
5125
|
let manifest;
|
|
4699
5126
|
try {
|
|
4700
|
-
manifest = JSON.parse(
|
|
5127
|
+
manifest = JSON.parse(readFileSync21(join27(cwd, "package.json"), "utf8"));
|
|
4701
5128
|
} catch {
|
|
4702
5129
|
return [];
|
|
4703
5130
|
}
|
|
@@ -4831,23 +5258,23 @@ ${result.stderr ?? ""}`);
|
|
|
4831
5258
|
* build the developer can see, rather than a silent half-adoption they cannot.
|
|
4832
5259
|
*/
|
|
4833
5260
|
writeModuleBaseline(ctx, oxlint, env) {
|
|
4834
|
-
const configPath =
|
|
4835
|
-
const baselinePath =
|
|
4836
|
-
if (!
|
|
5261
|
+
const configPath = join28(ctx.cwd, LINT_CONFIG_FILE);
|
|
5262
|
+
const baselinePath = join28(ctx.cwd, LINT_BASELINE_FILE);
|
|
5263
|
+
if (!existsSync24(configPath)) return;
|
|
4837
5264
|
let config;
|
|
4838
5265
|
try {
|
|
4839
5266
|
config = parseJsonc(
|
|
4840
|
-
|
|
5267
|
+
readFileSync22(configPath, "utf8"),
|
|
4841
5268
|
LINT_CONFIG_FILE
|
|
4842
5269
|
);
|
|
4843
5270
|
} catch {
|
|
4844
5271
|
return;
|
|
4845
5272
|
}
|
|
4846
5273
|
const current = Array.isArray(config.extends) ? config.extends : [];
|
|
4847
|
-
const measurePath =
|
|
5274
|
+
const measurePath = join28(ctx.cwd, LINT_MEASURE_FILE);
|
|
4848
5275
|
let measured;
|
|
4849
5276
|
try {
|
|
4850
|
-
|
|
5277
|
+
writeFileSync3(
|
|
4851
5278
|
measurePath,
|
|
4852
5279
|
`${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
|
|
4853
5280
|
`
|
|
@@ -4865,7 +5292,7 @@ ${result.stderr ?? ""}`);
|
|
|
4865
5292
|
{ cwd: ctx.cwd, encoding: "utf8", env, maxBuffer: CAPTURE_MAX_BUFFER }
|
|
4866
5293
|
);
|
|
4867
5294
|
} finally {
|
|
4868
|
-
|
|
5295
|
+
rmSync2(measurePath, { force: true });
|
|
4869
5296
|
}
|
|
4870
5297
|
if (measured.error || !(measured.stdout ?? "").includes('"diagnostics"')) {
|
|
4871
5298
|
process.stderr.write(
|
|
@@ -4879,20 +5306,20 @@ ${result.stderr ?? ""}`);
|
|
|
4879
5306
|
const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
|
|
4880
5307
|
const next = extendsWithBaseline(current, holds.length > 0);
|
|
4881
5308
|
if (holds.length > 0) {
|
|
4882
|
-
|
|
5309
|
+
writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
|
|
4883
5310
|
process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
|
|
4884
5311
|
`);
|
|
4885
5312
|
} else {
|
|
4886
|
-
|
|
5313
|
+
rmSync2(baselinePath, { force: true });
|
|
4887
5314
|
}
|
|
4888
5315
|
if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
|
|
4889
5316
|
config.extends = next;
|
|
4890
|
-
|
|
5317
|
+
writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
|
|
4891
5318
|
`);
|
|
4892
5319
|
}
|
|
4893
5320
|
}
|
|
4894
5321
|
async run(ctx) {
|
|
4895
|
-
if (!
|
|
5322
|
+
if (!existsSync24(join28(ctx.cwd, LINT_CONFIG_FILE))) {
|
|
4896
5323
|
process.stderr.write(
|
|
4897
5324
|
`sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
|
|
4898
5325
|
`
|
|
@@ -4937,7 +5364,7 @@ ${result.stderr ?? ""}`);
|
|
|
4937
5364
|
valueFlags: OXLINT_VALUE_FLAGS
|
|
4938
5365
|
});
|
|
4939
5366
|
const targets = paths.length > 0 ? paths : ["."];
|
|
4940
|
-
const emptyScope = scopeFlags(paths);
|
|
5367
|
+
const emptyScope = scopeFlags(paths, passedOptions);
|
|
4941
5368
|
const lint = (extra) => {
|
|
4942
5369
|
const passed = [...extra, ...emptyScope, ...passedOptions];
|
|
4943
5370
|
const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
|
|
@@ -5040,7 +5467,7 @@ ${result.stderr ?? ""}`);
|
|
|
5040
5467
|
let stub;
|
|
5041
5468
|
try {
|
|
5042
5469
|
stub = parseJsonc(
|
|
5043
|
-
|
|
5470
|
+
readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5044
5471
|
LINT_CONFIG_FILE
|
|
5045
5472
|
);
|
|
5046
5473
|
} catch {
|
|
@@ -5050,7 +5477,7 @@ ${result.stderr ?? ""}`);
|
|
|
5050
5477
|
for (const entry of stub.extends ?? []) {
|
|
5051
5478
|
try {
|
|
5052
5479
|
const preset = parseJsonc(
|
|
5053
|
-
|
|
5480
|
+
readFileSync22(join28(cwd, entry), "utf8"),
|
|
5054
5481
|
entry
|
|
5055
5482
|
);
|
|
5056
5483
|
for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
|
|
@@ -5120,14 +5547,14 @@ ${result.stderr ?? ""}`);
|
|
|
5120
5547
|
let parsed;
|
|
5121
5548
|
try {
|
|
5122
5549
|
parsed = parseJsonc(
|
|
5123
|
-
|
|
5550
|
+
readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5124
5551
|
LINT_CONFIG_FILE
|
|
5125
5552
|
);
|
|
5126
5553
|
} catch {
|
|
5127
5554
|
return void 0;
|
|
5128
5555
|
}
|
|
5129
5556
|
const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
5130
|
-
return targets.find((target) => !
|
|
5557
|
+
return targets.find((target) => !existsSync24(join28(cwd, target)));
|
|
5131
5558
|
}
|
|
5132
5559
|
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
5133
5560
|
announceDisabled(preset) {
|
|
@@ -5157,11 +5584,355 @@ function registerLint() {
|
|
|
5157
5584
|
setDefaultRunner("lint", "oxlint");
|
|
5158
5585
|
}
|
|
5159
5586
|
|
|
5160
|
-
// src/roles/
|
|
5587
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5161
5588
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5162
|
-
import { existsSync as
|
|
5589
|
+
import { existsSync as existsSync26 } from "fs";
|
|
5590
|
+
import { join as join30 } from "path";
|
|
5591
|
+
|
|
5592
|
+
// src/roles/test/presets/toolchain.json
|
|
5593
|
+
var toolchain_default2 = {
|
|
5594
|
+
$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.",
|
|
5595
|
+
owned: [
|
|
5596
|
+
{
|
|
5597
|
+
package: "vitest",
|
|
5598
|
+
specifier: "vitest/config",
|
|
5599
|
+
named: ["defineConfig", "mergeConfig"],
|
|
5600
|
+
types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
|
|
5601
|
+
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."
|
|
5602
|
+
},
|
|
5603
|
+
{
|
|
5604
|
+
package: "vite",
|
|
5605
|
+
specifier: "vite",
|
|
5606
|
+
named: ["loadEnv"],
|
|
5607
|
+
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."
|
|
5608
|
+
}
|
|
5609
|
+
]
|
|
5610
|
+
};
|
|
5611
|
+
|
|
5612
|
+
// src/roles/test/preset-data.ts
|
|
5613
|
+
var OWNED_TEST_PACKAGES = toolchain_default2.owned;
|
|
5614
|
+
|
|
5615
|
+
// src/roles/test/config-policy.ts
|
|
5616
|
+
var TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
|
|
5617
|
+
var TEST_CONFIG_FILES = [
|
|
5618
|
+
"vitest.config.ts",
|
|
5619
|
+
"vitest.config.mts",
|
|
5620
|
+
"vitest.config.js",
|
|
5621
|
+
"vitest.config.mjs"
|
|
5622
|
+
];
|
|
5623
|
+
var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
|
|
5624
|
+
var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
|
|
5625
|
+
(entry) => entry.package !== "vite"
|
|
5626
|
+
).map((entry) => entry.package);
|
|
5627
|
+
var TEST_TOOLCHAIN = {
|
|
5628
|
+
presetSpecifier: TEST_PRESET_SPECIFIER,
|
|
5629
|
+
owned: TEST_OWNED_PACKAGES
|
|
5630
|
+
};
|
|
5631
|
+
var TEST_SCRIPT_NAME = "test";
|
|
5632
|
+
var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
|
|
5633
|
+
var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
|
|
5634
|
+
function rewriteVitestScript(command) {
|
|
5635
|
+
return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
|
|
5636
|
+
const args = tail.trim().split(/\s+/).filter(Boolean);
|
|
5637
|
+
const oneShot = args[0] === "run" || args.includes("--run");
|
|
5638
|
+
const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
|
|
5639
|
+
const passthrough = oneShot ? rest : ["--watch", ...rest];
|
|
5640
|
+
const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
|
|
5641
|
+
const trailing = /\s$/.test(tail) ? " " : "";
|
|
5642
|
+
return `${lead}sentinel --run --test${suffix}${trailing}`;
|
|
5643
|
+
});
|
|
5644
|
+
}
|
|
5645
|
+
function testTarget(options) {
|
|
5646
|
+
const { configFiles, outputs } = options;
|
|
5647
|
+
const cached = outputs !== void 0 && outputs.length > 0;
|
|
5648
|
+
return {
|
|
5649
|
+
[TEST_SCRIPT_NAME]: {
|
|
5650
|
+
cache: cached,
|
|
5651
|
+
inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
|
|
5652
|
+
...cached ? { outputs: [...outputs] } : {}
|
|
5653
|
+
}
|
|
5654
|
+
};
|
|
5655
|
+
}
|
|
5656
|
+
|
|
5657
|
+
// src/roles/test/read-adoption.ts
|
|
5658
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
|
|
5659
|
+
import { join as join29 } from "path";
|
|
5660
|
+
var JEST_CONFIG_FILES = [
|
|
5661
|
+
"jest.config.ts",
|
|
5662
|
+
"jest.config.js",
|
|
5663
|
+
"jest.prisma.config.ts",
|
|
5664
|
+
"jest.functional.config.ts",
|
|
5665
|
+
"jest.integration-config.ts"
|
|
5666
|
+
];
|
|
5667
|
+
function testConfigFile(cwd) {
|
|
5668
|
+
return TEST_CONFIG_FILES.find((name) => existsSync25(join29(cwd, name)));
|
|
5669
|
+
}
|
|
5670
|
+
function jestConfigFiles(cwd) {
|
|
5671
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(cwd, name)));
|
|
5672
|
+
}
|
|
5673
|
+
function rootJestConfigFiles(cwd) {
|
|
5674
|
+
const root = findWorkspaceRoot(cwd);
|
|
5675
|
+
if (root === void 0 || root === cwd) return [];
|
|
5676
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(root, name)));
|
|
5677
|
+
}
|
|
5678
|
+
var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
|
|
5679
|
+
var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
|
|
5680
|
+
function hasTestFiles(cwd) {
|
|
5681
|
+
const stack = [cwd];
|
|
5682
|
+
while (stack.length > 0) {
|
|
5683
|
+
const dir = stack.pop();
|
|
5684
|
+
let entries;
|
|
5685
|
+
try {
|
|
5686
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
5687
|
+
} catch {
|
|
5688
|
+
continue;
|
|
5689
|
+
}
|
|
5690
|
+
for (const entry of entries) {
|
|
5691
|
+
if (entry.isDirectory()) {
|
|
5692
|
+
if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
|
|
5693
|
+
} else if (TEST_FILE.test(entry.name)) {
|
|
5694
|
+
return true;
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
return false;
|
|
5699
|
+
}
|
|
5700
|
+
function readTestConfigSource(cwd, fileName) {
|
|
5701
|
+
try {
|
|
5702
|
+
return readFileSync23(join29(cwd, fileName), "utf8");
|
|
5703
|
+
} catch {
|
|
5704
|
+
return "";
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
function importsPreset2(source) {
|
|
5708
|
+
const escaped = TEST_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5709
|
+
return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
|
|
5710
|
+
}
|
|
5711
|
+
function readTestAdoption(cwd) {
|
|
5712
|
+
const manifest = readProjectPackageJson(cwd);
|
|
5713
|
+
const jestConfigs = jestConfigFiles(cwd);
|
|
5714
|
+
const configFile = testConfigFile(cwd);
|
|
5715
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
5716
|
+
const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
|
|
5717
|
+
if (configFile === void 0) {
|
|
5718
|
+
const hasTests = hasTestFiles(cwd);
|
|
5719
|
+
const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
|
|
5720
|
+
return {
|
|
5721
|
+
configFile: null,
|
|
5722
|
+
preset: null,
|
|
5723
|
+
adopted: false,
|
|
5724
|
+
conformant: false,
|
|
5725
|
+
drift: [],
|
|
5726
|
+
unreadable: null,
|
|
5727
|
+
state: hasTests ? "jest" : "no-tests",
|
|
5728
|
+
runner: hasTests ? "jest" : "none",
|
|
5729
|
+
jestConfigs,
|
|
5730
|
+
inheritsRootConfig,
|
|
5731
|
+
ownDeclarations
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
const source = readTestConfigSource(cwd, configFile);
|
|
5735
|
+
if (source === "") {
|
|
5736
|
+
return {
|
|
5737
|
+
configFile,
|
|
5738
|
+
preset: null,
|
|
5739
|
+
adopted: false,
|
|
5740
|
+
conformant: false,
|
|
5741
|
+
drift: [],
|
|
5742
|
+
unreadable: `${configFile} could not be read`,
|
|
5743
|
+
state: "vitest-own-toolchain",
|
|
5744
|
+
runner: "vitest",
|
|
5745
|
+
jestConfigs,
|
|
5746
|
+
inheritsRootConfig: false,
|
|
5747
|
+
// it has a config of its own, readable or not
|
|
5748
|
+
ownDeclarations
|
|
5749
|
+
};
|
|
5750
|
+
}
|
|
5751
|
+
const adopted = importsPreset2(source);
|
|
5752
|
+
const script = moduleScripts(cwd)["test"];
|
|
5753
|
+
return {
|
|
5754
|
+
configFile,
|
|
5755
|
+
preset: adopted ? "react" : null,
|
|
5756
|
+
adopted,
|
|
5757
|
+
conformant: adopted && script !== void 0 && jestConfigs.length === 0,
|
|
5758
|
+
drift: [],
|
|
5759
|
+
unreadable: null,
|
|
5760
|
+
state: adopted ? "adopted" : "vitest-own-toolchain",
|
|
5761
|
+
runner: "vitest",
|
|
5762
|
+
jestConfigs,
|
|
5763
|
+
inheritsRootConfig: false,
|
|
5764
|
+
// local always wins: this module is out of the root's reach
|
|
5765
|
+
ownDeclarations
|
|
5766
|
+
};
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5769
|
+
// src/roles/test/plan.ts
|
|
5770
|
+
function ownedTestDependencies(cwd, declared) {
|
|
5771
|
+
return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
|
|
5772
|
+
["dependencies", entry.name],
|
|
5773
|
+
["devDependencies", entry.name]
|
|
5774
|
+
]).filter(([section]) => section !== void 0);
|
|
5775
|
+
}
|
|
5776
|
+
function plan3(context) {
|
|
5777
|
+
const configFile = testConfigFile(context.cwd);
|
|
5778
|
+
const adoption = readTestAdoption(context.cwd);
|
|
5779
|
+
if (configFile === void 0) {
|
|
5780
|
+
return {
|
|
5781
|
+
operations: [],
|
|
5782
|
+
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.`
|
|
5783
|
+
};
|
|
5784
|
+
}
|
|
5785
|
+
const operations = [];
|
|
5786
|
+
const notes = [];
|
|
5787
|
+
const rewrite = rewriteToolchainImports(
|
|
5788
|
+
TEST_TOOLCHAIN,
|
|
5789
|
+
readTestConfigSource(context.cwd, configFile),
|
|
5790
|
+
configFile
|
|
5791
|
+
);
|
|
5792
|
+
if (rewrite.kind === "blocked") {
|
|
5793
|
+
return {
|
|
5794
|
+
operations: [],
|
|
5795
|
+
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.`
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
if (rewrite.kind === "rewritten") {
|
|
5799
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
5800
|
+
notes.push(
|
|
5801
|
+
`${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.`
|
|
5802
|
+
);
|
|
5803
|
+
}
|
|
5804
|
+
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
|
|
5805
|
+
const outputs = existing?.outputs ?? [];
|
|
5806
|
+
if (outputs.length === 0) {
|
|
5807
|
+
notes.push(
|
|
5808
|
+
`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.`
|
|
5809
|
+
);
|
|
5810
|
+
}
|
|
5811
|
+
const siblingScripts = {};
|
|
5812
|
+
for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
|
|
5813
|
+
if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
|
|
5814
|
+
const rewritten = rewriteVitestScript(command);
|
|
5815
|
+
if (rewritten !== command) siblingScripts[name] = rewritten;
|
|
5816
|
+
}
|
|
5817
|
+
if (Object.keys(siblingScripts).length > 0) {
|
|
5818
|
+
notes.push(
|
|
5819
|
+
`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\`.`
|
|
5820
|
+
);
|
|
5821
|
+
}
|
|
5822
|
+
operations.push(
|
|
5823
|
+
manifestOperation(context.cwd, {
|
|
5824
|
+
[TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
|
|
5825
|
+
...siblingScripts
|
|
5826
|
+
})
|
|
5827
|
+
);
|
|
5828
|
+
operations.push(
|
|
5829
|
+
...nxTargetOperations({
|
|
5830
|
+
cwd: context.cwd,
|
|
5831
|
+
targets: testTarget({ configFiles: [configFile], outputs })
|
|
5832
|
+
})
|
|
5833
|
+
);
|
|
5834
|
+
const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
|
|
5835
|
+
if (owned.length > 0) {
|
|
5836
|
+
operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
|
|
5837
|
+
notes.push(
|
|
5838
|
+
`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.`
|
|
5839
|
+
);
|
|
5840
|
+
}
|
|
5841
|
+
if (adoption.jestConfigs.length > 0) {
|
|
5842
|
+
notes.push(
|
|
5843
|
+
`${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.`
|
|
5844
|
+
);
|
|
5845
|
+
}
|
|
5846
|
+
return { operations, notes };
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5850
|
+
function resolveVitest(cwd) {
|
|
5851
|
+
const fromSentinel = join30(
|
|
5852
|
+
new URL("../../../../../node_modules/.bin/vitest", import.meta.url).pathname
|
|
5853
|
+
);
|
|
5854
|
+
if (existsSync26(fromSentinel)) return { bin: fromSentinel, origin: "sentinel" };
|
|
5855
|
+
const fromModule = join30(cwd, "node_modules", ".bin", "vitest");
|
|
5856
|
+
return existsSync26(fromModule) ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
|
|
5857
|
+
}
|
|
5858
|
+
var VitestAdapter = class extends BaseAdapter {
|
|
5859
|
+
target = "test";
|
|
5860
|
+
runner = "vitest";
|
|
5861
|
+
appliesTo(_preset) {
|
|
5862
|
+
return true;
|
|
5863
|
+
}
|
|
5864
|
+
plan(context) {
|
|
5865
|
+
return plan3(context);
|
|
5866
|
+
}
|
|
5867
|
+
async status(ctx) {
|
|
5868
|
+
return Promise.resolve(readTestAdoption(ctx.cwd));
|
|
5869
|
+
}
|
|
5870
|
+
async inspect(ctx) {
|
|
5871
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
5872
|
+
return Promise.resolve({
|
|
5873
|
+
state: adoption.state,
|
|
5874
|
+
runner: adoption.runner,
|
|
5875
|
+
configFile: adoption.configFile,
|
|
5876
|
+
jestConfigs: adoption.jestConfigs,
|
|
5877
|
+
adopted: adoption.adopted,
|
|
5878
|
+
conformant: adoption.conformant,
|
|
5879
|
+
drift: adoption.drift,
|
|
5880
|
+
unreadable: adoption.unreadable
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5884
|
+
* Run the module's tests.
|
|
5885
|
+
*
|
|
5886
|
+
* A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
|
|
5887
|
+
* with no named target sweeps the workspace, so failing here would exit non-zero on the 107
|
|
5888
|
+
* modules still on jest, which says nothing anyone can act on.
|
|
5889
|
+
*/
|
|
5890
|
+
async run(ctx) {
|
|
5891
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
5892
|
+
if (!adoption.adopted) {
|
|
5893
|
+
this.say(
|
|
5894
|
+
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.`
|
|
5895
|
+
);
|
|
5896
|
+
return Promise.resolve({ ok: true, code: 0 });
|
|
5897
|
+
}
|
|
5898
|
+
const { bin, origin } = resolveVitest(ctx.cwd);
|
|
5899
|
+
if (bin === void 0) {
|
|
5900
|
+
this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
|
|
5901
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
5902
|
+
}
|
|
5903
|
+
if (origin === "module") {
|
|
5904
|
+
this.say(
|
|
5905
|
+
`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.`
|
|
5906
|
+
);
|
|
5907
|
+
}
|
|
5908
|
+
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
|
|
5909
|
+
const watching = options.includes("--watch") || options.includes("-w");
|
|
5910
|
+
const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
|
|
5911
|
+
const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
|
|
5912
|
+
if (result.error) {
|
|
5913
|
+
this.say(`could not run vitest (${result.error.message})`);
|
|
5914
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
5915
|
+
}
|
|
5916
|
+
const code = result.status ?? 1;
|
|
5917
|
+
return Promise.resolve({ ok: code === 0, code });
|
|
5918
|
+
}
|
|
5919
|
+
say(message) {
|
|
5920
|
+
process.stderr.write(`sentinel test(vitest): ${message}
|
|
5921
|
+
`);
|
|
5922
|
+
}
|
|
5923
|
+
};
|
|
5924
|
+
|
|
5925
|
+
// src/roles/test/register.ts
|
|
5926
|
+
function registerTest() {
|
|
5927
|
+
register(new VitestAdapter());
|
|
5928
|
+
setDefaultRunner("test", "vitest");
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5931
|
+
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
5932
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
5933
|
+
import { existsSync as existsSync27, readFileSync as readFileSync25 } from "fs";
|
|
5163
5934
|
import { createRequire as createRequire4 } from "module";
|
|
5164
|
-
import { join as
|
|
5935
|
+
import { join as join32 } from "path";
|
|
5165
5936
|
|
|
5166
5937
|
// src/roles/typescript/presets/nest.json
|
|
5167
5938
|
var nest_default2 = {
|
|
@@ -5342,8 +6113,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
5342
6113
|
}
|
|
5343
6114
|
|
|
5344
6115
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
5345
|
-
import { readFileSync as
|
|
5346
|
-
import { join as
|
|
6116
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
6117
|
+
import { join as join31 } from "path";
|
|
5347
6118
|
|
|
5348
6119
|
// src/roles/typescript/typecheck-script.ts
|
|
5349
6120
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -5436,7 +6207,7 @@ function planAdoption(context) {
|
|
|
5436
6207
|
};
|
|
5437
6208
|
}
|
|
5438
6209
|
const existing = parseJsonc(
|
|
5439
|
-
|
|
6210
|
+
readFileSync24(join31(context.cwd, target.path), "utf8"),
|
|
5440
6211
|
target.path
|
|
5441
6212
|
);
|
|
5442
6213
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -5574,7 +6345,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5574
6345
|
let chain;
|
|
5575
6346
|
try {
|
|
5576
6347
|
const parsed = parseJsonc(
|
|
5577
|
-
|
|
6348
|
+
readFileSync25(join32(cwd, target.path), "utf8"),
|
|
5578
6349
|
target.path
|
|
5579
6350
|
);
|
|
5580
6351
|
chain = parsed.extends;
|
|
@@ -5587,7 +6358,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5587
6358
|
);
|
|
5588
6359
|
if (preset === void 0) return void 0;
|
|
5589
6360
|
try {
|
|
5590
|
-
createRequire4(
|
|
6361
|
+
createRequire4(join32(cwd, "noop.js")).resolve(preset);
|
|
5591
6362
|
return void 0;
|
|
5592
6363
|
} catch {
|
|
5593
6364
|
return preset;
|
|
@@ -5628,7 +6399,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5628
6399
|
return { ok: false, code: 1 };
|
|
5629
6400
|
}
|
|
5630
6401
|
if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
|
|
5631
|
-
const result =
|
|
6402
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
|
|
5632
6403
|
if (result.error) {
|
|
5633
6404
|
process.stderr.write(
|
|
5634
6405
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5663,7 +6434,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5663
6434
|
let worst = 0;
|
|
5664
6435
|
for (const project of this.referencedProjects(ctx.cwd, config)) {
|
|
5665
6436
|
const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
|
|
5666
|
-
const result =
|
|
6437
|
+
const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
|
|
5667
6438
|
if (result.error) {
|
|
5668
6439
|
process.stderr.write(
|
|
5669
6440
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5683,7 +6454,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5683
6454
|
referencedProjects(cwd, config) {
|
|
5684
6455
|
try {
|
|
5685
6456
|
const parsed = parseJsonc(
|
|
5686
|
-
|
|
6457
|
+
readFileSync25(join32(cwd, config), "utf8"),
|
|
5687
6458
|
config
|
|
5688
6459
|
);
|
|
5689
6460
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -5699,7 +6470,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5699
6470
|
* check.
|
|
5700
6471
|
*/
|
|
5701
6472
|
typecheckTarget(cwd) {
|
|
5702
|
-
if (
|
|
6473
|
+
if (existsSync27(join32(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
5703
6474
|
const target = resolveTsconfigTarget(cwd);
|
|
5704
6475
|
return target.reason === "none" ? null : target.path;
|
|
5705
6476
|
}
|
|
@@ -5752,7 +6523,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5752
6523
|
return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
|
|
5753
6524
|
}
|
|
5754
6525
|
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
5755
|
-
const result =
|
|
6526
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
5756
6527
|
if (result.error) {
|
|
5757
6528
|
process.stderr.write(
|
|
5758
6529
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5795,7 +6566,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5795
6566
|
* errors in the output mean the rule must be on.
|
|
5796
6567
|
*/
|
|
5797
6568
|
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
5798
|
-
const shown =
|
|
6569
|
+
const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
5799
6570
|
if (shown.status === 0 && shown.stdout) {
|
|
5800
6571
|
try {
|
|
5801
6572
|
const co = parseJsonc(
|
|
@@ -5822,6 +6593,7 @@ function registerAdapters() {
|
|
|
5822
6593
|
registerLint();
|
|
5823
6594
|
registerFormat();
|
|
5824
6595
|
registerBuild();
|
|
6596
|
+
registerTest();
|
|
5825
6597
|
}
|
|
5826
6598
|
|
|
5827
6599
|
// src/core/detect-framework.ts
|
|
@@ -5903,7 +6675,7 @@ function replaceLines(current, replacements) {
|
|
|
5903
6675
|
}
|
|
5904
6676
|
|
|
5905
6677
|
// src/core/apply-plan.ts
|
|
5906
|
-
import { existsSync as
|
|
6678
|
+
import { existsSync as existsSync28, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5907
6679
|
import { resolve as resolve4, sep } from "path";
|
|
5908
6680
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
5909
6681
|
|
|
@@ -5922,7 +6694,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
5922
6694
|
return absolutePath;
|
|
5923
6695
|
}
|
|
5924
6696
|
function readIfExists(absolutePath) {
|
|
5925
|
-
return
|
|
6697
|
+
return existsSync28(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
|
|
5926
6698
|
}
|
|
5927
6699
|
function* leaves(value, prefix = []) {
|
|
5928
6700
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6003,9 +6775,9 @@ function applyOperationTo(current, operation) {
|
|
|
6003
6775
|
}
|
|
6004
6776
|
}
|
|
6005
6777
|
}
|
|
6006
|
-
function preparePlan(cwd,
|
|
6778
|
+
function preparePlan(cwd, plan4) {
|
|
6007
6779
|
const prepared = /* @__PURE__ */ new Map();
|
|
6008
|
-
for (const operation of
|
|
6780
|
+
for (const operation of plan4.operations) {
|
|
6009
6781
|
const absolutePath = resolveWithinRoot(cwd, operation.path);
|
|
6010
6782
|
const existing = prepared.get(operation.path);
|
|
6011
6783
|
const before = existing?.before ?? readIfExists(absolutePath) ?? "";
|
|
@@ -6024,14 +6796,14 @@ function preparePlan(cwd, plan3) {
|
|
|
6024
6796
|
}
|
|
6025
6797
|
function writeFileAtomic(absolutePath, contents) {
|
|
6026
6798
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
6027
|
-
|
|
6799
|
+
writeFileSync4(tempPath, contents);
|
|
6028
6800
|
renameSync(tempPath, absolutePath);
|
|
6029
6801
|
}
|
|
6030
|
-
function applyPlan(cwd,
|
|
6031
|
-
const changed = preparePlan(cwd,
|
|
6802
|
+
function applyPlan(cwd, plan4) {
|
|
6803
|
+
const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
|
|
6032
6804
|
for (const file of changed) {
|
|
6033
6805
|
if (file.deleted) {
|
|
6034
|
-
|
|
6806
|
+
rmSync3(file.absolutePath, { force: true });
|
|
6035
6807
|
continue;
|
|
6036
6808
|
}
|
|
6037
6809
|
writeFileAtomic(file.absolutePath, file.after);
|
|
@@ -6040,8 +6812,8 @@ function applyPlan(cwd, plan3) {
|
|
|
6040
6812
|
}
|
|
6041
6813
|
|
|
6042
6814
|
// src/core/config/preset-evidence.ts
|
|
6043
|
-
import { existsSync as
|
|
6044
|
-
import { join as
|
|
6815
|
+
import { existsSync as existsSync29, readdirSync as readdirSync4, readFileSync as readFileSync27 } from "fs";
|
|
6816
|
+
import { join as join33 } from "path";
|
|
6045
6817
|
var PATH_SIGNALS = [
|
|
6046
6818
|
{
|
|
6047
6819
|
preset: "nest",
|
|
@@ -6055,11 +6827,11 @@ var DEPENDENCY_SIGNALS = [
|
|
|
6055
6827
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
6056
6828
|
];
|
|
6057
6829
|
function dependencyNames(cwd) {
|
|
6058
|
-
const path =
|
|
6059
|
-
if (!
|
|
6830
|
+
const path = join33(cwd, "package.json");
|
|
6831
|
+
if (!existsSync29(path)) return [];
|
|
6060
6832
|
try {
|
|
6061
6833
|
const manifest = parseJsonc(
|
|
6062
|
-
|
|
6834
|
+
readFileSync27(path, "utf8"),
|
|
6063
6835
|
path
|
|
6064
6836
|
);
|
|
6065
6837
|
return [
|
|
@@ -6073,7 +6845,7 @@ function dependencyNames(cwd) {
|
|
|
6073
6845
|
function declaresJsx(cwd) {
|
|
6074
6846
|
let entries;
|
|
6075
6847
|
try {
|
|
6076
|
-
entries =
|
|
6848
|
+
entries = readdirSync4(cwd).filter(
|
|
6077
6849
|
(name) => name.startsWith("tsconfig") && name.endsWith(".json")
|
|
6078
6850
|
);
|
|
6079
6851
|
} catch {
|
|
@@ -6082,7 +6854,7 @@ function declaresJsx(cwd) {
|
|
|
6082
6854
|
for (const name of entries) {
|
|
6083
6855
|
try {
|
|
6084
6856
|
const config = parseJsonc(
|
|
6085
|
-
|
|
6857
|
+
readFileSync27(join33(cwd, name), "utf8"),
|
|
6086
6858
|
name
|
|
6087
6859
|
);
|
|
6088
6860
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -6133,14 +6905,14 @@ function resolveFlavour(opts) {
|
|
|
6133
6905
|
}
|
|
6134
6906
|
return detection.preset;
|
|
6135
6907
|
}
|
|
6136
|
-
function previewPlan(opts,
|
|
6137
|
-
const changed = preparePlan(opts.cwd,
|
|
6908
|
+
function previewPlan(opts, plan4) {
|
|
6909
|
+
const changed = preparePlan(opts.cwd, plan4).filter((file) => file.before !== file.after);
|
|
6138
6910
|
if (opts.json) {
|
|
6139
6911
|
process.stdout.write(
|
|
6140
6912
|
JSON.stringify(
|
|
6141
6913
|
{
|
|
6142
6914
|
dryRun: true,
|
|
6143
|
-
notes:
|
|
6915
|
+
notes: plan4.notes ?? [],
|
|
6144
6916
|
files: changed.map(({ path, before, after, deleted }) => ({
|
|
6145
6917
|
path,
|
|
6146
6918
|
action: deleted ? "delete" : before.length === 0 ? "create" : "update",
|
|
@@ -6155,7 +6927,7 @@ function previewPlan(opts, plan3) {
|
|
|
6155
6927
|
return 0;
|
|
6156
6928
|
}
|
|
6157
6929
|
process.stderr.write(" dry run: no files written\n");
|
|
6158
|
-
for (const note of
|
|
6930
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6159
6931
|
`);
|
|
6160
6932
|
if (changed.length === 0) {
|
|
6161
6933
|
process.stderr.write(" nothing to change\n");
|
|
@@ -6199,25 +6971,25 @@ async function dispatch(opts) {
|
|
|
6199
6971
|
}
|
|
6200
6972
|
}
|
|
6201
6973
|
const context = { cwd: opts.cwd, preset: effective };
|
|
6202
|
-
const
|
|
6203
|
-
if (
|
|
6204
|
-
process.stderr.write(`sentinel (${opts.target}): ${
|
|
6974
|
+
const plan4 = await adapter.plan(context);
|
|
6975
|
+
if (plan4.blocked) {
|
|
6976
|
+
process.stderr.write(`sentinel (${opts.target}): ${plan4.blocked}
|
|
6205
6977
|
`);
|
|
6206
6978
|
return 1;
|
|
6207
6979
|
}
|
|
6208
|
-
if (
|
|
6209
|
-
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${
|
|
6980
|
+
if (plan4.skipped) {
|
|
6981
|
+
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan4.skipped}`)}
|
|
6210
6982
|
`);
|
|
6211
6983
|
return 0;
|
|
6212
6984
|
}
|
|
6213
6985
|
if (opts.dryRun) {
|
|
6214
|
-
return previewPlan(opts,
|
|
6986
|
+
return previewPlan(opts, plan4);
|
|
6215
6987
|
}
|
|
6216
|
-
for (const change of applyPlan(opts.cwd,
|
|
6988
|
+
for (const change of applyPlan(opts.cwd, plan4)) {
|
|
6217
6989
|
process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
|
|
6218
6990
|
`);
|
|
6219
6991
|
}
|
|
6220
|
-
for (const note of
|
|
6992
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6221
6993
|
`);
|
|
6222
6994
|
if (adapter.afterInit) {
|
|
6223
6995
|
process.stderr.write(` fixing what ${opts.target} can fix automatically...
|
|
@@ -6241,24 +7013,25 @@ export {
|
|
|
6241
7013
|
declaredPresetFor,
|
|
6242
7014
|
availableTargets,
|
|
6243
7015
|
resolve,
|
|
6244
|
-
BaseAdapter,
|
|
6245
|
-
resolveBin,
|
|
6246
7016
|
readOwnVersion,
|
|
6247
7017
|
readProjectPackageJson,
|
|
6248
7018
|
moduleName,
|
|
7019
|
+
buildConfigFile,
|
|
7020
|
+
BaseAdapter,
|
|
7021
|
+
resolveBin,
|
|
7022
|
+
WORKSPACE_ROOT_MARKER,
|
|
7023
|
+
findWorkspaceRoot,
|
|
7024
|
+
ensureWorkspacePrep,
|
|
7025
|
+
inspectWorkspacePrep,
|
|
6249
7026
|
VERBS,
|
|
6250
7027
|
TARGETS,
|
|
6251
7028
|
LONG_RUNNING_TARGETS,
|
|
6252
7029
|
SWEEPABLE_TARGETS,
|
|
6253
7030
|
PRESET_NAMES,
|
|
6254
|
-
WORKSPACE_ROOT_MARKER,
|
|
6255
|
-
findWorkspaceRoot,
|
|
6256
|
-
ensureWorkspacePrep,
|
|
6257
|
-
inspectWorkspacePrep,
|
|
6258
7031
|
palette,
|
|
6259
7032
|
registerAdapters,
|
|
6260
7033
|
describeFramework,
|
|
6261
7034
|
detectFramework,
|
|
6262
7035
|
dispatch
|
|
6263
7036
|
};
|
|
6264
|
-
//# sourceMappingURL=chunk-
|
|
7037
|
+
//# sourceMappingURL=chunk-PLZ45MWU.js.map
|