@hublo/sentinel 1.2.0-alpha.3 → 1.2.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@ import {
21
21
  registerAdapters,
22
22
  resolve,
23
23
  resolveBin
24
- } from "../chunk-YZ4EKBK7.js";
24
+ } from "../chunk-XPWM77PB.js";
25
25
 
26
26
  // bin/sentinel.ts
27
27
  import { program } from "commander";
@@ -467,9 +467,13 @@ var BUILD_CONFIG_FILES = [
467
467
  "vite.config.js",
468
468
  "vite.config.mjs"
469
469
  ];
470
- var BUILD_PRESET_SPECIFIER = "@hublo/sentinel/build/react";
470
+ var BUILD_PRESET_SPECIFIERS = {
471
+ react: "@hublo/sentinel/build/react",
472
+ nest: "@hublo/sentinel/build/nest"
473
+ };
474
+ var BUILD_PRESET_SPECIFIER = BUILD_PRESET_SPECIFIERS.react;
471
475
  var BUILD_SCRIPT_NAME = "build";
472
- var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
476
+ var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
473
477
  var DEV_SCRIPT_NAME = "serve";
474
478
  var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
475
479
  function buildTarget() {
@@ -524,12 +528,15 @@ function importsSpecifier(source, specifier) {
524
528
  return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
525
529
  }
526
530
  function importsPreset(source) {
527
- return importsSpecifier(source, BUILD_PRESET_SPECIFIER);
531
+ return Object.values(BUILD_PRESET_SPECIFIERS).some(
532
+ (specifier) => importsSpecifier(source, specifier)
533
+ );
528
534
  }
529
535
  var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
530
536
  function declaredBuildPreset(cwd) {
531
537
  const source = readBuildConfigSource(cwd);
532
538
  if (source === void 0) return void 0;
539
+ if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
533
540
  if (importsPreset(source)) return "react";
534
541
  return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
535
542
  }
@@ -559,7 +566,9 @@ function readBuildAdoption(cwd) {
559
566
  );
560
567
  return {
561
568
  configFile,
562
- preset: "react",
569
+ // Read from the config rather than assumed: the two presets are adopted the same way and
570
+ // reporting the wrong one would make `--inspect` lie about what a module builds.
571
+ preset: declaredBuildPreset(cwd) ?? "react",
563
572
  adopted: true,
564
573
  conformant: ownDeclarations.length === 0,
565
574
  drift: ownDeclarations.map(
@@ -726,7 +735,9 @@ function buildScripts(cwd) {
726
735
  const own2 = moduleName(cwd);
727
736
  scripts[DEV_SCRIPT_NAME] = composeDevScript(
728
737
  existingDev,
729
- `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
738
+ // Trailing `--` for the same reason as the build script: whatever the caller appends
739
+ // reaches Vite, not sentinel's parser. It goes after `--module`, which is sentinel's own.
740
+ `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2} --`
730
741
  );
731
742
  }
732
743
  return scripts;
@@ -840,11 +851,17 @@ var ViteRoleAdapter = class extends BaseAdapter {
840
851
  * resolution let it run at all — filtering here would give "no adapter for preset svelte",
841
852
  * which tells nobody what to do about it.
842
853
  *
843
- * Nest, node and tools are genuinely declined: there is nothing to bundle, and no version
844
- * would change that.
854
+ * Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
855
+ * grounds that "there is nothing to bundle", which was never true: those 38 services bundle
856
+ * with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
857
+ * the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
858
+ * migration generator refuses every one of them because they use `@nx/js:node`
859
+ * (nrwl/nx#36389). So the way out runs through here.
860
+ *
861
+ * `node` and `tools` remain genuinely declined: nothing under them bundles at all.
845
862
  */
846
863
  appliesTo(preset) {
847
- return preset === "react" || preset === "svelte";
864
+ return preset === "react" || preset === "svelte" || preset === "nest";
848
865
  }
849
866
  /**
850
867
  * `react`, read from the module's Vite config rather than from its dependencies.
@@ -935,22 +952,24 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
935
952
  return "dev";
936
953
  }
937
954
  /**
938
- * There is no such thing as adopting the dev server on its own.
955
+ * `--init --dev` does the same thing as `--init --build`, and now says so.
939
956
  *
940
- * `ViteRoleAdapter.plan()` is shared with `--build` because everything else about the two
941
- * targets is shared, and inheriting it here made `--init --dev` perform the FULL build
942
- * adoption, silently, under a message that only ever mentioned the build. Exiting 0 on that
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`).
946
- *
947
- * A single plan writes both scripts, so the honest answer is to name the command that does
948
- * it rather than to do it under a different name.
957
+ * The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
958
+ * because adopting one without the other leaves the module half-migrated. What was missing
959
+ * was only the telling: the run printed a message about the build and nothing about `serve`,
960
+ * so a developer could believe the dev server had been adopted on its own. Refusing was the
961
+ * wrong correction, it added friction to a command that already produced the right result.
949
962
  */
950
- plan() {
951
- throw new Error(
952
- "the dev server is adopted together with the build, never on its own. Run --init --build: one plan writes both the `build` and the `serve` scripts."
953
- );
963
+ plan(context) {
964
+ const planned = super.plan(context);
965
+ if (planned.operations.length === 0) return planned;
966
+ return {
967
+ ...planned,
968
+ notes: [
969
+ ...planned.notes ?? [],
970
+ "--init --dev adopts the build too: one plan writes both `build` and `serve`."
971
+ ]
972
+ };
954
973
  }
955
974
  /**
956
975
  * Start the dev server. It does not return until stopped, so there is no verdict to report
@@ -6261,4 +6280,4 @@ export {
6261
6280
  detectFramework,
6262
6281
  dispatch
6263
6282
  };
6264
- //# sourceMappingURL=chunk-YZ4EKBK7.js.map
6283
+ //# sourceMappingURL=chunk-XPWM77PB.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-YZ4EKBK7.js";
10
+ } from "./chunk-XPWM77PB.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
@@ -0,0 +1,80 @@
1
+ import { UserConfig, Plugin } from 'vite';
2
+ export { Plugin, PluginOption, UserConfig, UserConfigExport, defineConfig, loadEnv, mergeConfig } from 'vite';
3
+
4
+ interface NestServiceOptions {
5
+ /** The nx project name. Keys the dependency graph, and names the module in messages. */
6
+ project: string;
7
+ /** Absolute path to the module's directory, normally `__dirname`. */
8
+ root: string;
9
+ /** Absolute path to the workspace root. */
10
+ workspaceRoot: string;
11
+ /** Entry point, relative to `root`. */
12
+ entry?: string;
13
+ /**
14
+ * The tsconfig whose emit must be preserved, when the service uses neither conventional name.
15
+ *
16
+ * Relative to `root`. Defaults to `tsconfig.app.json`, then `tsconfig.json`.
17
+ */
18
+ tsconfig?: string;
19
+ /** Output directory, relative to `workspaceRoot`. */
20
+ outDir?: string;
21
+ /**
22
+ * Anything this service needs that the shape does not give it.
23
+ *
24
+ * Merged with Vite's own `mergeConfig`, never with a merge of our own: plugins concatenate
25
+ * and aliases stack the way Vite does it everywhere else, so there is no second set of
26
+ * semantics to learn or to document.
27
+ */
28
+ overrides?: UserConfig;
29
+ }
30
+ declare const nestService: (options: NestServiceOptions) => UserConfig;
31
+
32
+ interface DecoratorMetadataOptions {
33
+ /** The module being built. Its tsconfig is the one whose emit must be preserved. */
34
+ root: string;
35
+ /**
36
+ * An explicit tsconfig, when the service does not use either conventional name.
37
+ *
38
+ * Relative paths resolve against `root`.
39
+ */
40
+ tsconfig?: string;
41
+ }
42
+ declare const decoratorMetadata: (options: DecoratorMetadataOptions) => Plugin;
43
+
44
+ interface NodeManifestOptions {
45
+ /** The nx project name, which is how the graph is keyed. */
46
+ project: string;
47
+ /** Absolute path to the workspace root. */
48
+ workspaceRoot: string;
49
+ /** Where the bundle is written; the two files land beside it. */
50
+ outDir: string;
51
+ /** Entry file name, written as `main` so `node .` resolves inside the image. */
52
+ entry?: string;
53
+ }
54
+ /**
55
+ * Writes the manifest and lockfile once the bundle is on disk.
56
+ *
57
+ * `closeBundle` rather than `writeBundle`, so the files land after Vite has finished with the
58
+ * directory and cannot be cleared by `emptyOutDir`.
59
+ *
60
+ * `closeBundle` also runs after a FAILED build, where there is no directory to write into. Left
61
+ * alone this hook then threw `ENOENT` on the manifest, and since it is the last error raised it
62
+ * became the one Vite printed, hiding the failure that actually stopped the build. It cost two
63
+ * diagnoses before being recognised, so the missing directory is now read as what it is: the
64
+ * bundle was never written, and this plugin has nothing to say about why.
65
+ */
66
+ declare const nodeManifest: (options: NodeManifestOptions) => Plugin;
67
+
68
+ interface Alias {
69
+ find: RegExp;
70
+ replacement: string;
71
+ }
72
+ /**
73
+ * Read the mappings from the workspace's base tsconfig.
74
+ *
75
+ * Longest pattern first, because Vite takes the first alias that matches and the mappings
76
+ * overlap by design: `@front/theme/node` must not be swallowed by `@front/theme`.
77
+ */
78
+ declare const tsconfigAliases: (workspaceRoot: string, file?: string) => Alias[];
79
+
80
+ export { type Alias, type DecoratorMetadataOptions, type NestServiceOptions, type NodeManifestOptions, decoratorMetadata, nestService, nodeManifest, tsconfigAliases };
@@ -0,0 +1,198 @@
1
+ // src/roles/build/nest/nest-service.ts
2
+ import { join as join3 } from "path";
3
+ import { mergeConfig } from "vite";
4
+
5
+ // src/roles/build/nest/decorator-metadata.ts
6
+ import { existsSync } from "fs";
7
+ import path from "path";
8
+ import ts from "typescript";
9
+ var TYPESCRIPT_SOURCE = /\.ts$/;
10
+ var TSCONFIG_CANDIDATES = ["tsconfig.app.json", "tsconfig.json"];
11
+ var assertCompilerApi = () => {
12
+ if (typeof ts.transpileModule === "function") return;
13
+ throw new Error(
14
+ `sentinel build(nest): the resolved typescript (${ts.version ?? "unknown version"}) has no transpileModule API. TypeScript 7 is the native port and exposes none; the emit API lives under @typescript/typescript6. Sentinel pins its own compiler, so this means one was substituted, usually by a pnpm override or an alias on \`typescript\`.`
15
+ );
16
+ };
17
+ var resolveTsconfig = (options) => {
18
+ if (options.tsconfig) {
19
+ const explicit = path.resolve(options.root, options.tsconfig);
20
+ if (!existsSync(explicit)) {
21
+ throw new Error(`sentinel build(nest): tsconfig not found at ${explicit}`);
22
+ }
23
+ return explicit;
24
+ }
25
+ for (const candidate of TSCONFIG_CANDIDATES) {
26
+ const found = path.join(options.root, candidate);
27
+ if (existsSync(found)) return found;
28
+ }
29
+ throw new Error(
30
+ `sentinel build(nest): no ${TSCONFIG_CANDIDATES.join(" or ")} in ${options.root}. Pass \`tsconfig\` if this service keeps it elsewhere.`
31
+ );
32
+ };
33
+ var readCompilerOptions = (options) => {
34
+ assertCompilerApi();
35
+ const configPath = resolveTsconfig(options);
36
+ const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
37
+ ...ts.sys,
38
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
39
+ throw new Error(
40
+ `sentinel build(nest): could not read ${configPath}: ` + ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")
41
+ );
42
+ }
43
+ });
44
+ return {
45
+ ...parsed?.options,
46
+ // ESM out, so Rollup sees imports and exports rather than an opaque `require` it cannot
47
+ // follow. The service still SHIPS as CJS: that conversion is the bundler's, further down.
48
+ module: ts.ModuleKind.ESNext,
49
+ // Vite consumes the map; the tsconfig's own answer is about a different pipeline.
50
+ sourceMap: true,
51
+ inlineSourceMap: false,
52
+ inlineSources: false,
53
+ // Types are another target's job, and `transpileModule` could not emit them anyway.
54
+ declaration: false,
55
+ declarationMap: false,
56
+ emitDeclarationOnly: false,
57
+ noEmit: false,
58
+ // `composite` projects refuse to emit without a `tsBuildInfoFile`, and there is no
59
+ // incremental build here to inform.
60
+ composite: false,
61
+ incremental: false
62
+ };
63
+ };
64
+ var decoratorMetadata = (options) => {
65
+ let compilerOptions;
66
+ return {
67
+ name: "sentinel:decorator-metadata",
68
+ // Before Vite's own transform, so esbuild never sees the decorators it cannot handle.
69
+ enforce: "pre",
70
+ transform(code, id) {
71
+ if (!TYPESCRIPT_SOURCE.test(id) || id.includes("node_modules")) {
72
+ return null;
73
+ }
74
+ compilerOptions ??= readCompilerOptions(options);
75
+ const output = ts.transpileModule(code, { fileName: id, compilerOptions });
76
+ return { code: output.outputText, map: output.sourceMapText ?? null };
77
+ }
78
+ };
79
+ };
80
+
81
+ // src/roles/build/nest/node-manifest.ts
82
+ import { existsSync as existsSync2, writeFileSync } from "fs";
83
+ import { join } from "path";
84
+ var nodeManifest = (options) => ({
85
+ name: "sentinel:node-manifest",
86
+ apply: "build",
87
+ async closeBundle() {
88
+ if (!existsSync2(options.outDir)) return;
89
+ const { createProjectGraphAsync } = await import("nx/src/devkit-exports.js");
90
+ const { createPackageJson, createLockFile, getLockFileName } = await import("@nx/js");
91
+ const graph = await createProjectGraphAsync({ exitOnError: false });
92
+ const manifest = createPackageJson(options.project, graph, {
93
+ root: options.workspaceRoot,
94
+ isProduction: true
95
+ });
96
+ manifest.main = options.entry ?? "main.js";
97
+ writeFileSync(join(options.outDir, "package.json"), `${JSON.stringify(manifest, null, 2)}
98
+ `);
99
+ writeFileSync(
100
+ join(options.outDir, getLockFileName("pnpm")),
101
+ createLockFile(manifest, graph, "pnpm")
102
+ );
103
+ }
104
+ });
105
+
106
+ // src/roles/build/nest/tsconfig-aliases.ts
107
+ import { readFileSync } from "fs";
108
+ import { join as join2 } from "path";
109
+ var stripLineComments = (json) => json.replace(/^\s*\/\/.*$/gm, "");
110
+ var toAlias = (workspaceRoot, pattern, target) => {
111
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "(.*)");
112
+ return {
113
+ find: new RegExp(`^${escaped}$`),
114
+ replacement: join2(workspaceRoot, target.replace(/\*/g, "$1"))
115
+ };
116
+ };
117
+ var tsconfigAliases = (workspaceRoot, file = "tsconfig.base.json") => {
118
+ const raw = readFileSync(join2(workspaceRoot, file), "utf8");
119
+ const { compilerOptions } = JSON.parse(stripLineComments(raw));
120
+ return Object.entries(compilerOptions?.paths ?? {}).flatMap(([pattern, targets]) => {
121
+ const [target] = targets;
122
+ return target === void 0 ? [] : [toAlias(workspaceRoot, pattern, target)];
123
+ }).sort((a, b) => b.find.source.length - a.find.source.length);
124
+ };
125
+
126
+ // src/roles/build/nest/nest-service.ts
127
+ var baseConfig = (options) => {
128
+ const outDir = join3(options.workspaceRoot, options.outDir ?? `dist/${options.project}`);
129
+ const entry = join3(options.root, options.entry ?? "src/main.ts");
130
+ return {
131
+ plugins: [
132
+ decoratorMetadata({ root: options.root, tsconfig: options.tsconfig }),
133
+ nodeManifest({
134
+ project: options.project,
135
+ workspaceRoot: options.workspaceRoot,
136
+ outDir
137
+ })
138
+ ],
139
+ resolve: { alias: tsconfigAliases(options.workspaceRoot) },
140
+ build: {
141
+ // An SSR build targets Node and leaves real packages external, which is what
142
+ // `webpack-node-externals` did: the image installs them from the generated manifest.
143
+ ssr: entry,
144
+ outDir,
145
+ emptyOutDir: true,
146
+ sourcemap: true,
147
+ target: "node20",
148
+ // The bundle is read by humans when a stack trace points into it, and minifying a
149
+ // server bundle buys nothing: it is never downloaded.
150
+ minify: false,
151
+ rollupOptions: {
152
+ output: {
153
+ format: "cjs",
154
+ // One output file per source module, instead of hoisting everything into one scope.
155
+ //
156
+ // This is not a preference, it is the only shape that keeps the OpenAPI contract.
157
+ // Merging every module into one scope forces Rollup to rename duplicate class names,
158
+ // and `@nestjs/swagger` keys its schemas on `class.name`, so a renamed class silently
159
+ // becomes a renamed schema in the published API. Measured on `network`, which has two
160
+ // such collisions: single bundle renames `InvalidPermissionError` and
161
+ // `PermissionNotFoundError`; preserving modules renames nothing and reproduces the
162
+ // committed contract byte for byte.
163
+ //
164
+ // The cost is 4.5 MB across 2003 files against 2.9 MB in one, still well under
165
+ // webpack's 6.9 MB for the same service, so nothing regresses against what it replaces.
166
+ preserveModules: true,
167
+ // The image runs `./dist/main.js`, so the entry keeps that name at the root while
168
+ // every other module keeps its own path. Under `preserveModules` a plain string here
169
+ // would be applied to all of them, which numbers them (`main962.js`) and loses the
170
+ // entry.
171
+ entryFileNames: (chunk) => chunk.facadeModuleId === entry ? "main.js" : "[name].js"
172
+ },
173
+ // Nest registers metadata as an IMPORT SIDE EFFECT: a decorator writes into a catalog
174
+ // when its module loads, and nothing references that module afterwards. Rollup may
175
+ // drop such a module; webpack never did.
176
+ //
177
+ // Measured on the first migrated service this changes nothing, so it is insurance
178
+ // rather than a fix, and it is recorded as such rather than credited with the smaller
179
+ // bundle (that comes from barrel re-exports the service does not use).
180
+ treeshake: { moduleSideEffects: true }
181
+ }
182
+ }
183
+ };
184
+ };
185
+ var nestService = (options) => options.overrides === void 0 ? baseConfig(options) : mergeConfig(baseConfig(options), options.overrides);
186
+
187
+ // src/roles/build/nest/toolchain.ts
188
+ import { defineConfig, loadEnv, mergeConfig as mergeConfig2 } from "vite";
189
+ export {
190
+ decoratorMetadata,
191
+ defineConfig,
192
+ loadEnv,
193
+ mergeConfig2 as mergeConfig,
194
+ nestService,
195
+ nodeManifest,
196
+ tsconfigAliases
197
+ };
198
+ //# sourceMappingURL=toolchain.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.2.0-alpha.3",
3
+ "version": "1.2.0-alpha.5",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,6 +41,10 @@
41
41
  "./build/react": {
42
42
  "types": "./dist/roles/build/toolchain.d.ts",
43
43
  "import": "./dist/roles/build/toolchain.js"
44
+ },
45
+ "./build/nest": {
46
+ "types": "./dist/roles/build/nest/toolchain.d.ts",
47
+ "import": "./dist/roles/build/nest/toolchain.js"
44
48
  }
45
49
  },
46
50
  "files": [
@@ -66,6 +70,7 @@
66
70
  "oxfmt": "0.63.0",
67
71
  "oxlint": "1.77.0",
68
72
  "oxlint-tsgolint": "7.0.2001",
73
+ "typescript": "5.9.3",
69
74
  "vite": "8.0.8",
70
75
  "vite-plugin-svgr": "5.2.0"
71
76
  },
@@ -96,6 +101,18 @@
96
101
  }
97
102
  }
98
103
  },
104
+ "peerDependencies": {
105
+ "nx": ">= 21",
106
+ "@nx/js": ">= 21"
107
+ },
108
+ "peerDependenciesMeta": {
109
+ "nx": {
110
+ "optional": true
111
+ },
112
+ "@nx/js": {
113
+ "optional": true
114
+ }
115
+ },
99
116
  "scripts": {
100
117
  "build": "tsup && tsx scripts/build-presets.ts",
101
118
  "dev": "tsup --watch",