@stratal/inertia 0.0.26 → 0.1.0
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/CHANGELOG.md +359 -0
- package/README.md +158 -14
- package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -1
- package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/generator/type-generator.worker.d.mts +1 -1
- package/dist/generator/type-generator.worker.mjs +1 -1
- package/dist/index.d.mts +214 -92
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +390 -130
- package/dist/index.mjs.map +1 -1
- package/dist/quarry.d.mts +6 -8
- package/dist/quarry.d.mts.map +1 -1
- package/dist/quarry.mjs +127 -12
- package/dist/quarry.mjs.map +1 -1
- package/dist/react/access.d.mts +95 -0
- package/dist/react/access.d.mts.map +1 -0
- package/dist/react/access.mjs +133 -0
- package/dist/react/access.mjs.map +1 -0
- package/dist/react-dom-server-legacy-stub.d.mts +20 -0
- package/dist/react-dom-server-legacy-stub.d.mts.map +1 -0
- package/dist/react-dom-server-legacy-stub.mjs +25 -0
- package/dist/react-dom-server-legacy-stub.mjs.map +1 -0
- package/dist/react.d.mts +4 -6
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/seo-runtime.d.mts +1 -1
- package/dist/seo-runtime.mjs +8 -6
- package/dist/seo-runtime.mjs.map +1 -1
- package/dist/services/ssr-exclusion.d.mts +38 -0
- package/dist/services/ssr-exclusion.d.mts.map +1 -0
- package/dist/services/ssr-exclusion.mjs +0 -0
- package/dist/services/ssr-exclusion.mjs.map +1 -0
- package/dist/ssr.d.mts +37 -8
- package/dist/ssr.d.mts.map +1 -1
- package/dist/ssr.mjs +7 -4
- package/dist/ssr.mjs.map +1 -1
- package/dist/testing.d.mts +3 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +20 -6
- package/dist/testing.mjs.map +1 -1
- package/dist/{type-generator-DFpha_Fp.mjs → type-generator-BVw8mj1y.mjs} +373 -64
- package/dist/type-generator-BVw8mj1y.mjs.map +1 -0
- package/dist/types-BltKoOR7.d.mts +193 -0
- package/dist/types-BltKoOR7.d.mts.map +1 -0
- package/dist/types-D-j_Ee_h.d.mts +52 -0
- package/dist/types-D-j_Ee_h.d.mts.map +1 -0
- package/dist/types-DzE1pdZs.d.mts.map +1 -1
- package/dist/vite.d.mts +19 -6
- package/dist/vite.d.mts.map +1 -1
- package/dist/vite.mjs +67 -5
- package/dist/vite.mjs.map +1 -1
- package/package.json +38 -27
- package/dist/type-generator-DFpha_Fp.mjs.map +0 -1
- package/dist/types-BhgXhWx6.d.mts +0 -82
- package/dist/types-BhgXhWx6.d.mts.map +0 -1
package/dist/quarry.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { t as __decorate } from "./decorate-
|
|
2
|
-
import { n as runTypeGeneration, t as findPagesDir } from "./type-generator-
|
|
1
|
+
import { t as __decorate } from "./decorate-RQD1h28J.mjs";
|
|
2
|
+
import { n as runTypeGeneration, t as findPagesDir } from "./type-generator-BVw8mj1y.mjs";
|
|
3
3
|
import { Module } from "stratal/module";
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, relative } from "node:path";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
6
|
+
import { execFile, spawn } from "node:child_process";
|
|
7
7
|
import { Command } from "stratal/quarry";
|
|
8
|
+
import { promisify } from "node:util";
|
|
8
9
|
import { watch } from "node:fs/promises";
|
|
9
10
|
//#region src/vite/create-client-vite-config.ts
|
|
10
11
|
/**
|
|
@@ -17,12 +18,13 @@ import { watch } from "node:fs/promises";
|
|
|
17
18
|
* the two phases removes the race entirely and keeps each build minimal.
|
|
18
19
|
*/
|
|
19
20
|
function writeTempClientViteConfig(options) {
|
|
20
|
-
const
|
|
21
|
+
const configDir = join(options.cwd, "node_modules", ".stratal");
|
|
22
|
+
const configPath = join(configDir, "vite.client.config.mjs");
|
|
21
23
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
22
24
|
const entry = (options.entry ?? "src/inertia/app.tsx").replace(/\\/g, "/");
|
|
23
25
|
const outDir = (options.outDir ?? "dist/client").replace(/\\/g, "/");
|
|
24
26
|
const hasUserConfig = existsSync(join(options.cwd, "vite.config.ts"));
|
|
25
|
-
|
|
27
|
+
const content = `
|
|
26
28
|
import { mergeConfig } from 'vite'
|
|
27
29
|
|
|
28
30
|
const baseConfig = {
|
|
@@ -40,13 +42,15 @@ const baseConfig = {
|
|
|
40
42
|
${hasUserConfig ? `const userModule = await import('${join(options.cwd, "vite.config.ts").replace(/\\/g, "/")}')
|
|
41
43
|
const userConfig = userModule.default ?? userModule
|
|
42
44
|
export default mergeConfig(userConfig, baseConfig)` : "export default baseConfig"}
|
|
43
|
-
|
|
45
|
+
`;
|
|
46
|
+
writeFileSync(configPath, content, "utf-8");
|
|
44
47
|
return configPath;
|
|
45
48
|
}
|
|
46
49
|
//#endregion
|
|
47
50
|
//#region src/vite/create-vite-config.ts
|
|
48
51
|
function writeTempViteConfig(options) {
|
|
49
|
-
const
|
|
52
|
+
const configDir = join(options.cwd, "node_modules", ".stratal");
|
|
53
|
+
const configPath = join(configDir, "vite.config.mjs");
|
|
50
54
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
51
55
|
const hasUserConfig = existsSync(join(options.cwd, "vite.config.ts"));
|
|
52
56
|
const serverConfig = options.server ? `server: { port: ${options.server.port}, host: ${options.server.host ? "true" : "undefined"} },` : "";
|
|
@@ -54,7 +58,7 @@ function writeTempViteConfig(options) {
|
|
|
54
58
|
const cloudflareOptions = [];
|
|
55
59
|
if (options.persistTo) cloudflareOptions.push(`persistState: { path: ${JSON.stringify(options.persistTo)} }`);
|
|
56
60
|
if (options.inspectorPort !== void 0) cloudflareOptions.push(`inspectorPort: ${options.inspectorPort === false ? "false" : options.inspectorPort}`);
|
|
57
|
-
|
|
61
|
+
const content = `
|
|
58
62
|
import { mergeConfig } from 'vite'
|
|
59
63
|
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
60
64
|
import { stratalInertia } from '@stratal/inertia/vite'
|
|
@@ -82,7 +86,8 @@ const baseConfig = {
|
|
|
82
86
|
${hasUserConfig ? `const userModule = await import('${join(options.cwd, "vite.config.ts").replace(/\\/g, "/")}')
|
|
83
87
|
const userConfig = userModule.default ?? userModule
|
|
84
88
|
export default mergeConfig(baseConfig, userConfig)` : "export default baseConfig"}
|
|
85
|
-
|
|
89
|
+
`;
|
|
90
|
+
writeFileSync(configPath, content, "utf-8");
|
|
86
91
|
return configPath;
|
|
87
92
|
}
|
|
88
93
|
//#endregion
|
|
@@ -160,9 +165,92 @@ var InertiaBuildCommand = class extends Command {
|
|
|
160
165
|
}
|
|
161
166
|
};
|
|
162
167
|
//#endregion
|
|
168
|
+
//#region src/commands/worker-recycler.ts
|
|
169
|
+
const execFileAsync = promisify(execFile);
|
|
170
|
+
function parsePsProcs(stdout) {
|
|
171
|
+
const procs = [];
|
|
172
|
+
for (const line of stdout.split("\n")) {
|
|
173
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
174
|
+
if (m) procs.push({
|
|
175
|
+
pid: Number(m[1]),
|
|
176
|
+
ppid: Number(m[2]),
|
|
177
|
+
rss: Number(m[3]),
|
|
178
|
+
cmd: m[4]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return procs;
|
|
182
|
+
}
|
|
183
|
+
function maxWorkerdRssMb(procs, rootPid) {
|
|
184
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const p of procs) {
|
|
186
|
+
const arr = childrenOf.get(p.ppid);
|
|
187
|
+
if (arr) arr.push(p.pid);
|
|
188
|
+
else childrenOf.set(p.ppid, [p.pid]);
|
|
189
|
+
}
|
|
190
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
191
|
+
const stack = [rootPid];
|
|
192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
193
|
+
let maxRssKb = 0;
|
|
194
|
+
while (stack.length) {
|
|
195
|
+
const pid = stack.pop();
|
|
196
|
+
if (seen.has(pid)) continue;
|
|
197
|
+
seen.add(pid);
|
|
198
|
+
const p = byPid.get(pid);
|
|
199
|
+
if (p && p.cmd.includes("workerd") && p.rss > maxRssKb) maxRssKb = p.rss;
|
|
200
|
+
for (const c of childrenOf.get(pid) ?? []) stack.push(c);
|
|
201
|
+
}
|
|
202
|
+
return maxRssKb > 0 ? Math.round(maxRssKb / 1024) : null;
|
|
203
|
+
}
|
|
204
|
+
async function readWorkerRssMb(rootPid) {
|
|
205
|
+
try {
|
|
206
|
+
const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,rss=,command="], { maxBuffer: 67108864 });
|
|
207
|
+
return maxWorkerdRssMb(parsePsProcs(stdout), rootPid);
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Start sampling the worker's RSS and recycle it (via a Vite restart) before it
|
|
214
|
+
* can reach the V8 OOM ceiling. Returns a stop function.
|
|
215
|
+
*/
|
|
216
|
+
function startWorkerRecycler(opts) {
|
|
217
|
+
const intervalMs = opts.intervalMs ?? 1500;
|
|
218
|
+
const cooldownMs = opts.cooldownMs ?? 3e3;
|
|
219
|
+
const rootPid = opts.child.pid;
|
|
220
|
+
const debug = process.env.STRATAL_RECYCLER_DEBUG === "1";
|
|
221
|
+
let lastRecycle = 0;
|
|
222
|
+
let stopped = false;
|
|
223
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
224
|
+
opts.log(`memory supervision unavailable on ${process.platform} (needs macOS/Linux)`);
|
|
225
|
+
return () => {};
|
|
226
|
+
}
|
|
227
|
+
if (rootPid == null) return () => {};
|
|
228
|
+
const timer = setInterval(() => {
|
|
229
|
+
if (stopped) return;
|
|
230
|
+
readWorkerRssMb(rootPid).then((rssMb) => {
|
|
231
|
+
if (stopped || rssMb == null) return;
|
|
232
|
+
if (debug) opts.log(`worker RSS ${rssMb}MB / limit ${opts.heapLimitMb}MB`);
|
|
233
|
+
if (rssMb >= opts.heapLimitMb && Date.now() - lastRecycle > cooldownMs) {
|
|
234
|
+
lastRecycle = Date.now();
|
|
235
|
+
opts.log(`recycling dev worker (${rssMb}MB)`);
|
|
236
|
+
try {
|
|
237
|
+
opts.child.stdin?.write("r\n");
|
|
238
|
+
} catch (e) {
|
|
239
|
+
opts.log(`failed to signal Vite restart: ${e.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}, intervalMs);
|
|
244
|
+
return () => {
|
|
245
|
+
stopped = true;
|
|
246
|
+
clearInterval(timer);
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
163
250
|
//#region src/commands/inertia-dev.command.ts
|
|
251
|
+
const DEFAULT_HEAP_LIMIT_MB = 900;
|
|
164
252
|
var InertiaDevCommand = class extends Command {
|
|
165
|
-
static command = "inertia:dev {--port= : Dev server port} {--host : Expose to network} {--inspector-port= : Worker debugger inspector port (number, or \"false\" to disable). Set a distinct value per worker to avoid EADDRINUSE when running multiple Inertia workers concurrently.} {--persist-to= : Shared persist directory for @cloudflare/vite-plugin (relative to cwd; the plugin appends /v3). Use to share R2/KV/cache emulator state across multiple workers in dev.}";
|
|
253
|
+
static command = "inertia:dev {--port= : Dev server port} {--host : Expose to network} {--inspector-port= : Worker debugger inspector port (number, or \"false\" to disable). Set a distinct value per worker to avoid EADDRINUSE when running multiple Inertia workers concurrently.} {--persist-to= : Shared persist directory for @cloudflare/vite-plugin (relative to cwd; the plugin appends /v3). Use to share R2/KV/cache emulator state across multiple workers in dev.} {--heap-limit= : Recycle the dev worker isolate when its memory (MB) reaches this, to prevent the HMR-driven V8 OOM. Default 900.}";
|
|
166
254
|
static description = "Start Inertia.js Vite development server";
|
|
167
255
|
async handle() {
|
|
168
256
|
const port = this.number("port");
|
|
@@ -178,6 +266,12 @@ var InertiaDevCommand = class extends Command {
|
|
|
178
266
|
return 1;
|
|
179
267
|
}
|
|
180
268
|
}
|
|
269
|
+
const heapLimitRaw = this.string("heap-limit");
|
|
270
|
+
const heapLimitMb = heapLimitRaw ? Number(heapLimitRaw) : DEFAULT_HEAP_LIMIT_MB;
|
|
271
|
+
if (!Number.isFinite(heapLimitMb) || heapLimitMb <= 0) {
|
|
272
|
+
this.fail(`Invalid --heap-limit "${heapLimitRaw}". Expected a positive number of MB.`);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
181
275
|
const cwd = process.cwd();
|
|
182
276
|
if (!existsSync(join(cwd, "src/inertia/app.tsx"))) {
|
|
183
277
|
this.fail("src/inertia/app.tsx not found. Run `quarry inertia:install` first.");
|
|
@@ -203,14 +297,34 @@ var InertiaDevCommand = class extends Command {
|
|
|
203
297
|
return new Promise((resolve) => {
|
|
204
298
|
const child = spawn("npx", args, {
|
|
205
299
|
cwd,
|
|
206
|
-
stdio:
|
|
300
|
+
stdio: [
|
|
301
|
+
"pipe",
|
|
302
|
+
"inherit",
|
|
303
|
+
"inherit"
|
|
304
|
+
],
|
|
207
305
|
shell: true
|
|
208
306
|
});
|
|
307
|
+
if (child.stdin && process.stdin.isTTY) {
|
|
308
|
+
process.stdin.pipe(child.stdin);
|
|
309
|
+
child.stdin.on("error", () => {});
|
|
310
|
+
process.stdin.unref();
|
|
311
|
+
}
|
|
312
|
+
const stopRecycler = startWorkerRecycler({
|
|
313
|
+
child,
|
|
314
|
+
heapLimitMb,
|
|
315
|
+
log: (msg) => this.info(`[worker-recycler] ${msg}`)
|
|
316
|
+
});
|
|
317
|
+
const cleanup = () => {
|
|
318
|
+
stopRecycler();
|
|
319
|
+
if (child.stdin && process.stdin.isTTY) process.stdin.unpipe(child.stdin);
|
|
320
|
+
};
|
|
209
321
|
child.on("error", (err) => {
|
|
322
|
+
cleanup();
|
|
210
323
|
this.fail(`Failed to start dev server: ${err.message}`);
|
|
211
324
|
resolve(1);
|
|
212
325
|
});
|
|
213
326
|
child.on("close", (code) => {
|
|
327
|
+
cleanup();
|
|
214
328
|
resolve(code ?? 0);
|
|
215
329
|
});
|
|
216
330
|
});
|
|
@@ -403,7 +517,8 @@ var InertiaTypesCommand = class extends Command {
|
|
|
403
517
|
static description = "Generate Inertia.js page type definitions";
|
|
404
518
|
async handle() {
|
|
405
519
|
const cwd = process.cwd();
|
|
406
|
-
|
|
520
|
+
const pagesDir = findPagesDir(cwd);
|
|
521
|
+
if (!existsSync(pagesDir)) {
|
|
407
522
|
this.fail("src/inertia/pages/ not found. Run `quarry inertia:install` first.");
|
|
408
523
|
return 1;
|
|
409
524
|
}
|
package/dist/quarry.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quarry.mjs","names":[],"sources":["../src/vite/create-client-vite-config.ts","../src/vite/create-vite-config.ts","../src/commands/inertia-build.command.ts","../src/commands/inertia-dev.command.ts","../src/commands/inertia-install.command.ts","../src/commands/inertia-types.command.ts","../src/quarry.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nexport interface TempClientViteConfigOptions {\n cwd: string\n entry?: string\n outDir?: string\n}\n\n/**\n * Emits a standalone Vite config for building the Inertia browser bundle.\n *\n * This runs as a separate `vite build` invocation BEFORE the worker build so\n * the worker's `stratal:inertia-inject-manifest` plugin has a finished\n * `<outDir>/.vite/manifest.json` to read. `@cloudflare/vite-plugin` builds its\n * environments in parallel, which made a single-config build racy — splitting\n * the two phases removes the race entirely and keeps each build minimal.\n */\nexport function writeTempClientViteConfig(options: TempClientViteConfigOptions): string {\n const configDir = join(options.cwd, 'node_modules', '.stratal')\n const configPath = join(configDir, 'vite.client.config.mjs')\n mkdirSync(dirname(configPath), { recursive: true })\n\n const entry = (options.entry ?? 'src/inertia/app.tsx').replace(/\\\\/g, '/')\n const outDir = (options.outDir ?? 'dist/client').replace(/\\\\/g, '/')\n const hasUserConfig = existsSync(join(options.cwd, 'vite.config.ts'))\n const publicDir = join(options.cwd, 'src', 'inertia', 'public').replace(/\\\\/g, '/')\n\n const content = `\nimport { mergeConfig } from 'vite'\n\nconst baseConfig = {\n publicDir: '${publicDir}',\n build: {\n outDir: '${outDir}',\n manifest: true,\n emptyOutDir: true,\n rollupOptions: {\n input: { app: '${entry}' },\n },\n },\n}\n\n${hasUserConfig\n ? `const userModule = await import('${join(options.cwd, 'vite.config.ts').replace(/\\\\/g, '/')}')\nconst userConfig = userModule.default ?? userModule\nexport default mergeConfig(userConfig, baseConfig)`\n : 'export default baseConfig'\n }\n`\n\n writeFileSync(configPath, content, 'utf-8')\n return configPath\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nexport interface TempViteConfigOptions {\n cwd: string\n server?: { port?: number; host?: boolean }\n outDir?: string\n persistTo?: string\n /**\n * Worker debugger inspector port passed to `@cloudflare/vite-plugin`.\n * Pass a distinct number per worker to avoid the `EADDRINUSE` race that\n * happens when several Inertia workers boot concurrently and all probe the\n * default port (9229). Pass `false` to disable the inspector entirely.\n * Left `undefined` preserves the plugin's default auto-pick behaviour.\n */\n inspectorPort?: number | false\n /**\n * Path (relative to `cwd`) to the Vite client manifest the worker bundle\n * should inline. Defaults to `dist/client/.vite/manifest.json`, matching\n * what `quarry inertia:build` emits in phase 1.\n */\n clientManifestPath?: string\n}\n\nexport function writeTempViteConfig(options: TempViteConfigOptions): string {\n const configDir = join(options.cwd, 'node_modules', '.stratal')\n const configPath = join(configDir, 'vite.config.mjs')\n mkdirSync(dirname(configPath), { recursive: true })\n\n const hasUserConfig = existsSync(join(options.cwd, 'vite.config.ts'))\n\n const serverConfig = options.server\n ? `server: { port: ${options.server.port}, host: ${options.server.host ? 'true' : 'undefined'} },`\n : ''\n\n const outDirConfig = options.outDir\n ? `outDir: '${options.outDir}',`\n : ''\n\n const cloudflareOptions: string[] = []\n if (options.persistTo) {\n cloudflareOptions.push(`persistState: { path: ${JSON.stringify(options.persistTo)} }`)\n }\n if (options.inspectorPort !== undefined) {\n cloudflareOptions.push(`inspectorPort: ${options.inspectorPort === false ? 'false' : options.inspectorPort}`)\n }\n const cloudflareArgs = cloudflareOptions.length ? `{ ${cloudflareOptions.join(', ')} }` : ''\n\n const stratalArgs = options.clientManifestPath\n ? `{ clientManifestPath: ${JSON.stringify(options.clientManifestPath)} }`\n : ''\n\n const content = `\nimport { mergeConfig } from 'vite'\nimport { cloudflare } from '@cloudflare/vite-plugin'\nimport { stratalInertia } from '@stratal/inertia/vite'\n\nlet inertiaPlugin = null\ntry {\n const mod = await import('@inertiajs/vite')\n const inertia = mod.default ?? mod\n inertiaPlugin = inertia()\n} catch {}\n\nconst baseConfig = {\n publicDir: 'src/inertia/public',\n plugins: [\n cloudflare(${cloudflareArgs}),\n ...(inertiaPlugin ? [inertiaPlugin] : []),\n ...stratalInertia(${stratalArgs}),\n ],\n build: {\n ${outDirConfig}\n },\n ${serverConfig}\n}\n\n${hasUserConfig\n ? `const userModule = await import('${join(options.cwd, 'vite.config.ts').replace(/\\\\/g, '/')}')\nconst userConfig = userModule.default ?? userModule\nexport default mergeConfig(baseConfig, userConfig)`\n : 'export default baseConfig'\n }\n`\n\n writeFileSync(configPath, content, 'utf-8')\n return configPath\n}\n","import { spawn } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { writeTempClientViteConfig } from '../vite/create-client-vite-config'\nimport { writeTempViteConfig } from '../vite/create-vite-config'\n\nexport class InertiaBuildCommand extends Command {\n static command = 'inertia:build {--outDir=dist : Output directory} {--ssr : Also build SSR bundle}'\n static description = 'Build Inertia.js frontend for production'\n\n async handle(): Promise<number | undefined> {\n const outDir = this.string('outDir') || 'dist'\n const shouldBuildSsr = this.boolean('ssr')\n const cwd = process.cwd()\n\n const entryPath = 'src/inertia/app.tsx'\n if (!existsSync(join(cwd, entryPath))) {\n this.fail('src/inertia/app.tsx not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n // Phase 1: standalone browser-bundle build. Runs without the Cloudflare\n // vite-plugin so it isn't subject to its parallel env orchestration. The\n // resulting `<clientOutDir>/.vite/manifest.json` is what the worker build\n // (phase 2) inlines into the worker entry via `stratal:inertia-inject-manifest`.\n const clientOutDir = join(outDir, 'client').replace(/\\\\/g, '/')\n const clientConfigPath = writeTempClientViteConfig({\n cwd,\n entry: entryPath,\n outDir: clientOutDir,\n })\n\n this.info('Building Inertia.js browser bundle...')\n const browserCode = await this.spawnVite(cwd, clientConfigPath, ['build'])\n if (browserCode !== 0) {\n this.fail('Browser bundle build failed.')\n return browserCode\n }\n this.success(`Browser bundle written to ${clientOutDir}/`)\n\n // Phase 2: worker build (Cloudflare vite-plugin). The injector plugin\n // reads the manifest produced in phase 1 and inlines it onto the worker\n // entry chunk.\n const configPath = writeTempViteConfig({\n cwd,\n outDir,\n clientManifestPath: join(clientOutDir, '.vite', 'manifest.json').replace(/\\\\/g, '/'),\n })\n\n this.info('Building Cloudflare worker bundle...')\n const workerCode = await this.spawnVite(cwd, configPath, ['build'])\n if (workerCode !== 0) {\n this.fail('Worker build failed.')\n return workerCode\n }\n this.success('Worker build complete!')\n\n if (shouldBuildSsr) {\n this.info('Building SSR bundle...')\n const ssrCode = await this.spawnVite(cwd, configPath, ['build', '--ssr'])\n if (ssrCode !== 0) {\n this.fail('SSR build failed.')\n return ssrCode\n }\n this.success('SSR build complete!')\n }\n\n this.success(`Output in ${outDir}/`)\n this.info('Deploy with: npx wrangler deploy')\n return 0\n }\n\n private spawnVite(cwd: string, configPath: string, args: string[]): Promise<number> {\n return new Promise((resolve) => {\n const child = spawn('npx', ['vite', '--config', configPath, ...args], {\n cwd,\n stdio: 'inherit',\n shell: true,\n })\n\n child.on('error', (err) => {\n this.fail(`Vite process error: ${err.message}`)\n resolve(1)\n })\n\n child.on('close', (code) => {\n resolve(code ?? 0)\n })\n })\n }\n}\n","import { spawn } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { writeTempViteConfig } from '../vite/create-vite-config'\n\nexport class InertiaDevCommand extends Command {\n static command = 'inertia:dev {--port= : Dev server port} {--host : Expose to network} {--inspector-port= : Worker debugger inspector port (number, or \"false\" to disable). Set a distinct value per worker to avoid EADDRINUSE when running multiple Inertia workers concurrently.} {--persist-to= : Shared persist directory for @cloudflare/vite-plugin (relative to cwd; the plugin appends /v3). Use to share R2/KV/cache emulator state across multiple workers in dev.}'\n static description = 'Start Inertia.js Vite development server'\n\n async handle(): Promise<number | undefined> {\n const port = this.number('port')\n const host = this.boolean('host')\n const persistTo = this.string('persist-to')\n const inspectorPortRaw = this.string('inspector-port')\n let inspectorPort: number | false | undefined\n if (inspectorPortRaw === 'false') {\n inspectorPort = false\n } else if (inspectorPortRaw !== undefined) {\n inspectorPort = Number(inspectorPortRaw)\n if (!Number.isInteger(inspectorPort) || inspectorPort < 0 || inspectorPort > 65535) {\n this.fail(`Invalid --inspector-port \"${inspectorPortRaw}\". Expected an integer between 0 and 65535, or \"false\" to disable.`)\n return 1\n }\n }\n const cwd = process.cwd()\n\n const entryPath = 'src/inertia/app.tsx'\n if (!existsSync(join(cwd, entryPath))) {\n this.fail('src/inertia/app.tsx not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n const configPath = writeTempViteConfig({\n cwd,\n server: { port, host },\n persistTo,\n inspectorPort,\n })\n\n this.info('Starting Vite dev server...')\n\n const args = ['vite', 'dev', '--config', configPath]\n if (host) args.push('--host')\n\n return new Promise<number>((resolve) => {\n const child = spawn('npx', args, {\n cwd,\n stdio: 'inherit',\n shell: true,\n })\n\n child.on('error', (err) => {\n this.fail(`Failed to start dev server: ${err.message}`)\n resolve(1)\n })\n\n child.on('close', (code) => {\n resolve(code ?? 0)\n })\n })\n }\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { join, relative } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport type { SourceFile, SyntaxKind } from 'ts-morph'\nimport { runTypeGeneration } from '../generator/type-generator'\n\n/** Outcome of reconciling `src/app.module.ts` with the SSR-enabled InertiaModule. */\ntype AppModuleUpdate = 'created' | 'ssr-added' | 'unchanged' | 'unwired'\n/**\n * The subset of ts-morph's runtime `SyntaxKind` enum that `ensureSsrWiring` reads.\n * Declared structurally (via the enum-member literal types) so ts-morph stays a\n * type-only import here and is loaded lazily where it's actually used.\n */\ninterface SyntaxKinds {\n CallExpression: SyntaxKind.CallExpression\n ObjectLiteralExpression: SyntaxKind.ObjectLiteralExpression\n}\n\nconst ROOT_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n @viteHead\n @inertiaHead\n</head>\n<body>\n @inertia\n @viteScripts\n</body>\n</html>`\n\nconst APP_TSX = `import { createInertiaApp } from '@inertiajs/react'\n\ncreateInertiaApp({\n resolve: async (name) => {\n const pages = import.meta.glob('./pages/**/*.tsx')\n const page = await pages[\\`./pages/\\${name}.tsx\\`]?.()\n if (!page) throw new Error(\\`Page not found: \\${name}\\`)\n return page\n },\n})`\n\nconst SSR_TSX = `import { createInertiaSsrApp } from '@stratal/inertia/ssr'\n\nexport const { render } = createInertiaSsrApp({\n resolve: async (name) => {\n const pages = import.meta.glob('./pages/**/*.tsx')\n const page = await pages[\\`./pages/\\${name}.tsx\\`]?.()\n if (!page) throw new Error(\\`Page not found: \\${name}\\`)\n return page\n },\n})`\n\nconst HOME_TSX = `export default function Home({ message }: { message: string }) {\n return (\n <div>\n <h1>{message}</h1>\n <p>This page is rendered with Inertia.js and Stratal.</p>\n </div>\n )\n}`\n\nexport class InertiaInstallCommand extends Command {\n static command = 'inertia:install {--skip-deps : Skip installing npm dependencies}'\n static description = 'Scaffold Inertia.js files for a Stratal project'\n\n async handle(): Promise<number | undefined> {\n const skipDeps = this.boolean('skip-deps')\n const cwd = process.cwd()\n const inertiaDir = join(cwd, 'src', 'inertia')\n const pagesDir = join(inertiaDir, 'pages')\n\n // Create directories\n this.info('Creating src/inertia/ directory...')\n mkdirSync(pagesDir, { recursive: true })\n\n const publicDir = join(inertiaDir, 'public')\n mkdirSync(publicDir, { recursive: true })\n const gitkeepPath = join(publicDir, '.gitkeep')\n if (!existsSync(gitkeepPath)) {\n writeFileSync(gitkeepPath, '', 'utf-8')\n }\n this.success('Created src/inertia/public/')\n\n // Write template files\n const files = [\n { path: join(inertiaDir, 'root.html'), content: ROOT_HTML, name: 'root.html' },\n { path: join(inertiaDir, 'app.tsx'), content: APP_TSX, name: 'app.tsx' },\n { path: join(inertiaDir, 'ssr.tsx'), content: SSR_TSX, name: 'ssr.tsx' },\n { path: join(pagesDir, 'Home.tsx'), content: HOME_TSX, name: 'pages/Home.tsx' },\n ]\n\n for (const file of files) {\n if (existsSync(file.path)) {\n this.warn(`Skipping ${file.name} (already exists)`)\n } else {\n writeFileSync(file.path, file.content, 'utf-8')\n this.success(`Created src/inertia/${file.name}`)\n }\n }\n\n // Modify app.module.ts\n const appModulePath = join(cwd, 'src', 'app.module.ts')\n if (existsSync(appModulePath)) {\n this.info('Updating src/app.module.ts...')\n try {\n const result = await this.updateAppModule(appModulePath)\n if (result === 'created') {\n this.success('Updated src/app.module.ts with InertiaModule')\n } else if (result === 'ssr-added') {\n this.success('Enabled streaming SSR in src/app.module.ts')\n } else if (result === 'unchanged') {\n this.info('InertiaModule (with SSR) already configured in app.module.ts')\n } else {\n this.warn('InertiaModule is configured but SSR could not be auto-wired.')\n this.info(\"Add `ssr: { bundle: () => import('./inertia/ssr') }` to your InertiaModule options\")\n }\n } catch (err) {\n this.warn(`Could not auto-update app.module.ts: ${(err as Error).message}`)\n this.info('Please manually add InertiaModule.forRoot() to your module imports')\n }\n } else {\n this.info('No src/app.module.ts found — please manually configure InertiaModule')\n }\n\n // Generate initial type definitions\n try {\n const { outputPath, pageCount } = await runTypeGeneration(cwd)\n const relPath = relative(cwd, outputPath)\n this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? 's' : ''})`)\n } catch {\n this.warn('Could not generate initial type definitions. Run `quarry inertia:types` manually.')\n }\n\n if (!skipDeps) {\n this.newLine()\n this.info('Install the following dependencies:')\n this.line(' npm install @stratal/inertia @inertiajs/react @inertiajs/vite react react-dom')\n this.line(' npm install -D @types/react @types/react-dom vite @cloudflare/vite-plugin')\n }\n\n this.newLine()\n this.success('Inertia.js scaffolding complete!')\n this.info('Run `quarry inertia:dev` to start the dev server')\n\n return 0\n }\n\n private async updateAppModule(modulePath: string): Promise<AppModuleUpdate> {\n const { Project, SyntaxKind } = await import('ts-morph')\n\n const project = new Project({ useInMemoryFileSystem: false })\n const sourceFile = project.addSourceFileAtPath(modulePath)\n\n // Already importing the package — an older install that predates streaming\n // SSR. Wire the existing InertiaModule config to the SSR bundle rather than\n // bailing (which would leave SSR silently disabled).\n const existingImport = sourceFile.getImportDeclaration((decl) =>\n decl.getModuleSpecifierValue() === '@stratal/inertia',\n )\n if (existingImport) {\n const result = this.ensureSsrWiring(sourceFile, SyntaxKind)\n if (result === 'ssr-added') await sourceFile.save()\n return result\n }\n\n // Fresh install: add the imports and an InertiaModule.forRoot wired for SSR.\n sourceFile.addImportDeclaration({\n defaultImport: 'rootView',\n moduleSpecifier: './inertia/root.html?raw',\n })\n sourceFile.addImportDeclaration({\n namedImports: ['InertiaModule'],\n moduleSpecifier: '@stratal/inertia',\n })\n\n // Find the @Module decorator and add InertiaModule to imports\n const classes = sourceFile.getClasses()\n for (const cls of classes) {\n const moduleDecorator = cls.getDecorator('Module')\n if (!moduleDecorator) continue\n\n const args = moduleDecorator.getArguments()\n if (args.length === 0) continue\n\n const objLiteral = args[0].asKind(SyntaxKind.ObjectLiteralExpression)\n if (!objLiteral) continue\n\n const importsProp = objLiteral.getProperty('imports')\n if (importsProp) {\n // Add to existing imports array\n const initializer = importsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer()\n const arrayLiteral = initializer?.asKind(SyntaxKind.ArrayLiteralExpression)\n if (arrayLiteral) {\n arrayLiteral.addElement(`InertiaModule.forRoot({\\n rootView,\\n ssr: { bundle: () => import('./inertia/ssr') },\\n })`)\n }\n } else {\n // Add imports property\n objLiteral.addPropertyAssignment({\n name: 'imports',\n initializer: `[\\n InertiaModule.forRoot({\\n rootView,\\n ssr: { bundle: () => import('./inertia/ssr') },\\n }),\\n ]`,\n })\n }\n\n break\n }\n\n await sourceFile.save()\n return 'created'\n }\n\n /**\n * Ensure an existing `InertiaModule.forRoot({...})` call opts into the streaming\n * SSR bundle. Returns `ssr-added` when the option is inserted, `unchanged` when\n * one is already present, or `unwired` when no plain `forRoot({...})` object\n * literal is found (e.g. `forRootAsync`, or a config passed by reference) — in\n * which case the caller surfaces a manual instruction.\n */\n private ensureSsrWiring(sourceFile: SourceFile, syntaxKind: SyntaxKinds): AppModuleUpdate {\n const calls = sourceFile.getDescendantsOfKind(syntaxKind.CallExpression)\n for (const call of calls) {\n if (call.getExpression().getText() !== 'InertiaModule.forRoot') continue\n\n const objLiteral = call.getArguments()[0]?.asKind(syntaxKind.ObjectLiteralExpression)\n if (!objLiteral) continue\n\n if (objLiteral.getProperty('ssr')) return 'unchanged'\n\n objLiteral.addPropertyAssignment({\n name: 'ssr',\n initializer: `{ bundle: () => import('./inertia/ssr') }`,\n })\n return 'ssr-added'\n }\n return 'unwired'\n }\n}\n","import { existsSync } from 'node:fs'\nimport { watch } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { findPagesDir, runTypeGeneration } from '../generator/type-generator'\n\nexport class InertiaTypesCommand extends Command {\n static command = 'inertia:types {--watch : Watch for changes and regenerate}'\n static description = 'Generate Inertia.js page type definitions'\n\n async handle(): Promise<number | undefined> {\n const cwd = process.cwd()\n const pagesDir = findPagesDir(cwd)\n\n if (!existsSync(pagesDir)) {\n this.fail('src/inertia/pages/ not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n const result = await this.generate(cwd)\n if (!result) return 1\n\n if (this.boolean('watch')) {\n this.info('Watching for changes...')\n await this.watchForChanges(cwd)\n }\n\n return 0\n }\n\n private async generate(cwd: string): Promise<boolean> {\n try {\n const { outputPath, pageCount } = await runTypeGeneration(cwd)\n const relPath = relative(cwd, outputPath)\n this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? 's' : ''})`)\n return true\n } catch (err) {\n this.fail(`Type generation failed: ${(err as Error).message}`)\n return false\n }\n }\n\n private async watchForChanges(cwd: string): Promise<void> {\n const srcDir = join(cwd, 'src')\n\n try {\n const watcher = watch(srcDir, { recursive: true })\n for await (const event of watcher) {\n if (event.filename && /\\.(tsx|ts)$/.test(event.filename)) {\n this.info(`Change detected: ${event.filename}`)\n await this.generate(cwd)\n }\n }\n } catch (err) {\n this.fail(`Watch failed: ${(err as Error).message}`)\n }\n }\n}\n","import { Module } from 'stratal/module'\nimport { InertiaBuildCommand } from './commands/inertia-build.command'\nimport { InertiaDevCommand } from './commands/inertia-dev.command'\nimport { InertiaInstallCommand } from './commands/inertia-install.command'\nimport { InertiaTypesCommand } from './commands/inertia-types.command'\n\n@Module({\n providers: [\n InertiaInstallCommand,\n InertiaTypesCommand,\n InertiaDevCommand,\n InertiaBuildCommand,\n ],\n})\nexport class InertiaQuarryModule {}\n\nexport { InertiaBuildCommand } from './commands/inertia-build.command'\nexport { InertiaDevCommand } from './commands/inertia-dev.command'\nexport { InertiaInstallCommand } from './commands/inertia-install.command'\nexport { InertiaTypesCommand } from './commands/inertia-types.command'\nexport { runTypeGeneration } from './generator/type-generator'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"quarry.mjs","names":[],"sources":["../src/vite/create-client-vite-config.ts","../src/vite/create-vite-config.ts","../src/commands/inertia-build.command.ts","../src/commands/worker-recycler.ts","../src/commands/inertia-dev.command.ts","../src/commands/inertia-install.command.ts","../src/commands/inertia-types.command.ts","../src/quarry.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nexport interface TempClientViteConfigOptions {\n cwd: string\n entry?: string\n outDir?: string\n}\n\n/**\n * Emits a standalone Vite config for building the Inertia browser bundle.\n *\n * This runs as a separate `vite build` invocation BEFORE the worker build so\n * the worker's `stratal:inertia-inject-manifest` plugin has a finished\n * `<outDir>/.vite/manifest.json` to read. `@cloudflare/vite-plugin` builds its\n * environments in parallel, which made a single-config build racy — splitting\n * the two phases removes the race entirely and keeps each build minimal.\n */\nexport function writeTempClientViteConfig(options: TempClientViteConfigOptions): string {\n const configDir = join(options.cwd, 'node_modules', '.stratal')\n const configPath = join(configDir, 'vite.client.config.mjs')\n mkdirSync(dirname(configPath), { recursive: true })\n\n const entry = (options.entry ?? 'src/inertia/app.tsx').replace(/\\\\/g, '/')\n const outDir = (options.outDir ?? 'dist/client').replace(/\\\\/g, '/')\n const hasUserConfig = existsSync(join(options.cwd, 'vite.config.ts'))\n const publicDir = join(options.cwd, 'src', 'inertia', 'public').replace(/\\\\/g, '/')\n\n const content = `\nimport { mergeConfig } from 'vite'\n\nconst baseConfig = {\n publicDir: '${publicDir}',\n build: {\n outDir: '${outDir}',\n manifest: true,\n emptyOutDir: true,\n rollupOptions: {\n input: { app: '${entry}' },\n },\n },\n}\n\n${hasUserConfig\n ? `const userModule = await import('${join(options.cwd, 'vite.config.ts').replace(/\\\\/g, '/')}')\nconst userConfig = userModule.default ?? userModule\nexport default mergeConfig(userConfig, baseConfig)`\n : 'export default baseConfig'\n }\n`\n\n writeFileSync(configPath, content, 'utf-8')\n return configPath\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nexport interface TempViteConfigOptions {\n cwd: string\n server?: { port?: number; host?: boolean }\n outDir?: string\n persistTo?: string\n /**\n * Worker debugger inspector port passed to `@cloudflare/vite-plugin`.\n * Pass a distinct number per worker to avoid the `EADDRINUSE` race that\n * happens when several Inertia workers boot concurrently and all probe the\n * default port (9229). Pass `false` to disable the inspector entirely.\n * Left `undefined` preserves the plugin's default auto-pick behaviour.\n */\n inspectorPort?: number | false\n /**\n * Path (relative to `cwd`) to the Vite client manifest the worker bundle\n * should inline. Defaults to `dist/client/.vite/manifest.json`, matching\n * what `quarry inertia:build` emits in phase 1.\n */\n clientManifestPath?: string\n}\n\nexport function writeTempViteConfig(options: TempViteConfigOptions): string {\n const configDir = join(options.cwd, 'node_modules', '.stratal')\n const configPath = join(configDir, 'vite.config.mjs')\n mkdirSync(dirname(configPath), { recursive: true })\n\n const hasUserConfig = existsSync(join(options.cwd, 'vite.config.ts'))\n\n const serverConfig = options.server\n ? `server: { port: ${options.server.port}, host: ${options.server.host ? 'true' : 'undefined'} },`\n : ''\n\n const outDirConfig = options.outDir\n ? `outDir: '${options.outDir}',`\n : ''\n\n const cloudflareOptions: string[] = []\n if (options.persistTo) {\n cloudflareOptions.push(`persistState: { path: ${JSON.stringify(options.persistTo)} }`)\n }\n if (options.inspectorPort !== undefined) {\n cloudflareOptions.push(`inspectorPort: ${options.inspectorPort === false ? 'false' : options.inspectorPort}`)\n }\n const cloudflareArgs = cloudflareOptions.length ? `{ ${cloudflareOptions.join(', ')} }` : ''\n\n const stratalArgs = options.clientManifestPath\n ? `{ clientManifestPath: ${JSON.stringify(options.clientManifestPath)} }`\n : ''\n\n const content = `\nimport { mergeConfig } from 'vite'\nimport { cloudflare } from '@cloudflare/vite-plugin'\nimport { stratalInertia } from '@stratal/inertia/vite'\n\nlet inertiaPlugin = null\ntry {\n const mod = await import('@inertiajs/vite')\n const inertia = mod.default ?? mod\n inertiaPlugin = inertia()\n} catch {}\n\nconst baseConfig = {\n publicDir: 'src/inertia/public',\n plugins: [\n cloudflare(${cloudflareArgs}),\n ...(inertiaPlugin ? [inertiaPlugin] : []),\n ...stratalInertia(${stratalArgs}),\n ],\n build: {\n ${outDirConfig}\n },\n ${serverConfig}\n}\n\n${hasUserConfig\n ? `const userModule = await import('${join(options.cwd, 'vite.config.ts').replace(/\\\\/g, '/')}')\nconst userConfig = userModule.default ?? userModule\nexport default mergeConfig(baseConfig, userConfig)`\n : 'export default baseConfig'\n }\n`\n\n writeFileSync(configPath, content, 'utf-8')\n return configPath\n}\n","import { spawn } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { writeTempClientViteConfig } from '../vite/create-client-vite-config'\nimport { writeTempViteConfig } from '../vite/create-vite-config'\n\nexport class InertiaBuildCommand extends Command {\n static command = 'inertia:build {--outDir=dist : Output directory} {--ssr : Also build SSR bundle}'\n static description = 'Build Inertia.js frontend for production'\n\n async handle(): Promise<number | undefined> {\n const outDir = this.string('outDir') || 'dist'\n const shouldBuildSsr = this.boolean('ssr')\n const cwd = process.cwd()\n\n const entryPath = 'src/inertia/app.tsx'\n if (!existsSync(join(cwd, entryPath))) {\n this.fail('src/inertia/app.tsx not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n // Phase 1: standalone browser-bundle build. Runs without the Cloudflare\n // vite-plugin so it isn't subject to its parallel env orchestration. The\n // resulting `<clientOutDir>/.vite/manifest.json` is what the worker build\n // (phase 2) inlines into the worker entry via `stratal:inertia-inject-manifest`.\n const clientOutDir = join(outDir, 'client').replace(/\\\\/g, '/')\n const clientConfigPath = writeTempClientViteConfig({\n cwd,\n entry: entryPath,\n outDir: clientOutDir,\n })\n\n this.info('Building Inertia.js browser bundle...')\n const browserCode = await this.spawnVite(cwd, clientConfigPath, ['build'])\n if (browserCode !== 0) {\n this.fail('Browser bundle build failed.')\n return browserCode\n }\n this.success(`Browser bundle written to ${clientOutDir}/`)\n\n // Phase 2: worker build (Cloudflare vite-plugin). The injector plugin\n // reads the manifest produced in phase 1 and inlines it onto the worker\n // entry chunk.\n const configPath = writeTempViteConfig({\n cwd,\n outDir,\n clientManifestPath: join(clientOutDir, '.vite', 'manifest.json').replace(/\\\\/g, '/'),\n })\n\n this.info('Building Cloudflare worker bundle...')\n const workerCode = await this.spawnVite(cwd, configPath, ['build'])\n if (workerCode !== 0) {\n this.fail('Worker build failed.')\n return workerCode\n }\n this.success('Worker build complete!')\n\n if (shouldBuildSsr) {\n this.info('Building SSR bundle...')\n const ssrCode = await this.spawnVite(cwd, configPath, ['build', '--ssr'])\n if (ssrCode !== 0) {\n this.fail('SSR build failed.')\n return ssrCode\n }\n this.success('SSR build complete!')\n }\n\n this.success(`Output in ${outDir}/`)\n this.info('Deploy with: npx wrangler deploy')\n return 0\n }\n\n private spawnVite(cwd: string, configPath: string, args: string[]): Promise<number> {\n return new Promise((resolve) => {\n const child = spawn('npx', ['vite', '--config', configPath, ...args], {\n cwd,\n stdio: 'inherit',\n shell: true,\n })\n\n child.on('error', (err) => {\n this.fail(`Vite process error: ${err.message}`)\n resolve(1)\n })\n\n child.on('close', (code) => {\n resolve(code ?? 0)\n })\n })\n }\n}\n","import { execFile } from 'node:child_process'\nimport type { ChildProcess } from 'node:child_process'\nimport { promisify } from 'node:util'\n\nconst execFileAsync = promisify(execFile)\n\n// Dev-only supervisor that keeps the Inertia worker's memory bounded.\n//\n// `@cloudflare/vite-plugin` runs the app worker in a single long-lived workerd\n// isolate. Under sustained HMR the isolate re-executes module top-level code\n// (DI registration, app boot) without discarding prior instances, so its heap\n// climbs monotonically to the ~1.4 GB V8 limit and aborts with \"JavaScript heap\n// out of memory\" — the worker dies and the browser shows \"Fetch failed\".\n//\n// There is no config-level isolate memory cap in miniflare/workerd, and killing\n// workerd externally wedges the dev server (miniflare never auto-respawns). The\n// only clean reset is a Vite server restart: it drives the plugin through\n// `miniflare.setOptions`, which SIGKILLs workerd and respawns a fresh ~40 MB\n// isolate while keeping ports stable and recovering to healthy within ~1 s.\n//\n// So quarry (the parent that owns the spawned `vite dev` child) samples the\n// workerd process RSS and, when it crosses a threshold, triggers that restart by\n// writing Vite's `r` shortcut to the child's stdin. RSS (not the V8 inspector)\n// is used because the consuming app installs a global undici dispatcher at boot\n// that intercepts the quarry process's own fetch/WebSocket; RSS via `ps` is\n// immune to that and tracks the isolate heap closely (verified: ~1.4 GB at OOM).\n\nexport interface Proc { pid: number, ppid: number, rss: number, cmd: string }\n\n// Parse `ps -axo pid=,ppid=,rss=,command=` output (rss in KB). Exported for tests.\nexport function parsePsProcs(stdout: string): Proc[] {\n const procs: Proc[] = []\n for (const line of stdout.split('\\n')) {\n const m = /^\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(.*)$/.exec(line)\n if (m) procs.push({ pid: Number(m[1]), ppid: Number(m[2]), rss: Number(m[3]), cmd: m[4] })\n }\n return procs\n}\n\n// Max RSS (MB) among workerd processes descended from `rootPid` (the spawned vite\n// child). Returns null when no workerd descendant is found (worker still booting).\n// Pure and exported for tests. `rss` is in KB.\nexport function maxWorkerdRssMb(procs: Proc[], rootPid: number): number | null {\n const childrenOf = new Map<number, number[]>()\n for (const p of procs) {\n const arr = childrenOf.get(p.ppid)\n if (arr) arr.push(p.pid)\n else childrenOf.set(p.ppid, [p.pid])\n }\n const byPid = new Map(procs.map(p => [p.pid, p]))\n const stack = [rootPid]\n const seen = new Set<number>()\n let maxRssKb = 0\n while (stack.length) {\n const pid = stack.pop()!\n if (seen.has(pid)) continue\n seen.add(pid)\n const p = byPid.get(pid)\n if (p && p.cmd.includes('workerd') && p.rss > maxRssKb) maxRssKb = p.rss\n for (const c of childrenOf.get(pid) ?? []) stack.push(c)\n }\n return maxRssKb > 0 ? Math.round(maxRssKb / 1024) : null\n}\n\nasync function readWorkerRssMb(rootPid: number): Promise<number | null> {\n try {\n const { stdout } = await execFileAsync('ps', ['-axo', 'pid=,ppid=,rss=,command='], { maxBuffer: 64 * 1024 * 1024 })\n return maxWorkerdRssMb(parsePsProcs(stdout), rootPid)\n } catch {\n return null\n }\n}\n\nexport interface WorkerRecyclerOptions {\n /** The spawned `vite dev` child; its stdin must be piped. */\n child: ChildProcess\n /** Recycle when the worker's RSS reaches this many MB. */\n heapLimitMb: number\n /** How often to sample RSS. */\n intervalMs?: number\n /** Minimum gap between recycles, to let a restart settle. */\n cooldownMs?: number\n log: (msg: string) => void\n}\n\n/**\n * Start sampling the worker's RSS and recycle it (via a Vite restart) before it\n * can reach the V8 OOM ceiling. Returns a stop function.\n */\nexport function startWorkerRecycler(opts: WorkerRecyclerOptions): () => void {\n // Poll fast and keep the post-recycle cooldown just long enough to cover a\n // Vite restart (~1-2 s), so RSS can't overshoot far past the threshold toward\n // the OOM ceiling under intense churn (overshoot ≈ (interval+cooldown)×growth).\n const intervalMs = opts.intervalMs ?? 1500\n const cooldownMs = opts.cooldownMs ?? 3000\n const rootPid = opts.child.pid\n const debug = process.env.STRATAL_RECYCLER_DEBUG === '1'\n let lastRecycle = 0\n let stopped = false\n\n // RSS sampling uses `ps`, which is POSIX-only; elsewhere the worker is left\n // unsupervised. Warn once (it never fires on the common macOS/Linux path) —\n // otherwise the recycler is silent unless it actually recycles.\n if (process.platform !== 'darwin' && process.platform !== 'linux') {\n opts.log(`memory supervision unavailable on ${process.platform} (needs macOS/Linux)`)\n return () => { /* nothing to stop */ }\n }\n\n if (rootPid == null) return () => { /* no child to supervise */ }\n\n const timer = setInterval(() => {\n if (stopped) return\n void readWorkerRssMb(rootPid).then((rssMb) => {\n if (stopped || rssMb == null) return\n if (debug) opts.log(`worker RSS ${rssMb}MB / limit ${opts.heapLimitMb}MB`)\n if (rssMb >= opts.heapLimitMb && Date.now() - lastRecycle > cooldownMs) {\n lastRecycle = Date.now()\n opts.log(`recycling dev worker (${rssMb}MB)`)\n try {\n opts.child.stdin?.write('r\\n')\n } catch (e) {\n opts.log(`failed to signal Vite restart: ${(e as Error).message}`)\n }\n }\n })\n }, intervalMs)\n\n return () => { stopped = true; clearInterval(timer) }\n}\n","import { spawn } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { writeTempViteConfig } from '../vite/create-vite-config'\nimport { startWorkerRecycler } from './worker-recycler'\n\n// Recycle the worker isolate once its RSS reaches this many MB. The worker V8\n// OOMs near ~1.4 GB; a freshly-warmed isolate sits ~250 MB, so 900 MB leaves\n// generous headroom for the restart to complete before the ceiling. See\n// worker-recycler.ts for why this is necessary.\nconst DEFAULT_HEAP_LIMIT_MB = 900\n\nexport class InertiaDevCommand extends Command {\n static command = 'inertia:dev {--port= : Dev server port} {--host : Expose to network} {--inspector-port= : Worker debugger inspector port (number, or \"false\" to disable). Set a distinct value per worker to avoid EADDRINUSE when running multiple Inertia workers concurrently.} {--persist-to= : Shared persist directory for @cloudflare/vite-plugin (relative to cwd; the plugin appends /v3). Use to share R2/KV/cache emulator state across multiple workers in dev.} {--heap-limit= : Recycle the dev worker isolate when its memory (MB) reaches this, to prevent the HMR-driven V8 OOM. Default 900.}'\n static description = 'Start Inertia.js Vite development server'\n\n async handle(): Promise<number | undefined> {\n const port = this.number('port')\n const host = this.boolean('host')\n const persistTo = this.string('persist-to')\n const inspectorPortRaw = this.string('inspector-port')\n let inspectorPort: number | false | undefined\n if (inspectorPortRaw === 'false') {\n inspectorPort = false\n } else if (inspectorPortRaw !== undefined) {\n inspectorPort = Number(inspectorPortRaw)\n if (!Number.isInteger(inspectorPort) || inspectorPort < 0 || inspectorPort > 65535) {\n this.fail(`Invalid --inspector-port \"${inspectorPortRaw}\". Expected an integer between 0 and 65535, or \"false\" to disable.`)\n return 1\n }\n }\n const heapLimitRaw = this.string('heap-limit')\n // Unset resolves to an empty string here, not undefined — treat it as default.\n const heapLimitMb = heapLimitRaw ? Number(heapLimitRaw) : DEFAULT_HEAP_LIMIT_MB\n if (!Number.isFinite(heapLimitMb) || heapLimitMb <= 0) {\n this.fail(`Invalid --heap-limit \"${heapLimitRaw}\". Expected a positive number of MB.`)\n return 1\n }\n const cwd = process.cwd()\n\n const entryPath = 'src/inertia/app.tsx'\n if (!existsSync(join(cwd, entryPath))) {\n this.fail('src/inertia/app.tsx not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n const configPath = writeTempViteConfig({\n cwd,\n server: { port, host },\n persistTo,\n inspectorPort,\n })\n\n this.info('Starting Vite dev server...')\n\n const args = ['vite', 'dev', '--config', configPath]\n if (host) args.push('--host')\n\n return new Promise<number>((resolve) => {\n // stdin is piped (not inherited) so the recycler can inject Vite's `r`\n // restart shortcut; stdout/stderr stay inherited so dev output is unchanged.\n const child = spawn('npx', args, {\n cwd,\n stdio: ['pipe', 'inherit', 'inherit'],\n shell: true,\n })\n\n // Forward the user's keystrokes to Vite so its interactive shortcuts\n // (r/u/o/q/h) keep working — piping stdin for the recycler would otherwise\n // swallow them. The recycler writes `r\\n` to the same stdin independently.\n // Only when stdin is an interactive TTY: keystrokes come from nowhere else,\n // and a non-TTY stdin (background/CI/piped) is a plain stream without\n // unref(). unref() so this forwarded stdin never keeps the process alive on\n // its own; the child process handle does that until it exits.\n if (child.stdin && process.stdin.isTTY) {\n process.stdin.pipe(child.stdin)\n // The child's stdin closes when it exits — ignore the resulting EPIPE.\n child.stdin.on('error', () => { /* child gone */ })\n process.stdin.unref()\n }\n\n // Supervise the worker's memory and recycle the isolate (via a Vite\n // restart) before the HMR-driven V8 OOM. A restart resets the isolate to a\n // fresh ~40 MB. Always on — this is the OOM fix, not an opt-in.\n const stopRecycler = startWorkerRecycler({\n child,\n heapLimitMb,\n log: (msg) => this.info(`[worker-recycler] ${msg}`),\n })\n\n const cleanup = () => {\n stopRecycler()\n if (child.stdin && process.stdin.isTTY) process.stdin.unpipe(child.stdin)\n }\n\n child.on('error', (err) => {\n cleanup()\n this.fail(`Failed to start dev server: ${err.message}`)\n resolve(1)\n })\n\n child.on('close', (code) => {\n cleanup()\n resolve(code ?? 0)\n })\n })\n }\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { join, relative } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport type { SourceFile, SyntaxKind } from 'ts-morph'\nimport { runTypeGeneration } from '../generator/type-generator'\n\n/** Outcome of reconciling `src/app.module.ts` with the SSR-enabled InertiaModule. */\ntype AppModuleUpdate = 'created' | 'ssr-added' | 'unchanged' | 'unwired'\n/**\n * The subset of ts-morph's runtime `SyntaxKind` enum that `ensureSsrWiring` reads.\n * Declared structurally (via the enum-member literal types) so ts-morph stays a\n * type-only import here and is loaded lazily where it's actually used.\n */\ninterface SyntaxKinds {\n CallExpression: SyntaxKind.CallExpression\n ObjectLiteralExpression: SyntaxKind.ObjectLiteralExpression\n}\n\nconst ROOT_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n @viteHead\n @inertiaHead\n</head>\n<body>\n @inertia\n @viteScripts\n</body>\n</html>`\n\nconst APP_TSX = `import { createInertiaApp } from '@inertiajs/react'\n\ncreateInertiaApp({\n resolve: async (name) => {\n const pages = import.meta.glob('./pages/**/*.tsx')\n const page = await pages[\\`./pages/\\${name}.tsx\\`]?.()\n if (!page) throw new Error(\\`Page not found: \\${name}\\`)\n return page\n },\n})`\n\nconst SSR_TSX = `import { createInertiaSsrApp } from '@stratal/inertia/ssr'\n\nexport const { render } = createInertiaSsrApp({\n resolve: async (name) => {\n const pages = import.meta.glob('./pages/**/*.tsx')\n const page = await pages[\\`./pages/\\${name}.tsx\\`]?.()\n if (!page) throw new Error(\\`Page not found: \\${name}\\`)\n return page\n },\n})`\n\nconst HOME_TSX = `export default function Home({ message }: { message: string }) {\n return (\n <div>\n <h1>{message}</h1>\n <p>This page is rendered with Inertia.js and Stratal.</p>\n </div>\n )\n}`\n\nexport class InertiaInstallCommand extends Command {\n static command = 'inertia:install {--skip-deps : Skip installing npm dependencies}'\n static description = 'Scaffold Inertia.js files for a Stratal project'\n\n async handle(): Promise<number | undefined> {\n const skipDeps = this.boolean('skip-deps')\n const cwd = process.cwd()\n const inertiaDir = join(cwd, 'src', 'inertia')\n const pagesDir = join(inertiaDir, 'pages')\n\n // Create directories\n this.info('Creating src/inertia/ directory...')\n mkdirSync(pagesDir, { recursive: true })\n\n const publicDir = join(inertiaDir, 'public')\n mkdirSync(publicDir, { recursive: true })\n const gitkeepPath = join(publicDir, '.gitkeep')\n if (!existsSync(gitkeepPath)) {\n writeFileSync(gitkeepPath, '', 'utf-8')\n }\n this.success('Created src/inertia/public/')\n\n // Write template files\n const files = [\n { path: join(inertiaDir, 'root.html'), content: ROOT_HTML, name: 'root.html' },\n { path: join(inertiaDir, 'app.tsx'), content: APP_TSX, name: 'app.tsx' },\n { path: join(inertiaDir, 'ssr.tsx'), content: SSR_TSX, name: 'ssr.tsx' },\n { path: join(pagesDir, 'Home.tsx'), content: HOME_TSX, name: 'pages/Home.tsx' },\n ]\n\n for (const file of files) {\n if (existsSync(file.path)) {\n this.warn(`Skipping ${file.name} (already exists)`)\n } else {\n writeFileSync(file.path, file.content, 'utf-8')\n this.success(`Created src/inertia/${file.name}`)\n }\n }\n\n // Modify app.module.ts\n const appModulePath = join(cwd, 'src', 'app.module.ts')\n if (existsSync(appModulePath)) {\n this.info('Updating src/app.module.ts...')\n try {\n const result = await this.updateAppModule(appModulePath)\n if (result === 'created') {\n this.success('Updated src/app.module.ts with InertiaModule')\n } else if (result === 'ssr-added') {\n this.success('Enabled streaming SSR in src/app.module.ts')\n } else if (result === 'unchanged') {\n this.info('InertiaModule (with SSR) already configured in app.module.ts')\n } else {\n this.warn('InertiaModule is configured but SSR could not be auto-wired.')\n this.info(\"Add `ssr: { bundle: () => import('./inertia/ssr') }` to your InertiaModule options\")\n }\n } catch (err) {\n this.warn(`Could not auto-update app.module.ts: ${(err as Error).message}`)\n this.info('Please manually add InertiaModule.forRoot() to your module imports')\n }\n } else {\n this.info('No src/app.module.ts found — please manually configure InertiaModule')\n }\n\n // Generate initial type definitions\n try {\n const { outputPath, pageCount } = await runTypeGeneration(cwd)\n const relPath = relative(cwd, outputPath)\n this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? 's' : ''})`)\n } catch {\n this.warn('Could not generate initial type definitions. Run `quarry inertia:types` manually.')\n }\n\n if (!skipDeps) {\n this.newLine()\n this.info('Install the following dependencies:')\n this.line(' npm install @stratal/inertia @inertiajs/react @inertiajs/vite react react-dom')\n this.line(' npm install -D @types/react @types/react-dom vite @cloudflare/vite-plugin')\n }\n\n this.newLine()\n this.success('Inertia.js scaffolding complete!')\n this.info('Run `quarry inertia:dev` to start the dev server')\n\n return 0\n }\n\n private async updateAppModule(modulePath: string): Promise<AppModuleUpdate> {\n const { Project, SyntaxKind } = await import('ts-morph')\n\n const project = new Project({ useInMemoryFileSystem: false })\n const sourceFile = project.addSourceFileAtPath(modulePath)\n\n // Already importing the package — an older install that predates streaming\n // SSR. Wire the existing InertiaModule config to the SSR bundle rather than\n // bailing (which would leave SSR silently disabled).\n const existingImport = sourceFile.getImportDeclaration((decl) =>\n decl.getModuleSpecifierValue() === '@stratal/inertia',\n )\n if (existingImport) {\n const result = this.ensureSsrWiring(sourceFile, SyntaxKind)\n if (result === 'ssr-added') await sourceFile.save()\n return result\n }\n\n // Fresh install: add the imports and an InertiaModule.forRoot wired for SSR.\n sourceFile.addImportDeclaration({\n defaultImport: 'rootView',\n moduleSpecifier: './inertia/root.html?raw',\n })\n sourceFile.addImportDeclaration({\n namedImports: ['InertiaModule'],\n moduleSpecifier: '@stratal/inertia',\n })\n\n // Find the @Module decorator and add InertiaModule to imports\n const classes = sourceFile.getClasses()\n for (const cls of classes) {\n const moduleDecorator = cls.getDecorator('Module')\n if (!moduleDecorator) continue\n\n const args = moduleDecorator.getArguments()\n if (args.length === 0) continue\n\n const objLiteral = args[0].asKind(SyntaxKind.ObjectLiteralExpression)\n if (!objLiteral) continue\n\n const importsProp = objLiteral.getProperty('imports')\n if (importsProp) {\n // Add to existing imports array\n const initializer = importsProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer()\n const arrayLiteral = initializer?.asKind(SyntaxKind.ArrayLiteralExpression)\n if (arrayLiteral) {\n arrayLiteral.addElement(`InertiaModule.forRoot({\\n rootView,\\n ssr: { bundle: () => import('./inertia/ssr') },\\n })`)\n }\n } else {\n // Add imports property\n objLiteral.addPropertyAssignment({\n name: 'imports',\n initializer: `[\\n InertiaModule.forRoot({\\n rootView,\\n ssr: { bundle: () => import('./inertia/ssr') },\\n }),\\n ]`,\n })\n }\n\n break\n }\n\n await sourceFile.save()\n return 'created'\n }\n\n /**\n * Ensure an existing `InertiaModule.forRoot({...})` call opts into the streaming\n * SSR bundle. Returns `ssr-added` when the option is inserted, `unchanged` when\n * one is already present, or `unwired` when no plain `forRoot({...})` object\n * literal is found (e.g. `forRootAsync`, or a config passed by reference) — in\n * which case the caller surfaces a manual instruction.\n */\n private ensureSsrWiring(sourceFile: SourceFile, syntaxKind: SyntaxKinds): AppModuleUpdate {\n const calls = sourceFile.getDescendantsOfKind(syntaxKind.CallExpression)\n for (const call of calls) {\n if (call.getExpression().getText() !== 'InertiaModule.forRoot') continue\n\n const objLiteral = call.getArguments()[0]?.asKind(syntaxKind.ObjectLiteralExpression)\n if (!objLiteral) continue\n\n if (objLiteral.getProperty('ssr')) return 'unchanged'\n\n objLiteral.addPropertyAssignment({\n name: 'ssr',\n initializer: `{ bundle: () => import('./inertia/ssr') }`,\n })\n return 'ssr-added'\n }\n return 'unwired'\n }\n}\n","import { existsSync } from 'node:fs'\nimport { watch } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\nimport { Command } from 'stratal/quarry'\nimport { findPagesDir, runTypeGeneration } from '../generator/type-generator'\n\nexport class InertiaTypesCommand extends Command {\n static command = 'inertia:types {--watch : Watch for changes and regenerate}'\n static description = 'Generate Inertia.js page type definitions'\n\n async handle(): Promise<number | undefined> {\n const cwd = process.cwd()\n const pagesDir = findPagesDir(cwd)\n\n if (!existsSync(pagesDir)) {\n this.fail('src/inertia/pages/ not found. Run `quarry inertia:install` first.')\n return 1\n }\n\n const result = await this.generate(cwd)\n if (!result) return 1\n\n if (this.boolean('watch')) {\n this.info('Watching for changes...')\n await this.watchForChanges(cwd)\n }\n\n return 0\n }\n\n private async generate(cwd: string): Promise<boolean> {\n try {\n const { outputPath, pageCount } = await runTypeGeneration(cwd)\n const relPath = relative(cwd, outputPath)\n this.success(`Generated ${relPath} (${pageCount} page${pageCount !== 1 ? 's' : ''})`)\n return true\n } catch (err) {\n this.fail(`Type generation failed: ${(err as Error).message}`)\n return false\n }\n }\n\n private async watchForChanges(cwd: string): Promise<void> {\n const srcDir = join(cwd, 'src')\n\n try {\n const watcher = watch(srcDir, { recursive: true })\n for await (const event of watcher) {\n if (event.filename && /\\.(tsx|ts)$/.test(event.filename)) {\n this.info(`Change detected: ${event.filename}`)\n await this.generate(cwd)\n }\n }\n } catch (err) {\n this.fail(`Watch failed: ${(err as Error).message}`)\n }\n }\n}\n","import { Module } from 'stratal/module'\nimport { InertiaBuildCommand } from './commands/inertia-build.command'\nimport { InertiaDevCommand } from './commands/inertia-dev.command'\nimport { InertiaInstallCommand } from './commands/inertia-install.command'\nimport { InertiaTypesCommand } from './commands/inertia-types.command'\n\n@Module({\n providers: [\n InertiaInstallCommand,\n InertiaTypesCommand,\n InertiaDevCommand,\n InertiaBuildCommand,\n ],\n})\nexport class InertiaQuarryModule {}\n\nexport { InertiaBuildCommand } from './commands/inertia-build.command'\nexport { InertiaDevCommand } from './commands/inertia-dev.command'\nexport { InertiaInstallCommand } from './commands/inertia-install.command'\nexport { InertiaTypesCommand } from './commands/inertia-types.command'\nexport { runTypeGeneration } from './generator/type-generator'\n"],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { a as RoleName, i as Permission, o as SharedAccess, r as Check, t as AccessControlRegistry } from "../types-D-j_Ee_h.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/react/access/components.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Exactly one of the three forms, enforced by the union: supplying two at once
|
|
6
|
+
* is a compile error rather than a precedence rule to remember.
|
|
7
|
+
*/
|
|
8
|
+
type GateProps<TKey extends string, TValue> = (Record<TKey, TValue> & {
|
|
9
|
+
any?: never;
|
|
10
|
+
all?: never;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}) | ({
|
|
13
|
+
any: readonly TValue[];
|
|
14
|
+
} & Partial<Record<TKey, never>> & {
|
|
15
|
+
all?: never;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}) | ({
|
|
18
|
+
all: readonly TValue[];
|
|
19
|
+
} & Partial<Record<TKey, never>> & {
|
|
20
|
+
any?: never;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
});
|
|
23
|
+
type CanProps = GateProps<'do', Permission>;
|
|
24
|
+
type RoleProps = GateProps<'is', RoleName>;
|
|
25
|
+
/**
|
|
26
|
+
* Renders its children when the current user holds the permission(s).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* <Can do="posts:update"><EditButton /></Can>
|
|
31
|
+
* <Can any={['posts:update', 'posts:delete']}><Toolbar /></Can>
|
|
32
|
+
* <Can all={['posts:read', 'admin:access']}><AuditLog /></Can>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function Can(props: CanProps): ReactNode;
|
|
36
|
+
/** Renders its children when the current user does **not** hold the permission(s). */
|
|
37
|
+
export declare function Cannot(props: CanProps): ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Renders its children when the current user holds the role(s).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* <HasRole is="admin"><AdminPanel /></HasRole>
|
|
44
|
+
* <HasRole any={['editor', 'reviewer']}><ReviewQueue /></HasRole>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function HasRole(props: RoleProps): ReactNode;
|
|
48
|
+
/** Renders its children when the current user holds **none** of the role(s). */
|
|
49
|
+
export declare function HasNoRole(props: RoleProps): ReactNode;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/react/access/missing-access-props.error.d.ts
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when a component or hook from `@stratal/inertia/react/access` runs on
|
|
54
|
+
* a page with no `access` prop.
|
|
55
|
+
*
|
|
56
|
+
* This is always a wiring problem, never a permission problem — a user with no
|
|
57
|
+
* permissions gets `{ roles: [], permissions: {} }`. Failing loudly keeps
|
|
58
|
+
* "misconfigured" from being indistinguishable from "denied", which would fail
|
|
59
|
+
* open the moment the middleware stopped running.
|
|
60
|
+
*/
|
|
61
|
+
export declare class MissingAccessPropsError extends Error {
|
|
62
|
+
constructor();
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/react/access/use-access.d.ts
|
|
66
|
+
/**
|
|
67
|
+
* Returns the current user's roles and merged permissions.
|
|
68
|
+
*
|
|
69
|
+
* @throws {MissingAccessPropsError} when the page carries no `access` prop.
|
|
70
|
+
*/
|
|
71
|
+
export declare function useAccess(): SharedAccess;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the current user holds the given permission(s).
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const canEdit = useCan('posts:update')
|
|
78
|
+
* const canEither = useCan({ any: ['posts:update', 'posts:delete'] })
|
|
79
|
+
* const canBoth = useCan({ all: ['posts:read', 'admin:access'] })
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function useCan(check: Check<Permission>): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the current user holds the given role(s).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const isAdmin = useRole('admin')
|
|
89
|
+
* const isStaff = useRole({ any: ['editor', 'reviewer'] })
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function useRole(check: Check<RoleName>): boolean;
|
|
93
|
+
//#endregion
|
|
94
|
+
export type { AccessControlRegistry, CanProps, Check, Permission, RoleName, RoleProps, SharedAccess };
|
|
95
|
+
//# sourceMappingURL=access.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.mts","names":[],"sources":["../../src/react/access/components.ts","../../src/react/access/missing-access-props.error.ts","../../src/react/access/use-access.ts"],"mappings":";;;;;;;KAQK,UAAU,qBAAqB,WAC/B,OAAO,MAAM;EAAY;EAAa;EAAa,WAAW;;EAC5D,cAAc;IAAa,QAAQ,OAAO;EAAkB;EAAa,WAAW;;EACpF,cAAc;IAAa,QAAQ,OAAO;EAAkB;EAAa,WAAW;;KAE/E,WAAW,gBAAgB;KAC3B,YAAY,gBAAgB;;;;;;;;;;;wBAsBxB,IAAI,OAAO,WAAW;;wBAKtB,OAAO,OAAO,WAAW;;;;;;;;;;wBAazB,QAAQ,OAAO,YAAY;;wBAK3B,UAAU,OAAO,YAAY;;;;;;;;;;;;qBClDhC,gCAAgC;EAC3C;;;;;;;;;wBCKc,aAAa;;;;;;;;;;;wBAgBb,OAAO,OAAO,MAAM;;;;;;;;;;wBAapB,QAAQ,OAAO,MAAM"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { usePage } from "@inertiajs/react";
|
|
2
|
+
//#region src/react/access/match.ts
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a permission string against a shared permission set.
|
|
5
|
+
*
|
|
6
|
+
* The grammar matches `AuthGuard` exactly, so a string copy-pasted between a
|
|
7
|
+
* guard and a component means the same thing in both places:
|
|
8
|
+
*
|
|
9
|
+
* - `posts:update` — that specific action
|
|
10
|
+
* - `posts` or `posts:*` — any action on the resource
|
|
11
|
+
*
|
|
12
|
+
* A single-permission check here is equivalent to the server's `AuthGuard`
|
|
13
|
+
* check. An `all` group is not: `evaluate()` ANDs over the caller's merged
|
|
14
|
+
* permissions across every held role, while `AuthGuard` requires one single
|
|
15
|
+
* role to satisfy the entire set. A user with two roles that each satisfy
|
|
16
|
+
* half of an `all` list can pass here and still get a 403 from `AuthGuard`.
|
|
17
|
+
*/
|
|
18
|
+
function matchesPermission(access, permission) {
|
|
19
|
+
const colon = permission.indexOf(":");
|
|
20
|
+
const resource = colon === -1 ? permission : permission.slice(0, colon);
|
|
21
|
+
const action = colon === -1 ? "*" : permission.slice(colon + 1);
|
|
22
|
+
const actions = access.permissions[resource];
|
|
23
|
+
if (!actions || actions.length === 0) return false;
|
|
24
|
+
return action === "*" || actions.includes(action);
|
|
25
|
+
}
|
|
26
|
+
/** Resolves a role name against the user's held roles. */
|
|
27
|
+
function matchesRole(access, role) {
|
|
28
|
+
return access.roles.includes(role);
|
|
29
|
+
}
|
|
30
|
+
/** Applies a matcher across a single value, an `any` group, or an `all` group. */
|
|
31
|
+
function evaluate(access, check, matches) {
|
|
32
|
+
if (typeof check === "string") return matches(access, check);
|
|
33
|
+
if ("any" in check) return check.any.some((value) => matches(access, value));
|
|
34
|
+
return check.all.every((value) => matches(access, value));
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/react/access/missing-access-props.error.ts
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when a component or hook from `@stratal/inertia/react/access` runs on
|
|
40
|
+
* a page with no `access` prop.
|
|
41
|
+
*
|
|
42
|
+
* This is always a wiring problem, never a permission problem — a user with no
|
|
43
|
+
* permissions gets `{ roles: [], permissions: {} }`. Failing loudly keeps
|
|
44
|
+
* "misconfigured" from being indistinguishable from "denied", which would fail
|
|
45
|
+
* open the moment the middleware stopped running.
|
|
46
|
+
*/
|
|
47
|
+
var MissingAccessPropsError = class extends Error {
|
|
48
|
+
constructor() {
|
|
49
|
+
super("The `access` page prop is missing. Configure `accessControl` on AuthModule.forRootAsync() — @stratal/framework shares it automatically on every Inertia render once access control is enabled.");
|
|
50
|
+
this.name = "MissingAccessPropsError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/react/access/use-access.ts
|
|
55
|
+
/**
|
|
56
|
+
* Returns the current user's roles and merged permissions.
|
|
57
|
+
*
|
|
58
|
+
* @throws {MissingAccessPropsError} when the page carries no `access` prop.
|
|
59
|
+
*/
|
|
60
|
+
function useAccess() {
|
|
61
|
+
const access = usePage().props.access;
|
|
62
|
+
if (!access) throw new MissingAccessPropsError();
|
|
63
|
+
return access;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether the current user holds the given permission(s).
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const canEdit = useCan('posts:update')
|
|
71
|
+
* const canEither = useCan({ any: ['posts:update', 'posts:delete'] })
|
|
72
|
+
* const canBoth = useCan({ all: ['posts:read', 'admin:access'] })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function useCan(check) {
|
|
76
|
+
return evaluate(useAccess(), check, matchesPermission);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether the current user holds the given role(s).
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const isAdmin = useRole('admin')
|
|
84
|
+
* const isStaff = useRole({ any: ['editor', 'reviewer'] })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
function useRole(check) {
|
|
88
|
+
return evaluate(useAccess(), check, matchesRole);
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/react/access/components.ts
|
|
92
|
+
function toCheck(props, key) {
|
|
93
|
+
if (props.any !== void 0) return { any: props.any };
|
|
94
|
+
if (props.all !== void 0) return { all: props.all };
|
|
95
|
+
return props[key];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Renders its children when the current user holds the permission(s).
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* <Can do="posts:update"><EditButton /></Can>
|
|
103
|
+
* <Can any={['posts:update', 'posts:delete']}><Toolbar /></Can>
|
|
104
|
+
* <Can all={['posts:read', 'admin:access']}><AuditLog /></Can>
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
function Can(props) {
|
|
108
|
+
return useCan(toCheck(props, "do")) ? props.children : null;
|
|
109
|
+
}
|
|
110
|
+
/** Renders its children when the current user does **not** hold the permission(s). */
|
|
111
|
+
function Cannot(props) {
|
|
112
|
+
return useCan(toCheck(props, "do")) ? null : props.children;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Renders its children when the current user holds the role(s).
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* <HasRole is="admin"><AdminPanel /></HasRole>
|
|
120
|
+
* <HasRole any={['editor', 'reviewer']}><ReviewQueue /></HasRole>
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
function HasRole(props) {
|
|
124
|
+
return useRole(toCheck(props, "is")) ? props.children : null;
|
|
125
|
+
}
|
|
126
|
+
/** Renders its children when the current user holds **none** of the role(s). */
|
|
127
|
+
function HasNoRole(props) {
|
|
128
|
+
return useRole(toCheck(props, "is")) ? null : props.children;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { Can, Cannot, HasNoRole, HasRole, MissingAccessPropsError, useAccess, useCan, useRole };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=access.mjs.map
|