@hublo/sentinel 1.2.0-alpha.4 → 1.2.0-alpha.40
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 +32 -3
- package/dist/chunk-3TDUIKVQ.js +178 -0
- package/dist/chunk-NKFGYPIQ.js +13 -0
- package/dist/{chunk-6DBTDMSN.js → chunk-PBCVWRXM.js} +4657 -2960
- package/dist/index.js +1 -1
- package/dist/roles/build/nest/toolchain.d.ts +98 -0
- package/dist/roles/build/nest/toolchain.js +235 -0
- package/dist/roles/test/nest/toolchain.d.ts +17 -0
- package/dist/roles/test/nest/toolchain.js +116 -0
- package/dist/roles/test/react/toolchain.d.ts +2 -0
- package/dist/roles/test/react/toolchain.js +9 -0
- package/dist/roles/test/setup/msw-server.d.ts +15 -0
- package/dist/roles/test/setup/msw-server.js +7 -0
- package/dist/roles/test/setup/nest.d.ts +2 -0
- package/dist/roles/test/setup/nest.js +47 -0
- package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
- package/lint/react-lib.json +3 -1
- package/lint/react.json +4 -1
- package/package.json +41 -5
- package/plugins/hublo.js +68 -0
package/dist/index.js
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { UserConfig, Plugin } from 'vite';
|
|
2
|
+
export { Plugin, PluginOption, UserConfig, UserConfigExport, defineConfig, loadEnv, mergeConfig } from 'vite';
|
|
3
|
+
export { A as Alias, D as DecoratorMetadataOptions, d as decoratorMetadata, t as tsconfigAliases } from '../../../tsconfig-aliases-Ce6axdJ4.js';
|
|
4
|
+
|
|
5
|
+
/** A transformer as the target declares it, passed through to the config unchanged. */
|
|
6
|
+
interface TransformerDeclaration {
|
|
7
|
+
name: string;
|
|
8
|
+
options?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** An asset as the target declares it, in `@nx/webpack`'s own shape. */
|
|
11
|
+
interface AssetDeclaration {
|
|
12
|
+
glob: string;
|
|
13
|
+
/** Relative to the workspace root, as the target declares it. */
|
|
14
|
+
input: string;
|
|
15
|
+
/** Relative to the output directory. */
|
|
16
|
+
output: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface NestServiceOptions {
|
|
20
|
+
/** The nx project name. Keys the dependency graph, and names the module in messages. */
|
|
21
|
+
project: string;
|
|
22
|
+
/** Absolute path to the module's directory, normally `__dirname`. */
|
|
23
|
+
root: string;
|
|
24
|
+
/** Absolute path to the workspace root. */
|
|
25
|
+
workspaceRoot: string;
|
|
26
|
+
/** Entry point, relative to `root`. */
|
|
27
|
+
entry?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The tsconfig whose emit must be preserved, when the service uses neither conventional name.
|
|
30
|
+
*
|
|
31
|
+
* Relative to `root`. Defaults to `tsconfig.app.json`, then `tsconfig.json`.
|
|
32
|
+
*/
|
|
33
|
+
tsconfig?: string;
|
|
34
|
+
/** Output directory, relative to `workspaceRoot`. */
|
|
35
|
+
outDir?: string;
|
|
36
|
+
/**
|
|
37
|
+
* TypeScript transformers this service compiles with, as its build target declared them.
|
|
38
|
+
*
|
|
39
|
+
* Read off the webpack target by `--init` and written here, never inferred: what a service
|
|
40
|
+
* compiles with is the service's own. Eight services and BFFs run `@nestjs/swagger/plugin`,
|
|
41
|
+
* which writes the `@ApiProperty` decorators their DTOs do not declare by hand. Building
|
|
42
|
+
* without it succeeds and silently costs them most of their published contract (measured on
|
|
43
|
+
* `institution`: 385 insertions, 1378 deletions in the committed OpenAPI document).
|
|
44
|
+
*/
|
|
45
|
+
transformers?: readonly TransformerDeclaration[];
|
|
46
|
+
/**
|
|
47
|
+
* Files copied into the output beside the bundle, in the shape `@nx/webpack` declared them.
|
|
48
|
+
*
|
|
49
|
+
* One service here uses it: `planning-period` ships the pug, css and ttf templates it renders
|
|
50
|
+
* printed documents from. Without them the service starts and fails on its first print.
|
|
51
|
+
*/
|
|
52
|
+
assets?: readonly AssetDeclaration[];
|
|
53
|
+
/**
|
|
54
|
+
* Anything this service needs that the shape does not give it.
|
|
55
|
+
*
|
|
56
|
+
* Merged with Vite's own `mergeConfig`, never with a merge of our own: plugins concatenate
|
|
57
|
+
* and aliases stack the way Vite does it everywhere else, so there is no second set of
|
|
58
|
+
* semantics to learn or to document.
|
|
59
|
+
*/
|
|
60
|
+
overrides?: UserConfig;
|
|
61
|
+
}
|
|
62
|
+
declare const nestService: (options: NestServiceOptions) => UserConfig;
|
|
63
|
+
|
|
64
|
+
interface NodeManifestOptions {
|
|
65
|
+
/** The nx project name, which is how the graph is keyed. */
|
|
66
|
+
project: string;
|
|
67
|
+
/** Absolute path to the workspace root. */
|
|
68
|
+
workspaceRoot: string;
|
|
69
|
+
/** Where the bundle is written; the two files land beside it. */
|
|
70
|
+
outDir: string;
|
|
71
|
+
/** Entry file name, written as `main` so `node .` resolves inside the image. */
|
|
72
|
+
entry?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Packages the IMAGE provides, which are therefore allowed to be required without being
|
|
75
|
+
* declared in the manifest.
|
|
76
|
+
*
|
|
77
|
+
* The generated Prisma clients are the case this exists for: the Dockerfile copies
|
|
78
|
+
* `node_modules/@prisma` from its own stage, so they resolve at runtime while no module
|
|
79
|
+
* declares them. Everything else that is required and undeclared is a bug, and the check
|
|
80
|
+
* below refuses the build.
|
|
81
|
+
*/
|
|
82
|
+
providedByImage?: readonly string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Writes the manifest and lockfile once the bundle is on disk.
|
|
86
|
+
*
|
|
87
|
+
* `closeBundle` rather than `writeBundle`, so the files land after Vite has finished with the
|
|
88
|
+
* directory and cannot be cleared by `emptyOutDir`.
|
|
89
|
+
*
|
|
90
|
+
* `closeBundle` also runs after a FAILED build, where there is no directory to write into. Left
|
|
91
|
+
* alone this hook then threw `ENOENT` on the manifest, and since it is the last error raised it
|
|
92
|
+
* became the one Vite printed, hiding the failure that actually stopped the build. It cost two
|
|
93
|
+
* diagnoses before being recognised, so the missing directory is now read as what it is: the
|
|
94
|
+
* bundle was never written, and this plugin has nothing to say about why.
|
|
95
|
+
*/
|
|
96
|
+
declare const nodeManifest: (options: NodeManifestOptions) => Plugin;
|
|
97
|
+
|
|
98
|
+
export { type NestServiceOptions, type NodeManifestOptions, nestService, nodeManifest };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decoratorMetadata,
|
|
3
|
+
tsconfigAliases
|
|
4
|
+
} from "../../../chunk-3TDUIKVQ.js";
|
|
5
|
+
|
|
6
|
+
// src/roles/build/nest/nest-service.ts
|
|
7
|
+
import { join as join3 } from "path";
|
|
8
|
+
import { mergeConfig } from "vite";
|
|
9
|
+
|
|
10
|
+
// src/roles/build/nest/copy-assets.ts
|
|
11
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
12
|
+
import { dirname, join, relative, sep } from "path";
|
|
13
|
+
function globToRegExp(glob) {
|
|
14
|
+
let pattern = "";
|
|
15
|
+
for (let index = 0; index < glob.length; index++) {
|
|
16
|
+
const char = glob[index] ?? "";
|
|
17
|
+
if (char === "*") {
|
|
18
|
+
if (glob[index + 1] === "*") {
|
|
19
|
+
pattern += glob[index + 2] === "/" ? "(?:.*/)?" : ".*";
|
|
20
|
+
index += glob[index + 2] === "/" ? 2 : 1;
|
|
21
|
+
} else {
|
|
22
|
+
pattern += "[^/]*";
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (char === "{") {
|
|
27
|
+
const close = glob.indexOf("}", index);
|
|
28
|
+
if (close === -1)
|
|
29
|
+
throw new Error(`sentinel build(nest): unbalanced \`{\` in asset glob ${glob}`);
|
|
30
|
+
const alternatives = glob.slice(index + 1, close).split(",");
|
|
31
|
+
pattern += `(?:${alternatives.map((one) => one.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`;
|
|
32
|
+
index = close;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
pattern += char.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
}
|
|
37
|
+
return new RegExp(`^${pattern}$`);
|
|
38
|
+
}
|
|
39
|
+
function filesUnder(root) {
|
|
40
|
+
const found = [];
|
|
41
|
+
const walk = (dir) => {
|
|
42
|
+
for (const entry of readdirSync(dir)) {
|
|
43
|
+
const full = join(dir, entry);
|
|
44
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
45
|
+
else found.push(relative(root, full).split(sep).join("/"));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
walk(root);
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
function resolveAssets(options) {
|
|
52
|
+
const copies = [];
|
|
53
|
+
for (const asset of options.assets) {
|
|
54
|
+
const input = join(options.workspaceRoot, asset.input);
|
|
55
|
+
if (!existsSync(input)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`sentinel build(nest): the build target declares an asset from \`${asset.input}\`, which does not exist. webpack copied it into the output, so the built service would be missing files it reads at runtime.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const matcher = globToRegExp(asset.glob);
|
|
61
|
+
const matched = filesUnder(input).filter((file) => matcher.test(file));
|
|
62
|
+
if (matched.length === 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`sentinel build(nest): the asset glob \`${asset.glob}\` under \`${asset.input}\` matches no file. An entry that matches nothing is the same silent loss as no entry at all.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
for (const file of matched) {
|
|
68
|
+
copies.push({ from: join(input, file), to: join(options.outDir, asset.output, file) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return copies;
|
|
72
|
+
}
|
|
73
|
+
var copyAssets = (options) => ({
|
|
74
|
+
name: "sentinel:copy-assets",
|
|
75
|
+
// After the bundle is written and the output directory has stopped being emptied.
|
|
76
|
+
closeBundle() {
|
|
77
|
+
for (const { from, to } of resolveAssets(options)) {
|
|
78
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
79
|
+
cpSync(from, to);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// src/roles/build/nest/node-manifest.ts
|
|
85
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
86
|
+
import { isBuiltin } from "module";
|
|
87
|
+
import { join as join2 } from "path";
|
|
88
|
+
var DEFAULT_PROVIDED_BY_IMAGE = ["@prisma"];
|
|
89
|
+
var externalPackages = (bundle) => {
|
|
90
|
+
const emitted = new Set(Object.keys(bundle));
|
|
91
|
+
const packages = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const emittedFile of Object.values(bundle)) {
|
|
93
|
+
if (emittedFile.type !== "chunk") continue;
|
|
94
|
+
for (const imported of emittedFile.imports) {
|
|
95
|
+
if (emitted.has(imported)) continue;
|
|
96
|
+
if (imported.startsWith(".") || imported.startsWith("/")) continue;
|
|
97
|
+
if (isBuiltin(imported)) continue;
|
|
98
|
+
const parts = imported.split("/");
|
|
99
|
+
const name = imported.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
100
|
+
if (name) packages.add(name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return packages;
|
|
104
|
+
};
|
|
105
|
+
var nodeManifest = (options) => {
|
|
106
|
+
let required;
|
|
107
|
+
return {
|
|
108
|
+
name: "sentinel:node-manifest",
|
|
109
|
+
apply: "build",
|
|
110
|
+
generateBundle(_outputOptions, bundle) {
|
|
111
|
+
required = externalPackages(bundle);
|
|
112
|
+
},
|
|
113
|
+
async closeBundle() {
|
|
114
|
+
const entry = options.entry ?? "main.js";
|
|
115
|
+
if (!existsSync2(join2(options.outDir, entry))) return;
|
|
116
|
+
const { createProjectGraphAsync } = await import("nx/src/devkit-exports.js");
|
|
117
|
+
const { createPackageJson, createLockFile, getLockFileName } = await import("@nx/js");
|
|
118
|
+
const graph = await createProjectGraphAsync({ exitOnError: false });
|
|
119
|
+
const manifest = createPackageJson(options.project, graph, {
|
|
120
|
+
root: options.workspaceRoot,
|
|
121
|
+
isProduction: true
|
|
122
|
+
});
|
|
123
|
+
manifest.main = entry;
|
|
124
|
+
if (required === void 0) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`sentinel build(nest): the bundle was written but never inspected, so the manifest could not be checked against what it requires. Refusing to write one that might not install.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
assertManifestCovers(required, manifest, options);
|
|
130
|
+
writeFileSync(join2(options.outDir, "package.json"), `${JSON.stringify(manifest, null, 2)}
|
|
131
|
+
`);
|
|
132
|
+
writeFileSync(
|
|
133
|
+
join2(options.outDir, getLockFileName("pnpm")),
|
|
134
|
+
createLockFile(manifest, graph, "pnpm")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
var assertManifestCovers = (required, manifest, options) => {
|
|
140
|
+
const declared = new Set(Object.keys(manifest.dependencies ?? {}));
|
|
141
|
+
const provided = options.providedByImage ?? DEFAULT_PROVIDED_BY_IMAGE;
|
|
142
|
+
const missing = [...required].filter((name) => !declared.has(name)).filter((name) => !provided.some((prefix) => name === prefix || name.startsWith(`${prefix}/`))).sort();
|
|
143
|
+
if (missing.length === 0) return;
|
|
144
|
+
throw new Error(
|
|
145
|
+
`sentinel build(nest): the bundle requires ${missing.length} package(s) the generated manifest does not declare, so the image would not install them: ${missing.join(", ")}. Either declare them in the module's package.json, inline them with \`ssr.noExternal\`, or list them in \`providedByImage\` if the Dockerfile copies them in.`
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/roles/build/nest/nest-service.ts
|
|
150
|
+
var baseConfig = (options) => {
|
|
151
|
+
const outDir = join3(options.workspaceRoot, options.outDir ?? `dist/${options.project}`);
|
|
152
|
+
const entry = join3(options.root, options.entry ?? "src/main.ts");
|
|
153
|
+
return {
|
|
154
|
+
plugins: [
|
|
155
|
+
decoratorMetadata({
|
|
156
|
+
root: options.root,
|
|
157
|
+
tsconfig: options.tsconfig,
|
|
158
|
+
transformers: options.transformers
|
|
159
|
+
}),
|
|
160
|
+
...options.assets?.length ? [copyAssets({ workspaceRoot: options.workspaceRoot, outDir, assets: options.assets })] : [],
|
|
161
|
+
nodeManifest({
|
|
162
|
+
project: options.project,
|
|
163
|
+
workspaceRoot: options.workspaceRoot,
|
|
164
|
+
outDir
|
|
165
|
+
})
|
|
166
|
+
],
|
|
167
|
+
resolve: { alias: tsconfigAliases(options.workspaceRoot) },
|
|
168
|
+
ssr: {
|
|
169
|
+
// `importHelpers: true` in the workspace tsconfig makes TypeScript emit `require("tslib")`
|
|
170
|
+
// for `__decorate` and `__metadata`, so every decorated file depends on it. Hoisted into
|
|
171
|
+
// one scope Rollup inlined it and nobody noticed; preserving modules left it external, and
|
|
172
|
+
// the manifest nx generates from the project graph does not list it, because no module
|
|
173
|
+
// DECLARES tslib. The image therefore did not install it and the service died on its first
|
|
174
|
+
// require, only in the image: locally and in CI the workspace root has tslib.
|
|
175
|
+
noExternal: ["tslib"]
|
|
176
|
+
},
|
|
177
|
+
build: {
|
|
178
|
+
// An SSR build targets Node and leaves real packages external, which is what
|
|
179
|
+
// `webpack-node-externals` did: the image installs them from the generated manifest.
|
|
180
|
+
ssr: entry,
|
|
181
|
+
outDir,
|
|
182
|
+
emptyOutDir: true,
|
|
183
|
+
sourcemap: true,
|
|
184
|
+
target: "node20",
|
|
185
|
+
// The bundle is read by humans when a stack trace points into it, and minifying a
|
|
186
|
+
// server bundle buys nothing: it is never downloaded.
|
|
187
|
+
minify: false,
|
|
188
|
+
rollupOptions: {
|
|
189
|
+
output: {
|
|
190
|
+
format: "cjs",
|
|
191
|
+
// One output file per source module, instead of hoisting everything into one scope.
|
|
192
|
+
//
|
|
193
|
+
// This is not a preference, it is the only shape that keeps the OpenAPI contract.
|
|
194
|
+
// Merging every module into one scope forces Rollup to rename duplicate class names,
|
|
195
|
+
// and `@nestjs/swagger` keys its schemas on `class.name`, so a renamed class silently
|
|
196
|
+
// becomes a renamed schema in the published API. Measured on `network`, which has two
|
|
197
|
+
// such collisions: single bundle renames `InvalidPermissionError` and
|
|
198
|
+
// `PermissionNotFoundError`; preserving modules renames nothing and reproduces the
|
|
199
|
+
// committed contract byte for byte.
|
|
200
|
+
//
|
|
201
|
+
// The cost is 4.5 MB across 2003 files against 2.9 MB in one, still well under
|
|
202
|
+
// webpack's 6.9 MB for the same service, so nothing regresses against what it replaces.
|
|
203
|
+
preserveModules: true,
|
|
204
|
+
// The image runs `./dist/main.js`, so the entry keeps that name at the root while
|
|
205
|
+
// every other module keeps its own path. Under `preserveModules` a plain string here
|
|
206
|
+
// would be applied to all of them, which numbers them (`main962.js`) and loses the
|
|
207
|
+
// entry.
|
|
208
|
+
entryFileNames: (chunk) => chunk.facadeModuleId === entry ? "main.js" : "[name].js"
|
|
209
|
+
},
|
|
210
|
+
// Nest registers metadata as an IMPORT SIDE EFFECT: a decorator writes into a catalog
|
|
211
|
+
// when its module loads, and nothing references that module afterwards. Rollup may
|
|
212
|
+
// drop such a module; webpack never did.
|
|
213
|
+
//
|
|
214
|
+
// Measured on the first migrated service this changes nothing, so it is insurance
|
|
215
|
+
// rather than a fix, and it is recorded as such rather than credited with the smaller
|
|
216
|
+
// bundle (that comes from barrel re-exports the service does not use).
|
|
217
|
+
treeshake: { moduleSideEffects: true }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
var nestService = (options) => options.overrides === void 0 ? baseConfig(options) : mergeConfig(baseConfig(options), options.overrides);
|
|
223
|
+
|
|
224
|
+
// src/roles/build/nest/toolchain.ts
|
|
225
|
+
import { defineConfig, loadEnv, mergeConfig as mergeConfig2 } from "vite";
|
|
226
|
+
export {
|
|
227
|
+
decoratorMetadata,
|
|
228
|
+
defineConfig,
|
|
229
|
+
loadEnv,
|
|
230
|
+
mergeConfig2 as mergeConfig,
|
|
231
|
+
nestService,
|
|
232
|
+
nodeManifest,
|
|
233
|
+
tsconfigAliases
|
|
234
|
+
};
|
|
235
|
+
//# sourceMappingURL=toolchain.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ViteUserConfig } from 'vitest/config';
|
|
2
|
+
export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
|
|
3
|
+
export { loadEnv } from 'vite';
|
|
4
|
+
export { A as Alias, D as DecoratorMetadataOptions, d as decoratorMetadata, t as tsconfigAliases } from '../../../tsconfig-aliases-Ce6axdJ4.js';
|
|
5
|
+
|
|
6
|
+
interface NestTestOptions {
|
|
7
|
+
/** The module's own directory: where its specs live and what its config is relative to. */
|
|
8
|
+
root: string;
|
|
9
|
+
/** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */
|
|
10
|
+
workspaceRoot: string;
|
|
11
|
+
/** Merged over the base. For what a module genuinely needs to differ on, nothing else. */
|
|
12
|
+
overrides?: ViteUserConfig;
|
|
13
|
+
}
|
|
14
|
+
/** The config, with the module's own overrides merged over it. */
|
|
15
|
+
declare function nestTestConfig(options: NestTestOptions): ViteUserConfig;
|
|
16
|
+
|
|
17
|
+
export { type NestTestOptions, nestTestConfig };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decoratorMetadata,
|
|
3
|
+
tsconfigAliases
|
|
4
|
+
} from "../../../chunk-3TDUIKVQ.js";
|
|
5
|
+
|
|
6
|
+
// src/roles/test/nest/toolchain.ts
|
|
7
|
+
import { defineConfig, mergeConfig } from "vitest/config";
|
|
8
|
+
import { loadEnv } from "vite";
|
|
9
|
+
|
|
10
|
+
// src/roles/test/nest/nest-test-config.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { createRequire } from "module";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
function vitestMockExtendedPath() {
|
|
16
|
+
try {
|
|
17
|
+
return fileURLToPath(import.meta.resolve("vitest-mock-extended"));
|
|
18
|
+
} catch {
|
|
19
|
+
try {
|
|
20
|
+
return createRequire(import.meta.url).resolve("vitest-mock-extended");
|
|
21
|
+
} catch {
|
|
22
|
+
return "vitest-mock-extended";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function axiosAlias(root) {
|
|
27
|
+
const manifest = join(root, "node_modules", "axios", "package.json");
|
|
28
|
+
return existsSync(manifest) ? [{ find: /^axios$/, replacement: dirname(manifest) }] : [];
|
|
29
|
+
}
|
|
30
|
+
function baseConfig(options) {
|
|
31
|
+
const { root, workspaceRoot } = options;
|
|
32
|
+
return {
|
|
33
|
+
/*
|
|
34
|
+
* Nest reads constructor parameter types from `emitDecoratorMetadata`, and esbuild does not
|
|
35
|
+
* emit it. Without this plugin every injected dependency arrives `undefined`, which surfaces
|
|
36
|
+
* far from its cause. It is the BUILD role's plugin, imported rather than reimplemented: it
|
|
37
|
+
* already carries the two compile modes copied from nx, and a second copy here would be a
|
|
38
|
+
* second thing to keep in step.
|
|
39
|
+
*/
|
|
40
|
+
plugins: [decoratorMetadata({ root })],
|
|
41
|
+
resolve: {
|
|
42
|
+
alias: [
|
|
43
|
+
...axiosAlias(root),
|
|
44
|
+
/*
|
|
45
|
+
* `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491
|
|
46
|
+
* files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers
|
|
47
|
+
* breaks every module still on jest and leaving them breaks every module moved to Vitest.
|
|
48
|
+
* Old and new therefore do not cohabit on shared helpers, which would have killed the
|
|
49
|
+
* per-module plan.
|
|
50
|
+
*
|
|
51
|
+
* This one line removes the constraint: an unmigrated helper resolves to the Vitest fork
|
|
52
|
+
* inside an adopted module and keeps resolving to the jest one everywhere else.
|
|
53
|
+
* `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.
|
|
54
|
+
*
|
|
55
|
+
* Measured on `mission`: failing suites went from 159 to 10.
|
|
56
|
+
*/
|
|
57
|
+
{ find: /^jest-mock-extended$/, replacement: vitestMockExtendedPath() },
|
|
58
|
+
...tsconfigAliases(workspaceRoot)
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
test: {
|
|
62
|
+
globals: true,
|
|
63
|
+
environment: "node",
|
|
64
|
+
root,
|
|
65
|
+
/*
|
|
66
|
+
* Both extensions, because jest ran both. `**\/*.spec.ts` alone read green while missing
|
|
67
|
+
* three `.test.ts` files and 23 tests, with nothing saying so: a counter does not compare to
|
|
68
|
+
* itself, it compares to the reference.
|
|
69
|
+
*/
|
|
70
|
+
include: ["**/*.spec.ts", "**/*.test.ts"],
|
|
71
|
+
/*
|
|
72
|
+
* Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:
|
|
73
|
+
* a decorator writes into a catalog when its module loads. Sharing a module registry across
|
|
74
|
+
* files lets one suite see what another registered, and the failure appears in whichever
|
|
75
|
+
* file happens to run second.
|
|
76
|
+
*/
|
|
77
|
+
isolate: true
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function asAliasArray(alias) {
|
|
82
|
+
if (alias === void 0) return [];
|
|
83
|
+
if (Array.isArray(alias)) return alias;
|
|
84
|
+
return Object.entries(alias).map(([find, replacement]) => ({
|
|
85
|
+
find,
|
|
86
|
+
replacement
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
function nestTestConfig(options) {
|
|
90
|
+
const base = baseConfig(options);
|
|
91
|
+
if (options.overrides === void 0) return base;
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
...options.overrides,
|
|
95
|
+
plugins: [...base.plugins ?? [], ...options.overrides.plugins ?? []],
|
|
96
|
+
resolve: {
|
|
97
|
+
...base.resolve,
|
|
98
|
+
...options.overrides.resolve,
|
|
99
|
+
// The module's own aliases come AFTER sentinel's, so a module that needs to win can.
|
|
100
|
+
alias: [
|
|
101
|
+
...asAliasArray(base.resolve?.alias),
|
|
102
|
+
...asAliasArray(options.overrides.resolve?.alias)
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
test: { ...base.test, ...options.overrides.test }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
decoratorMetadata,
|
|
110
|
+
defineConfig,
|
|
111
|
+
loadEnv,
|
|
112
|
+
mergeConfig,
|
|
113
|
+
nestTestConfig,
|
|
114
|
+
tsconfigAliases
|
|
115
|
+
};
|
|
116
|
+
//# sourceMappingURL=toolchain.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as msw_node from 'msw/node';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* No default handlers, deliberately.
|
|
5
|
+
*
|
|
6
|
+
* The repo's own server had `defaultHandlers = []` too, so nothing is lost. A handler here would
|
|
7
|
+
* be a Hublo endpoint inside a package meant to be usable by anyone, which is the line between
|
|
8
|
+
* what sentinel owns (the config) and what an app owns (what it mocks).
|
|
9
|
+
*
|
|
10
|
+
* A suite adds its own with `server.use(...)`, which is how all 600 files that touch this already
|
|
11
|
+
* work.
|
|
12
|
+
*/
|
|
13
|
+
declare const server: msw_node.SetupServer;
|
|
14
|
+
|
|
15
|
+
export { server };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
installMswLifecycle,
|
|
3
|
+
server
|
|
4
|
+
} from "../../../chunk-NKFGYPIQ.js";
|
|
5
|
+
|
|
6
|
+
// src/roles/test/setup/nest.ts
|
|
7
|
+
import { afterAll, afterEach, beforeAll } from "vitest";
|
|
8
|
+
|
|
9
|
+
// src/roles/test/setup/workspace.ts
|
|
10
|
+
function isMissingModule(error) {
|
|
11
|
+
return error?.code === "ERR_MODULE_NOT_FOUND";
|
|
12
|
+
}
|
|
13
|
+
async function optional(specifier, use) {
|
|
14
|
+
try {
|
|
15
|
+
use(await import(
|
|
16
|
+
/* @vite-ignore */
|
|
17
|
+
specifier
|
|
18
|
+
));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (!isMissingModule(error)) throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function loadEnvironment() {
|
|
24
|
+
await optional("dotenv-flow", (module) => {
|
|
25
|
+
const flow = module;
|
|
26
|
+
const config = flow.config ?? flow.default?.config;
|
|
27
|
+
config?.({ silent: true, purge_dotenv: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function pinLuxonZone() {
|
|
31
|
+
await optional("luxon", (module) => {
|
|
32
|
+
const settings = module.Settings;
|
|
33
|
+
if (settings) settings.defaultZone = "utc";
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function installWorkspaceSetup() {
|
|
37
|
+
await loadEnvironment();
|
|
38
|
+
await pinLuxonZone();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/roles/test/setup/nest.ts
|
|
42
|
+
installMswLifecycle({ beforeAll, afterEach, afterAll });
|
|
43
|
+
await installWorkspaceSetup();
|
|
44
|
+
export {
|
|
45
|
+
server
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=nest.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
/** A TypeScript transformer a service runs at build time, as its build target declares it. */
|
|
4
|
+
interface TransformerDeclaration {
|
|
5
|
+
/** The package to load it from, e.g. `@nestjs/swagger/plugin`. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Passed to the transformer's own factory, unread here. */
|
|
8
|
+
options?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
interface DecoratorMetadataOptions {
|
|
11
|
+
/** The module being built. Its tsconfig is the one whose emit must be preserved. */
|
|
12
|
+
root: string;
|
|
13
|
+
/**
|
|
14
|
+
* An explicit tsconfig, when the service does not use either conventional name.
|
|
15
|
+
*
|
|
16
|
+
* Relative paths resolve against `root`.
|
|
17
|
+
*/
|
|
18
|
+
tsconfig?: string;
|
|
19
|
+
/** The transformers this service declared, translated from its webpack target. */
|
|
20
|
+
transformers?: readonly TransformerDeclaration[];
|
|
21
|
+
}
|
|
22
|
+
declare const decoratorMetadata: (options: DecoratorMetadataOptions) => Plugin;
|
|
23
|
+
|
|
24
|
+
interface Alias {
|
|
25
|
+
find: RegExp;
|
|
26
|
+
replacement: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read the mappings from the workspace's base tsconfig.
|
|
30
|
+
*
|
|
31
|
+
* Longest pattern first, because Vite takes the first alias that matches and the mappings
|
|
32
|
+
* overlap by design: `@front/theme/node` must not be swallowed by `@front/theme`.
|
|
33
|
+
*/
|
|
34
|
+
declare const tsconfigAliases: (workspaceRoot: string, file?: string) => Alias[];
|
|
35
|
+
|
|
36
|
+
export { type Alias as A, type DecoratorMetadataOptions as D, decoratorMetadata as d, tsconfigAliases as t };
|
package/lint/react-lib.json
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"all"
|
|
59
59
|
],
|
|
60
60
|
"for-direction": "error",
|
|
61
|
+
"hublo/no-regex-lookbehind": "error",
|
|
61
62
|
"jest-dom/prefer-checked": "error",
|
|
62
63
|
"jest-dom/prefer-empty": "error",
|
|
63
64
|
"jest-dom/prefer-enabled-disabled": "error",
|
|
@@ -348,7 +349,8 @@
|
|
|
348
349
|
{
|
|
349
350
|
"name": "storybook",
|
|
350
351
|
"specifier": "eslint-plugin-storybook"
|
|
351
|
-
}
|
|
352
|
+
},
|
|
353
|
+
"../plugins/hublo.js"
|
|
352
354
|
],
|
|
353
355
|
"overrides": [
|
|
354
356
|
{
|
package/lint/react.json
CHANGED
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
"all"
|
|
59
59
|
],
|
|
60
60
|
"for-direction": "error",
|
|
61
|
+
"hublo/no-palette-common": "off",
|
|
62
|
+
"hublo/no-regex-lookbehind": "error",
|
|
61
63
|
"jest/expect-expect": "off",
|
|
62
64
|
"jest/no-alias-methods": "off",
|
|
63
65
|
"jest/no-commented-out-tests": "off",
|
|
@@ -469,6 +471,7 @@
|
|
|
469
471
|
{
|
|
470
472
|
"name": "react-refresh",
|
|
471
473
|
"specifier": "eslint-plugin-react-refresh"
|
|
472
|
-
}
|
|
474
|
+
},
|
|
475
|
+
"../plugins/hublo.js"
|
|
473
476
|
]
|
|
474
477
|
}
|