@moku-labs/web 0.1.0-alpha.4 → 0.3.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.
Files changed (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -51
  3. package/dist/chunk-D7D4PA-g.mjs +13 -0
  4. package/dist/index.cjs +5972 -113
  5. package/dist/index.d.cts +2078 -106
  6. package/dist/index.d.mts +2078 -106
  7. package/dist/index.mjs +5859 -33
  8. package/package.json +65 -65
  9. package/dist/bin/moku.cjs +0 -1383
  10. package/dist/bin/moku.d.cts +0 -1
  11. package/dist/bin/moku.d.mts +0 -1
  12. package/dist/bin/moku.mjs +0 -1383
  13. package/dist/chunk-DQk6qfdC.mjs +0 -18
  14. package/dist/factory-CMOo4n6a.cjs +0 -1722
  15. package/dist/factory-DRFGSslp.d.mts +0 -114
  16. package/dist/factory-DiKypQqs.mjs +0 -1602
  17. package/dist/factory-k-YoScgB.d.cts +0 -114
  18. package/dist/index-DH3jlpNi.d.mts +0 -503
  19. package/dist/index-DaY7vTuo.d.cts +0 -503
  20. package/dist/plugins/head/build.cjs +0 -35
  21. package/dist/plugins/head/build.d.cts +0 -17
  22. package/dist/plugins/head/build.d.mts +0 -17
  23. package/dist/plugins/head/build.mjs +0 -27
  24. package/dist/plugins/spa/index.cjs +0 -26
  25. package/dist/plugins/spa/index.d.cts +0 -30
  26. package/dist/plugins/spa/index.d.mts +0 -30
  27. package/dist/plugins/spa/index.mjs +0 -24
  28. package/dist/primitives-BYUp6kae.cjs +0 -100
  29. package/dist/primitives-DKgZfRAO.d.mts +0 -71
  30. package/dist/primitives-Dhko-oLM.mjs +0 -58
  31. package/dist/primitives-yZqQkOVR.d.cts +0 -71
  32. package/dist/project-B8z4jeMC.cjs +0 -1383
  33. package/dist/project-guCYpUeD.mjs +0 -1244
  34. package/dist/test.cjs +0 -82
  35. package/dist/test.d.cts +0 -61
  36. package/dist/test.d.mts +0 -61
  37. package/dist/test.mjs +0 -79
  38. package/dist/wrangler-BlZWVmX9.mjs +0 -369
  39. package/dist/wrangler-Bomk9mU-.cjs +0 -423
package/dist/bin/moku.cjs DELETED
@@ -1,1383 +0,0 @@
1
- #!/usr/bin/env bun
2
- const require_project = require('../project-B8z4jeMC.cjs');
3
- const require_wrangler = require('../wrangler-Bomk9mU-.cjs');
4
- let node_fs = require("node:fs");
5
- let node_fs_promises = require("node:fs/promises");
6
- let node_path = require("node:path");
7
- let node_util = require("node:util");
8
-
9
- //#region src/bin/help.ts
10
- /** @file Help text and banner formatters for the moku CLI. */
11
- /**
12
- * Format the banner line shown before any output.
13
- *
14
- * @param version - The package version.
15
- * @returns The banner string.
16
- */
17
- const formatBanner = (version) => `moku v${version}`;
18
- /**
19
- * Format the top-level help text listing all commands and global flags.
20
- *
21
- * @returns The help text.
22
- */
23
- const formatHelp = () => `
24
- Usage: moku <command> [options]
25
-
26
- Commands:
27
- build [folder] Build static site (default folder: src/)
28
- dev [folder] Start dev server with watch + rebuild
29
- preview [folder] Build and serve site for local preview
30
- deploy [folder] Deploy dist/ to Cloudflare Pages (run \`deploy init\` once first)
31
-
32
- Options:
33
- --version Show version number
34
- --help, -h Show help
35
-
36
- Run moku <command> --help for command-specific options.`.trim();
37
- /**
38
- * Format the help text for `moku build`.
39
- *
40
- * @returns The help text.
41
- */
42
- const formatBuildHelp = () => `
43
- Usage: moku build [folder] [options]
44
-
45
- Arguments:
46
- folder Source folder containing main.ts (default: src/)
47
-
48
- Options:
49
- --verbose, -v Show detailed build output
50
- --mode <mode> Build mode: ssg, spa, hybrid
51
- --help, -h Show help`.trim();
52
- /**
53
- * Format the help text for `moku dev`.
54
- *
55
- * @returns The help text.
56
- */
57
- const formatDevHelp = () => `
58
- Usage: moku dev [folder] [options]
59
-
60
- Arguments:
61
- folder Source folder containing main.ts (default: src/)
62
-
63
- Options:
64
- --verbose, -v Show detailed build output
65
- --port, -p <n> Server port (default: 4173)
66
- --help, -h Show help`.trim();
67
- /**
68
- * Format the help text for `moku deploy`.
69
- *
70
- * @returns The help text.
71
- */
72
- const formatDeployHelp = () => `
73
- Usage: moku deploy [folder] [options]
74
- moku deploy init [folder] [options]
75
-
76
- Arguments:
77
- folder Source folder containing main.ts (default: src/)
78
-
79
- Deploy options:
80
- --build Run \`moku build\` before deploying
81
- --branch <name> Branch to deploy to (default: from wrangler.jsonc workflow)
82
- --help, -h Show help
83
-
84
- Init options:
85
- --ci Also generate .github/workflows/deploy.yml
86
- --force Overwrite existing files (warns on slug change)
87
- --create-project Transactionally create the Cloudflare project before writing wrangler.jsonc
88
- --check Diff only, no writes; exits non-zero on drift
89
- --branch <name> Override the auto-detected default branch
90
- --help, -h Show help`.trim();
91
- /**
92
- * Format the help text for `moku preview`.
93
- *
94
- * @returns The help text.
95
- */
96
- const formatPreviewHelp = () => `
97
- Usage: moku preview [folder] [options]
98
-
99
- Arguments:
100
- folder Source folder containing main.ts (default: src/)
101
-
102
- Options:
103
- --port, -p <n> Server port (default: 3000)
104
- --help, -h Show help`.trim();
105
-
106
- //#endregion
107
- //#region src/bin/parse.ts
108
- /** @file Argument parsers for the moku CLI built on node:util parseArgs. */
109
- const VALID_MODES = new Set([
110
- "ssg",
111
- "spa",
112
- "hybrid"
113
- ]);
114
- const MIN_PORT = 1;
115
- const MAX_PORT = 65535;
116
- const DEFAULT_DEV_PORT = 4173;
117
- const DEFAULT_PREVIEW_PORT = 3e3;
118
- /**
119
- * Classify the top-level invocation into version / help / unknown-flag / command.
120
- *
121
- * @param argv - The argv slice (already stripped of `process.argv[0..1]`).
122
- * @returns A {@link TopLevel} discriminant.
123
- */
124
- const parseTopLevel = (argv) => {
125
- if (argv.length === 0) return { kind: "help" };
126
- const [first, ...rest] = argv;
127
- if (first === void 0) return { kind: "help" };
128
- if (first === "--version") return { kind: "version" };
129
- if (first === "--help" || first === "-h") return { kind: "help" };
130
- if (first.startsWith("-")) return {
131
- kind: "unknown-flag",
132
- flag: first
133
- };
134
- return {
135
- kind: "command",
136
- command: first,
137
- rest
138
- };
139
- };
140
- const parsePort = (value, fallback) => {
141
- if (value === void 0) return {
142
- ok: true,
143
- value: fallback
144
- };
145
- const port = Number.parseInt(value, 10);
146
- if (!Number.isFinite(port) || `${port}` !== value) return {
147
- ok: false,
148
- message: `Invalid port: ${value}. Must be a number.`
149
- };
150
- if (port < MIN_PORT || port > MAX_PORT) return {
151
- ok: false,
152
- message: `Invalid port: ${port}. Must be ${MIN_PORT}-${MAX_PORT}.`
153
- };
154
- return {
155
- ok: true,
156
- value: port
157
- };
158
- };
159
- /**
160
- * Parse `moku build` arguments.
161
- *
162
- * @param argv - Argv after the `build` keyword.
163
- * @returns A {@link ParseResult} carrying a {@link BuildArgs} or an error message.
164
- */
165
- const parseBuild = (argv) => {
166
- try {
167
- const { values, positionals } = (0, node_util.parseArgs)({
168
- args: argv,
169
- options: {
170
- verbose: {
171
- type: "boolean",
172
- short: "v",
173
- default: false
174
- },
175
- mode: { type: "string" },
176
- help: {
177
- type: "boolean",
178
- short: "h",
179
- default: false
180
- }
181
- },
182
- allowPositionals: true,
183
- strict: true
184
- });
185
- if (values.mode !== void 0 && !VALID_MODES.has(values.mode)) return {
186
- ok: false,
187
- message: `Invalid mode: ${values.mode}. Must be one of: ssg, spa, hybrid.`
188
- };
189
- const mode = values.mode;
190
- return {
191
- ok: true,
192
- value: {
193
- folder: positionals[0] ?? "src",
194
- verbose: values.verbose,
195
- help: values.help,
196
- ...mode === void 0 ? {} : { mode }
197
- }
198
- };
199
- } catch (error) {
200
- return {
201
- ok: false,
202
- message: error.message
203
- };
204
- }
205
- };
206
- /**
207
- * Parse `moku dev` arguments.
208
- *
209
- * @param argv - Argv after the `dev` keyword.
210
- * @returns A {@link ParseResult} carrying a {@link DevArgs} or an error message.
211
- */
212
- const parseDev = (argv) => {
213
- try {
214
- const { values, positionals } = (0, node_util.parseArgs)({
215
- args: argv,
216
- options: {
217
- verbose: {
218
- type: "boolean",
219
- short: "v",
220
- default: false
221
- },
222
- port: {
223
- type: "string",
224
- short: "p"
225
- },
226
- help: {
227
- type: "boolean",
228
- short: "h",
229
- default: false
230
- }
231
- },
232
- allowPositionals: true,
233
- strict: true
234
- });
235
- const port = parsePort(values.port, DEFAULT_DEV_PORT);
236
- if (!port.ok) return port;
237
- return {
238
- ok: true,
239
- value: {
240
- folder: positionals[0] ?? "src",
241
- verbose: values.verbose,
242
- help: values.help,
243
- port: port.value
244
- }
245
- };
246
- } catch (error) {
247
- return {
248
- ok: false,
249
- message: error.message
250
- };
251
- }
252
- };
253
- /**
254
- * Parse `moku deploy` arguments.
255
- *
256
- * @param argv - Argv after the `deploy` keyword (NOT including `init`).
257
- * @returns A {@link ParseResult} carrying a {@link DeployArgs} or an error message.
258
- */
259
- const parseDeploy = (argv) => {
260
- try {
261
- const { values, positionals } = (0, node_util.parseArgs)({
262
- args: argv,
263
- options: {
264
- build: {
265
- type: "boolean",
266
- default: false
267
- },
268
- branch: { type: "string" },
269
- help: {
270
- type: "boolean",
271
- short: "h",
272
- default: false
273
- }
274
- },
275
- allowPositionals: true,
276
- strict: true
277
- });
278
- return {
279
- ok: true,
280
- value: {
281
- folder: positionals[0] ?? "src",
282
- help: values.help,
283
- build: values.build,
284
- ...values.branch === void 0 ? {} : { branch: values.branch }
285
- }
286
- };
287
- } catch (error) {
288
- return {
289
- ok: false,
290
- message: error.message
291
- };
292
- }
293
- };
294
- /**
295
- * Parse `moku deploy init` arguments.
296
- *
297
- * @param argv - Argv after the `deploy init` keywords.
298
- * @returns A {@link ParseResult} carrying a {@link DeployInitArgs} or an error message.
299
- */
300
- const parseDeployInit = (argv) => {
301
- try {
302
- const { values, positionals } = (0, node_util.parseArgs)({
303
- args: argv,
304
- options: {
305
- ci: {
306
- type: "boolean",
307
- default: false
308
- },
309
- force: {
310
- type: "boolean",
311
- default: false
312
- },
313
- "create-project": {
314
- type: "boolean",
315
- default: false
316
- },
317
- check: {
318
- type: "boolean",
319
- default: false
320
- },
321
- branch: { type: "string" },
322
- help: {
323
- type: "boolean",
324
- short: "h",
325
- default: false
326
- }
327
- },
328
- allowPositionals: true,
329
- strict: true
330
- });
331
- return {
332
- ok: true,
333
- value: {
334
- folder: positionals[0] ?? "src",
335
- help: values.help,
336
- ci: values.ci,
337
- force: values.force,
338
- createProject: values["create-project"],
339
- check: values.check,
340
- ...values.branch === void 0 ? {} : { branch: values.branch }
341
- }
342
- };
343
- } catch (error) {
344
- return {
345
- ok: false,
346
- message: error.message
347
- };
348
- }
349
- };
350
- /**
351
- * Parse `moku preview` arguments.
352
- *
353
- * @param argv - Argv after the `preview` keyword.
354
- * @returns A {@link ParseResult} carrying a {@link PreviewArgs} or an error message.
355
- */
356
- const parsePreview = (argv) => {
357
- try {
358
- const { values, positionals } = (0, node_util.parseArgs)({
359
- args: argv,
360
- options: {
361
- port: {
362
- type: "string",
363
- short: "p"
364
- },
365
- help: {
366
- type: "boolean",
367
- short: "h",
368
- default: false
369
- }
370
- },
371
- allowPositionals: true,
372
- strict: true
373
- });
374
- const port = parsePort(values.port, DEFAULT_PREVIEW_PORT);
375
- if (!port.ok) return port;
376
- return {
377
- ok: true,
378
- value: {
379
- folder: positionals[0] ?? "src",
380
- help: values.help,
381
- port: port.value
382
- }
383
- };
384
- } catch (error) {
385
- return {
386
- ok: false,
387
- message: error.message
388
- };
389
- }
390
- };
391
-
392
- //#endregion
393
- //#region src/bin/version.ts
394
- const parseSemver = (raw) => {
395
- const stripped = raw.split(/[+-]/, 1)[0] ?? raw;
396
- const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stripped);
397
- if (match === null) return null;
398
- return {
399
- major: Number(match[1]),
400
- minor: Number(match[2]),
401
- patch: Number(match[3])
402
- };
403
- };
404
- const compareSemver = (a, b) => {
405
- if (a.major !== b.major) return a.major - b.major;
406
- if (a.minor !== b.minor) return a.minor - b.minor;
407
- return a.patch - b.patch;
408
- };
409
- const satisfiesRange = (actual, min, prefix) => {
410
- if (prefix === "^") return actual.major === min.major && compareSemver(actual, min) >= 0;
411
- if (prefix === "~") {
412
- if (actual.major !== min.major || actual.minor !== min.minor) return false;
413
- return actual.patch >= min.patch;
414
- }
415
- return compareSemver(actual, min) >= 0;
416
- };
417
- const splitRange = (engine) => {
418
- const trimmed = engine.trim();
419
- const match = /^(>=|\^|~)(.+)$/.exec(trimmed);
420
- if (match === null) return {
421
- prefix: ">=",
422
- rawMin: trimmed
423
- };
424
- return {
425
- prefix: match[1],
426
- rawMin: match[2] ?? ""
427
- };
428
- };
429
- /**
430
- * Check whether a Bun runtime version satisfies a simple engines range.
431
- *
432
- * Supports `>=A.B.C`, `^A.B.C`, `~A.B.C`. Returns ok=true if `engine` is
433
- * undefined (no constraint). Pre-release suffixes on `bunVersion` are stripped
434
- * for the comparison.
435
- *
436
- * @param bunVersion - The current `Bun.version` string.
437
- * @param engine - The `engines.bun` field from package.json (or undefined).
438
- * @returns A {@link VersionCheck} with `ok: false` carrying a user-facing message.
439
- */
440
- const checkBunVersion = (bunVersion, engine) => {
441
- if (engine === void 0) return { ok: true };
442
- const actual = parseSemver(bunVersion);
443
- if (actual === null) return {
444
- ok: false,
445
- message: `Unable to parse Bun version: ${bunVersion}`
446
- };
447
- const { prefix, rawMin } = splitRange(engine);
448
- const min = parseSemver(rawMin);
449
- if (min === null) return {
450
- ok: false,
451
- message: `Unable to parse engine range: ${engine}`
452
- };
453
- if (satisfiesRange(actual, min, prefix)) return { ok: true };
454
- return {
455
- ok: false,
456
- message: `Bun ${bunVersion} does not satisfy engines.bun "${engine}". Install Bun ${rawMin} or newer.`
457
- };
458
- };
459
-
460
- //#endregion
461
- //#region src/bin/cli.ts
462
- /** @file runCli — pure dispatch over injected deps. Tested without spawning subprocesses. */
463
- /**
464
- * Top-level CLI dispatcher.
465
- *
466
- * Exit codes:
467
- * 0 - success
468
- * 1 - config / discovery error
469
- * 2 - build / runtime error
470
- * 3 - invalid arguments
471
- * 4 - unsupported runtime
472
- *
473
- * @param argv - Argv slice (already stripped of `node`/`bun` + script path).
474
- * @param deps - Injected dependencies.
475
- * @returns The exit code wrapper.
476
- */
477
- const runCli = async (argv, deps) => {
478
- const top = parseTopLevel(argv);
479
- if (top.kind === "version") {
480
- deps.stdout(deps.version);
481
- return { code: 0 };
482
- }
483
- if (top.kind === "help") {
484
- deps.stdout(formatBanner(deps.version));
485
- deps.stdout(formatHelp());
486
- return { code: 0 };
487
- }
488
- if (top.kind === "unknown-flag") {
489
- deps.stderr(`Unknown flag: ${top.flag}`);
490
- deps.stdout(formatHelp());
491
- return { code: 3 };
492
- }
493
- const versionCheck = checkBunVersion(deps.bunVersion, deps.bunEngine);
494
- if (!versionCheck.ok) {
495
- deps.stderr(versionCheck.message);
496
- return { code: 4 };
497
- }
498
- deps.stdout(formatBanner(deps.version));
499
- switch (top.command) {
500
- case "build": return deps.buildCommand(top.rest);
501
- case "dev": return deps.devCommand(top.rest);
502
- case "preview": return deps.previewCommand(top.rest);
503
- case "deploy": return deps.deployCommand(top.rest);
504
- default:
505
- deps.stderr(`Unknown command: ${top.command}`);
506
- deps.stdout(formatHelp());
507
- return { code: 3 };
508
- }
509
- };
510
-
511
- //#endregion
512
- //#region src/bin/commands/prepare.ts
513
- /**
514
- * Parse argv, branch on help/error, then load the app. Reduces command
515
- * complexity by hiding the four-way branch behind a single discriminant.
516
- *
517
- * @param options - Parser + loader + cwd.
518
- * @returns A {@link PreparedOutcome}.
519
- */
520
- const prepareApp = async (options) => {
521
- const parsed = options.parse(options.argv);
522
- if (!parsed.ok) return {
523
- kind: "bad-args",
524
- message: parsed.message
525
- };
526
- if (parsed.value.help) return { kind: "help" };
527
- const loaded = await options.loadApp({
528
- cwd: options.cwd,
529
- folder: parsed.value.folder
530
- });
531
- if (!loaded.ok) return {
532
- kind: "load-failed",
533
- message: loaded.message
534
- };
535
- return {
536
- kind: "ready",
537
- args: parsed.value,
538
- app: loaded.value
539
- };
540
- };
541
- /**
542
- * Time a build run and report it.
543
- *
544
- * @param app - The loaded CliApp.
545
- * @param stdout - Output channel.
546
- * @returns Exit code (0 ok, 2 build failure) and message already emitted.
547
- */
548
- const runBuildOnce = async (app, stdout, stderr) => {
549
- const start = performance.now();
550
- try {
551
- await app.build.run();
552
- } catch (error) {
553
- stderr(`Build failed: ${error.message}`);
554
- return { code: 2 };
555
- }
556
- stdout(`Built in ${Math.round((performance.now() - start) / 100) / 10}s`);
557
- return { code: 0 };
558
- };
559
-
560
- //#endregion
561
- //#region src/bin/commands/build.ts
562
- /** @file `moku build` command — load app, run app.build.run(), report timing. */
563
- /**
564
- * Run the build subcommand.
565
- *
566
- * @param argv - Argv after the `build` keyword.
567
- * @param deps - Injected IO / loader dependencies.
568
- * @returns Exit code: 0 ok, 1 config error, 2 build error, 3 arg error.
569
- */
570
- const buildCommand = async (argv, deps) => {
571
- const prepared = await prepareApp({
572
- argv,
573
- parse: parseBuild,
574
- loadApp: deps.loadApp,
575
- cwd: deps.cwd
576
- });
577
- if (prepared.kind === "help") {
578
- deps.stdout(formatBuildHelp());
579
- return { code: 0 };
580
- }
581
- if (prepared.kind === "bad-args") {
582
- deps.stderr(prepared.message);
583
- deps.stdout(formatBuildHelp());
584
- return { code: 3 };
585
- }
586
- if (prepared.kind === "load-failed") {
587
- deps.stderr(prepared.message);
588
- return { code: 1 };
589
- }
590
- return runBuildOnce(prepared.app, deps.stdout, deps.stderr);
591
- };
592
-
593
- //#endregion
594
- //#region src/plugins/deploy/generators/github-workflow.ts
595
- /** @file deploy plugin GitHub Actions workflow generator — SHA-pinned, single-source-of-truth wrangler version. */
596
- /** Pinned SHAs for the standard runner actions. Update on each plugin release. */
597
- const CHECKOUT_ACTION_SHA = "11bd71901bbe5b1630ceea73d27597364c9af683";
598
- const SETUP_BUN_ACTION_SHA = "4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5";
599
- /**
600
- * Generate the textual contents of `.github/workflows/deploy.yml`.
601
- *
602
- * Notes baked in:
603
- * - SHA-pinned actions (supply-chain hygiene).
604
- * - `wranglerVersion` pinned via {@link MOKU_WRANGLER_VERSION} — single SoT with `ensureWrangler()`.
605
- * - Command line uses `buildWranglerArgs(...)` so runtime and CI emit identical argv.
606
- * - No `--project-name` flag — wrangler reads `name` from `wrangler.jsonc`.
607
- * - Two-step pattern: `bun run moku build` then `wrangler-action` (enables Actions cache reuse).
608
- * - `gitHubToken` enables GitHub Deployments status updates on PRs and commits.
609
- *
610
- * @param input - Target, outdir, and branch.
611
- * @returns YAML text suitable for writing to `.github/workflows/deploy.yml`.
612
- */
613
- const generateGitHubWorkflow = (input) => {
614
- const args = require_wrangler.buildWranglerArgs(input.target, input.outdir, input.branch).join(" ");
615
- return `# .github/workflows/deploy.yml — generated by \`moku deploy init --ci\`.
616
- # To re-sync after config changes, run \`moku deploy init --ci --force\`.
617
-
618
- name: Deploy
619
-
620
- on:
621
- push:
622
- branches: [${input.branch}]
623
- workflow_dispatch:
624
-
625
- permissions:
626
- contents: read
627
-
628
- jobs:
629
- deploy:
630
- runs-on: ubuntu-latest
631
- steps:
632
- - uses: actions/checkout@${CHECKOUT_ACTION_SHA}
633
- - uses: oven-sh/setup-bun@${SETUP_BUN_ACTION_SHA}
634
- - run: bun install --frozen-lockfile
635
- # Two-step pattern (build then deploy) enables GitHub Actions cache reuse.
636
- - run: bun run moku build
637
- - uses: cloudflare/wrangler-action@${require_wrangler.WRANGLER_ACTION_SHA}
638
- with:
639
- apiToken: \${{ secrets.CLOUDFLARE_API_TOKEN }}
640
- accountId: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
641
- wranglerVersion: "${require_wrangler.MOKU_WRANGLER_VERSION}"
642
- gitHubToken: \${{ secrets.GITHUB_TOKEN }}
643
- # buildWranglerArgs() — keep aligned with runtime argv. wrangler.jsonc#name is SSoT
644
- # for the project name, so we deliberately do NOT pass --project-name here.
645
- command: ${args}
646
- `;
647
- };
648
- /** Resolve the workflow path inside `cwd`. */
649
- const workflowPath = (cwd) => (0, node_path.join)(cwd, ".github", "workflows", "deploy.yml");
650
- /** Check whether `.github/workflows/deploy.yml` exists in `cwd`. */
651
- const githubWorkflowExists = (cwd) => (0, node_fs.existsSync)(workflowPath(cwd));
652
- /** Write the generated workflow file, creating `.github/workflows/` as needed. */
653
- const writeGitHubWorkflow = async (cwd, content) => {
654
- const path = workflowPath(cwd);
655
- (0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
656
- await (0, node_fs_promises.writeFile)(path, content, "utf8");
657
- };
658
-
659
- //#endregion
660
- //#region src/plugins/deploy/slug.ts
661
- /** @file deploy plugin slug derivation — Cloudflare Pages project-name validation + slugification. */
662
- /**
663
- * Cloudflare Pages project-name regex: lowercase alphanumerics and dashes only,
664
- * 1–58 chars, no leading/trailing dash, no underscores. Source: workers-sdk #3222.
665
- */
666
- const CLOUDFLARE_PROJECT_NAME_REGEX = /^[a-z0-9][a-z0-9-]{0,56}[a-z0-9]$/;
667
- const MAX_PROJECT_NAME_LENGTH = 58;
668
- /**
669
- * Derive a Cloudflare Pages–valid project slug from an arbitrary site name.
670
- *
671
- * Algorithm: lowercase → replace non-alphanumeric with `-` → collapse repeated
672
- * dashes → strip leading/trailing dashes → truncate to 58 chars → strip
673
- * trailing dashes again after truncation.
674
- *
675
- * @param siteName - The consumer-facing site name (typically `site.name`).
676
- * @returns A slug matching {@link CLOUDFLARE_PROJECT_NAME_REGEX}.
677
- * @throws Error when the input slugifies to an empty string or fails regex validation.
678
- */
679
- const slugify = (siteName) => {
680
- const raw = siteName.toLowerCase().replaceAll(/[^a-z0-9-]/g, "-").replaceAll(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, MAX_PROJECT_NAME_LENGTH).replace(/-+$/, "");
681
- if (raw === "") throw new Error(`deploy: cannot derive a Cloudflare project slug from "${siteName}" — result is empty after sanitization`);
682
- if (!CLOUDFLARE_PROJECT_NAME_REGEX.test(raw)) throw new Error(`deploy: derived slug "${raw}" does not match Cloudflare Pages project-name regex`);
683
- return raw;
684
- };
685
- /**
686
- * Validate that an explicit project name matches Cloudflare's rules.
687
- *
688
- * @param name - Candidate project name.
689
- * @throws Error if the name violates {@link CLOUDFLARE_PROJECT_NAME_REGEX}.
690
- */
691
- const assertValidProjectName = (name) => {
692
- if (!CLOUDFLARE_PROJECT_NAME_REGEX.test(name)) throw new Error(`deploy: project name "${name}" must match ^[a-z0-9][a-z0-9-]{0,56}[a-z0-9]$ (1–58 chars, lowercase alphanumerics and dashes only, no leading/trailing dash, no underscores)`);
693
- };
694
-
695
- //#endregion
696
- //#region src/plugins/deploy/init.ts
697
- /** @file deploy plugin init orchestrator — branch detection, slug derivation, generators, checklist. */
698
- const DEFAULT_BRANCH = "main";
699
- const sanitizedProcessEnv = () => {
700
- const out = {};
701
- for (const [k, v] of Object.entries(process.env)) if (typeof v === "string") out[k] = v;
702
- return out;
703
- };
704
- const getBun = () => globalThis;
705
- const runGitSymbolicRef = async () => {
706
- const { Bun: bun } = getBun();
707
- if (bun === void 0) return null;
708
- try {
709
- const proc = bun.spawn([
710
- "git",
711
- "symbolic-ref",
712
- "refs/remotes/origin/HEAD"
713
- ], {
714
- env: sanitizedProcessEnv(),
715
- stdout: "pipe",
716
- stderr: "pipe"
717
- });
718
- const stdout = await new Response(proc.stdout).text();
719
- if (await proc.exited !== 0) return null;
720
- return stdout.trim().match(/refs\/remotes\/origin\/(.+)$/)?.[1] ?? null;
721
- } catch {
722
- return null;
723
- }
724
- };
725
- const runGitInitDefaultBranch = async () => {
726
- const { Bun: bun } = getBun();
727
- if (bun === void 0) return null;
728
- try {
729
- const proc = bun.spawn([
730
- "git",
731
- "config",
732
- "--get",
733
- "init.defaultBranch"
734
- ], {
735
- env: sanitizedProcessEnv(),
736
- stdout: "pipe",
737
- stderr: "pipe"
738
- });
739
- const stdout = (await new Response(proc.stdout).text()).trim();
740
- return await proc.exited === 0 && stdout !== "" ? stdout : null;
741
- } catch {
742
- return null;
743
- }
744
- };
745
- /** Spawn `git symbolic-ref` with a fallback to `git config --get init.defaultBranch`. */
746
- const defaultDetectDefaultBranch = async () => {
747
- const primary = await runGitSymbolicRef();
748
- if (primary !== null) return primary;
749
- return await runGitInitDefaultBranch() ?? DEFAULT_BRANCH;
750
- };
751
- /** Format a drift list as a printable diff block. */
752
- const formatDriftReport = (drift) => {
753
- if (drift.length === 0) return "No drift detected.";
754
- const lines = ["Drift detected:"];
755
- for (const entry of drift) {
756
- lines.push(` ${entry.field}:`);
757
- lines.push(` - ${entry.current}`);
758
- lines.push(` + ${entry.proposed}`);
759
- }
760
- return lines.join("\n");
761
- };
762
- /** Print the post-init setup checklist to stdout. */
763
- const printChecklist = (stdout, slug, branch, createdProject) => {
764
- stdout("");
765
- stdout("Cloudflare Pages deploy — setup checklist");
766
- stdout("");
767
- stdout("1. Find your account ID:");
768
- stdout(" https://dash.cloudflare.com → string after dash.cloudflare.com/ in the URL.");
769
- stdout("");
770
- if (createdProject) stdout(`2. Pages project '${slug}' already exists (created by --create-project).`);
771
- else {
772
- stdout("2. Create the Pages project (skip if already created):");
773
- stdout(` wrangler pages project create ${slug}`);
774
- }
775
- stdout("");
776
- stdout("3. Mint a Cloudflare API token:");
777
- stdout(" https://dash.cloudflare.com/profile/api-tokens");
778
- stdout(" → Create Token → Custom Token →");
779
- stdout(" → Permissions: Account → Cloudflare Pages → Edit (NOT \"Read\" — Read causes silent 403)");
780
- stdout(" Click \"Create Token\", copy the value (shown only once).");
781
- stdout("");
782
- stdout("4. Add GitHub Actions secrets:");
783
- stdout(" Settings → Secrets and variables → Actions → New repository secret");
784
- stdout(" CLOUDFLARE_API_TOKEN = (the token from step 3)");
785
- stdout(" CLOUDFLARE_ACCOUNT_ID = (the ID from step 1)");
786
- stdout("");
787
- stdout("5. (Optional) Place _headers and _redirects in `public/` to control Cloudflare response headers and redirects.");
788
- stdout("");
789
- stdout(`6. Push to ${branch} — the generated workflow fires automatically.`);
790
- stdout("");
791
- stdout("Tip: run `moku deploy init --check` in release CI to assert config freshness.");
792
- stdout("Note: Cloudflare dashboard git-push auto-build does NOT recognize wrangler.jsonc — use this workflow.");
793
- };
794
- const defaultPromptYesNo = async (_message, defaultYes) => Promise.resolve(defaultYes);
795
- /** Warn-and-overwrite branch of the slug drift decision (interactive "n" or `--force`). */
796
- const warnSlugChanged = (ctx, existingName, slug) => {
797
- ctx.log.warn("deploy:init:slug-changed", {
798
- existing: existingName,
799
- computed: slug,
800
- note: `Project rename: run \`wrangler pages project create ${slug}\`. The old project remains deployed under ${existingName}.pages.dev.`
801
- });
802
- };
803
- const promptKeepExisting = async (ctx, existingName, slug) => {
804
- return (ctx.promptYesNo ?? defaultPromptYesNo)(`${`wrangler.jsonc#name = "${existingName}" (existing) vs slug("${ctx.siteName}") = "${slug}" (computed).`}\nKeep existing? [Y/n]`, true);
805
- };
806
- /** Internal "should we write wrangler.jsonc?" decision around the diff-on-rename gate. */
807
- const resolveSlugWriteDecision = async (ctx, options, existing, slug, interactive) => {
808
- if (existing === null || existing.name === slug) return true;
809
- if (options.force === true) {
810
- warnSlugChanged(ctx, existing.name, slug);
811
- return true;
812
- }
813
- if (!interactive) {
814
- ctx.log.info("deploy:init:slug-keep-existing", {
815
- existing: existing.name,
816
- computed: slug
817
- });
818
- return false;
819
- }
820
- if (await promptKeepExisting(ctx, existing.name, slug)) return false;
821
- warnSlugChanged(ctx, existing.name, slug);
822
- return true;
823
- };
824
- /** Run `wrangler pages project create` for the `--create-project` flag. Treats "already exists" as success. */
825
- const tryCreateCloudflareProject = async (ctx, slug) => {
826
- const env = ctx.env ?? sanitizedProcessEnv();
827
- try {
828
- await require_wrangler.runWrangler([
829
- "pages",
830
- "project",
831
- "create",
832
- slug
833
- ], env, ctx.spawn);
834
- return true;
835
- } catch (error) {
836
- const wranglerError = error instanceof Error ? error : new Error(String(error));
837
- if (/already exists/i.test(wranglerError.message)) return true;
838
- throw wranglerError;
839
- }
840
- };
841
- /** Maybe write the GitHub Actions workflow file. */
842
- const maybeWriteWorkflow = async (ctx, options, cwd, branch) => {
843
- if (options.ci !== true) return;
844
- if (githubWorkflowExists(cwd) && options.force !== true) {
845
- ctx.log.info("deploy:init:workflow-skipped", { reason: "exists; pass --force to overwrite" });
846
- return;
847
- }
848
- await writeGitHubWorkflow(cwd, generateGitHubWorkflow({
849
- target: ctx.config.target,
850
- outdir: ctx.config.outdir,
851
- branch
852
- }));
853
- };
854
- /** Emit the "no build plugin registered" informational warning when appropriate. */
855
- const maybeWarnMissingBuild = (ctx) => {
856
- if (!ctx.buildPluginRegistered && ctx.config.outdir === "dist") ctx.log.warn("deploy:init:no-build-plugin", { note: "No 'build' plugin registered and no explicit outdir set — using default 'dist'. Configure pluginConfigs.deploy.outdir if your build output lives elsewhere." });
857
- };
858
- /** Handle the `--check` short-circuit. */
859
- const handleCheckMode = (ctx, cwd, slug, outdir, branch) => {
860
- const drift = require_wrangler.diffWranglerConfig(require_wrangler.readWranglerConfig(cwd), {
861
- slug,
862
- outdir
863
- });
864
- ctx.stdout(formatDriftReport(drift));
865
- return {
866
- slug,
867
- outdir,
868
- branch,
869
- drift,
870
- wroteFiles: false
871
- };
872
- };
873
- const resolveInitInputs = async (ctx, options) => {
874
- const detectBranch = ctx.detectDefaultBranch ?? defaultDetectDefaultBranch;
875
- const branch = options.branch ?? await detectBranch();
876
- const slug = ctx.config.projectName ?? slugify(ctx.siteName);
877
- assertValidProjectName(slug);
878
- const interactive = options.interactive ?? Boolean(process.stdout.isTTY);
879
- return {
880
- branch,
881
- slug,
882
- outdir: ctx.config.outdir,
883
- interactive
884
- };
885
- };
886
- /** Compose the writable artifacts step — wrangler.jsonc + workflow + warnings. */
887
- const writeArtifacts = async (ctx, options, cwd, inputs, writeJsonc) => {
888
- if (writeJsonc) await require_wrangler.writeWranglerConfig(cwd, require_wrangler.generateWranglerConfig({
889
- slug: inputs.slug,
890
- outdir: inputs.outdir
891
- }));
892
- await maybeWriteWorkflow(ctx, options, cwd, inputs.branch);
893
- maybeWarnMissingBuild(ctx);
894
- };
895
- /**
896
- * Run `moku deploy init`.
897
- *
898
- * @param ctx - {@link InitContext} carrying logger, stdout, and injectable spawn/git deps.
899
- * @param options - {@link InitOptions} flag set.
900
- * @returns A {@link InitResult} summarizing what happened.
901
- */
902
- const runInit = async (ctx, options = {}) => {
903
- const cwd = options.cwd ?? process.cwd();
904
- const inputs = await resolveInitInputs(ctx, options);
905
- if (options.check === true) return handleCheckMode(ctx, cwd, inputs.slug, inputs.outdir, inputs.branch);
906
- const writeJsonc = await resolveSlugWriteDecision(ctx, options, require_wrangler.readWranglerConfig(cwd), inputs.slug, inputs.interactive);
907
- const projectCreated = options.createProject === true ? await tryCreateCloudflareProject(ctx, inputs.slug) : void 0;
908
- await writeArtifacts(ctx, options, cwd, inputs, writeJsonc);
909
- const wroteFiles = writeJsonc || options.ci === true;
910
- if (wroteFiles) printChecklist(ctx.stdout, inputs.slug, inputs.branch, projectCreated === true);
911
- return {
912
- slug: inputs.slug,
913
- outdir: inputs.outdir,
914
- branch: inputs.branch,
915
- wroteFiles,
916
- ...projectCreated === void 0 ? {} : { projectCreated }
917
- };
918
- };
919
-
920
- //#endregion
921
- //#region src/bin/commands/deploy.ts
922
- /** @file `moku deploy` command — load app, run app.deploy.run() or runInit(). */
923
- /**
924
- * Top-level dispatcher for `moku deploy` and `moku deploy init`.
925
- *
926
- * @param argv - Argv after the `deploy` keyword.
927
- * @param deps - Injected IO + loader dependencies.
928
- * @returns Exit code: 0 ok, 1 load error, 2 deploy error, 3 arg error.
929
- */
930
- const deployCommand = async (argv, deps) => {
931
- const [first, ...rest] = argv;
932
- if (first === "init") return runInitCommand(rest, deps);
933
- return runDeployCommand(argv, deps);
934
- };
935
- const runDeployCommand = async (argv, deps) => {
936
- const prepared = await prepareApp({
937
- argv,
938
- parse: parseDeploy,
939
- loadApp: deps.loadApp,
940
- cwd: deps.cwd
941
- });
942
- if (prepared.kind === "help") {
943
- deps.stdout(formatDeployHelp());
944
- return { code: 0 };
945
- }
946
- if (prepared.kind === "bad-args") {
947
- deps.stderr(prepared.message);
948
- deps.stdout(formatDeployHelp());
949
- return { code: 3 };
950
- }
951
- if (prepared.kind === "load-failed") {
952
- deps.stderr(prepared.message);
953
- return { code: 1 };
954
- }
955
- if (prepared.app.deploy === void 0) {
956
- deps.stderr("deploy: this app does not register the deploy plugin.");
957
- return { code: 1 };
958
- }
959
- try {
960
- const result = await prepared.app.deploy.run({
961
- ...prepared.args.branch === void 0 ? {} : { branch: prepared.args.branch },
962
- build: prepared.args.build
963
- });
964
- deps.stdout(`Deployed to ${result.url} (branch=${result.branch}, ${result.durationMs}ms)`);
965
- return { code: 0 };
966
- } catch (error) {
967
- deps.stderr(`Deploy failed: ${error.message}`);
968
- return { code: 2 };
969
- }
970
- };
971
- const resolveDeployConfig = (app) => {
972
- const fromAppConfig = app.config?.deploy;
973
- if (fromAppConfig !== void 0) return fromAppConfig;
974
- return {
975
- target: "pages",
976
- outdir: "dist",
977
- productionBranch: "main"
978
- };
979
- };
980
- const makeInitLogger = (deps) => ({
981
- info: (event, data) => deps.stdout(`[info] ${event} ${data ? JSON.stringify(data) : ""}`),
982
- warn: (event, data) => deps.stderr(`[warn] ${event} ${data ? JSON.stringify(data) : ""}`),
983
- error: (event, data) => deps.stderr(`[error] ${event} ${data ? JSON.stringify(data) : ""}`)
984
- });
985
- const runInitForPreparedApp = async (deps, app, args) => {
986
- const siteName = app.site?.name() ?? "";
987
- if (siteName === "") {
988
- deps.stderr("deploy init: site.name is empty — set it in your app config.");
989
- return { code: 1 };
990
- }
991
- try {
992
- const result = await runInit({
993
- siteName,
994
- config: resolveDeployConfig(app),
995
- buildPluginRegistered: app.build !== void 0,
996
- log: makeInitLogger(deps),
997
- stdout: deps.stdout
998
- }, {
999
- cwd: deps.cwd,
1000
- ci: args.ci,
1001
- force: args.force,
1002
- createProject: args.createProject,
1003
- check: args.check,
1004
- ...args.branch === void 0 ? {} : { branch: args.branch }
1005
- });
1006
- if (args.check && result.drift !== void 0 && result.drift.length > 0) return { code: 2 };
1007
- return { code: 0 };
1008
- } catch (error) {
1009
- deps.stderr(`Deploy init failed: ${error.message}`);
1010
- return { code: 2 };
1011
- }
1012
- };
1013
- const runInitCommand = async (argv, deps) => {
1014
- const prepared = await prepareApp({
1015
- argv,
1016
- parse: parseDeployInit,
1017
- loadApp: deps.loadApp,
1018
- cwd: deps.cwd
1019
- });
1020
- if (prepared.kind === "help") {
1021
- deps.stdout(formatDeployHelp());
1022
- return { code: 0 };
1023
- }
1024
- if (prepared.kind === "bad-args") {
1025
- deps.stderr(prepared.message);
1026
- deps.stdout(formatDeployHelp());
1027
- return { code: 3 };
1028
- }
1029
- if (prepared.kind === "load-failed") {
1030
- deps.stderr(prepared.message);
1031
- return { code: 1 };
1032
- }
1033
- return runInitForPreparedApp(deps, prepared.app, prepared.args);
1034
- };
1035
-
1036
- //#endregion
1037
- //#region src/bin/commands/dev.ts
1038
- /** @file `moku dev` command — watch + invalidate + rebuild + serve loop. */
1039
- const makeRebuildHandler = (app, deps) => async (paths) => {
1040
- if (app.content !== void 0) app.content.invalidate(paths);
1041
- try {
1042
- await app.build.run();
1043
- deps.stdout(`[dev] Rebuilt (${paths.length} change${paths.length === 1 ? "" : "s"})`);
1044
- } catch (error) {
1045
- deps.stderr(`[dev] Rebuild failed: ${error.message}`);
1046
- }
1047
- };
1048
- const startDevServer = (app, deps, port) => {
1049
- const outdir = app.config?.build?.outdir ?? "dist";
1050
- const defaultLocale = app.config?.i18n?.defaultLocale ?? "en";
1051
- return deps.serve({
1052
- rootDir: (0, node_path.resolve)(deps.cwd, outdir),
1053
- port,
1054
- defaultLocale
1055
- });
1056
- };
1057
- /**
1058
- * Run the dev subcommand.
1059
- *
1060
- * Builds once, then watches `contentDir` and on each batch invalidates
1061
- * content paths BEFORE calling `app.build.run()` (hard rule from CLAUDE.md).
1062
- *
1063
- * @param argv - Argv after the `dev` keyword.
1064
- * @param deps - Injected IO / loader / watch / serve dependencies.
1065
- * @returns Exit code: 0 ok, 1 config error, 2 build error, 3 arg error.
1066
- */
1067
- const devCommand = async (argv, deps) => {
1068
- const prepared = await prepareApp({
1069
- argv,
1070
- parse: parseDev,
1071
- loadApp: deps.loadApp,
1072
- cwd: deps.cwd
1073
- });
1074
- if (prepared.kind === "help") {
1075
- deps.stdout(formatDevHelp());
1076
- return { code: 0 };
1077
- }
1078
- if (prepared.kind === "bad-args") {
1079
- deps.stderr(prepared.message);
1080
- deps.stdout(formatDevHelp());
1081
- return { code: 3 };
1082
- }
1083
- if (prepared.kind === "load-failed") {
1084
- deps.stderr(prepared.message);
1085
- return { code: 1 };
1086
- }
1087
- try {
1088
- await prepared.app.build.run();
1089
- } catch (error) {
1090
- deps.stderr(`Initial build failed: ${error.message}`);
1091
- return { code: 2 };
1092
- }
1093
- const handle = startDevServer(prepared.app, deps, prepared.args.port);
1094
- deps.stdout(`[dev] Serving at http://localhost:${handle.port}`);
1095
- deps.watch({
1096
- rootDir: deps.cwd,
1097
- contentDir: (0, node_path.resolve)(deps.cwd, "content"),
1098
- onChange: makeRebuildHandler(prepared.app, deps)
1099
- });
1100
- return { code: 0 };
1101
- };
1102
-
1103
- //#endregion
1104
- //#region src/bin/commands/preview.ts
1105
- /** @file `moku preview` command — build once, then serve the outdir. */
1106
- const startPreviewServer = (app, cwd, port, serve) => {
1107
- const outdir = app.config?.build?.outdir ?? "dist";
1108
- const defaultLocale = app.config?.i18n?.defaultLocale ?? "en";
1109
- return serve({
1110
- rootDir: (0, node_path.resolve)(cwd, outdir),
1111
- port,
1112
- defaultLocale
1113
- });
1114
- };
1115
- /**
1116
- * Run the preview subcommand.
1117
- *
1118
- * @param argv - Argv after the `preview` keyword.
1119
- * @param deps - Injected IO / loader / serve dependencies.
1120
- * @returns Exit code: 0 ok, 1 config error, 2 build error, 3 arg error.
1121
- */
1122
- const previewCommand = async (argv, deps) => {
1123
- const prepared = await prepareApp({
1124
- argv,
1125
- parse: parsePreview,
1126
- loadApp: deps.loadApp,
1127
- cwd: deps.cwd
1128
- });
1129
- if (prepared.kind === "help") {
1130
- deps.stdout(formatPreviewHelp());
1131
- return { code: 0 };
1132
- }
1133
- if (prepared.kind === "bad-args") {
1134
- deps.stderr(prepared.message);
1135
- deps.stdout(formatPreviewHelp());
1136
- return { code: 3 };
1137
- }
1138
- if (prepared.kind === "load-failed") {
1139
- deps.stderr(prepared.message);
1140
- return { code: 1 };
1141
- }
1142
- const build = await runBuildOnce(prepared.app, deps.stdout, deps.stderr);
1143
- if (build.code !== 0) return build;
1144
- const handle = startPreviewServer(prepared.app, deps.cwd, prepared.args.port, deps.serve);
1145
- deps.stdout(`[preview] Serving at http://localhost:${handle.port}`);
1146
- return { code: 0 };
1147
- };
1148
-
1149
- //#endregion
1150
- //#region src/bin/load-app.ts
1151
- /** @file Discovers and dynamically imports the consumer's main.ts entry. */
1152
- /**
1153
- * Dynamically import `{cwd}/{folder}/main.ts` and return its default export.
1154
- *
1155
- * Returns a structured result rather than throwing; the CLI maps errors to
1156
- * exit codes.
1157
- *
1158
- * @param options - The cwd and folder to look in.
1159
- * @returns A {@link LoadResult}.
1160
- */
1161
- const loadApp = async (options) => {
1162
- const mainPath = (0, node_path.resolve)(options.cwd, options.folder, "main.ts");
1163
- if (!(0, node_fs.existsSync)(mainPath)) return {
1164
- ok: false,
1165
- message: `main.ts not found at ${mainPath}. Run \`moku <command> <folder>\` with the correct path.`,
1166
- path: mainPath
1167
- };
1168
- try {
1169
- const mod = await import(mainPath);
1170
- if (mod.default === void 0) return {
1171
- ok: false,
1172
- message: `${mainPath} has no default export. Export your createApp() result as default.`,
1173
- path: mainPath
1174
- };
1175
- return {
1176
- ok: true,
1177
- value: mod.default,
1178
- path: mainPath
1179
- };
1180
- } catch (error) {
1181
- return {
1182
- ok: false,
1183
- message: `Failed to import ${mainPath}: ${error.message}`,
1184
- path: mainPath
1185
- };
1186
- }
1187
- };
1188
-
1189
- //#endregion
1190
- //#region src/bin/serve.ts
1191
- /** @file Static file handler used by `moku preview` and `moku dev`. */
1192
- const MIME_TYPES = new Map([
1193
- [".html", "text/html; charset=utf-8"],
1194
- [".css", "text/css; charset=utf-8"],
1195
- [".js", "application/javascript; charset=utf-8"],
1196
- [".mjs", "application/javascript; charset=utf-8"],
1197
- [".json", "application/json; charset=utf-8"],
1198
- [".svg", "image/svg+xml"],
1199
- [".png", "image/png"],
1200
- [".jpg", "image/jpeg"],
1201
- [".jpeg", "image/jpeg"],
1202
- [".gif", "image/gif"],
1203
- [".webp", "image/webp"],
1204
- [".ico", "image/x-icon"],
1205
- [".txt", "text/plain; charset=utf-8"],
1206
- [".xml", "application/xml; charset=utf-8"],
1207
- [".woff", "font/woff"],
1208
- [".woff2", "font/woff2"]
1209
- ]);
1210
- const mimeFor = (path) => MIME_TYPES.get((0, node_path.extname)(path).toLowerCase()) ?? "application/octet-stream";
1211
- const isPathInside = (parent, child) => `${child}${node_path.sep}`.startsWith(`${parent}${node_path.sep}`);
1212
- const resolveTarget = (rootDir, urlPath) => {
1213
- const target = (0, node_path.resolve)(rootDir, (0, node_path.normalize)(`./${decodeURIComponent(urlPath).replaceAll("\\", "/").replace(/\/+/g, "/")}`));
1214
- if (!isPathInside(rootDir, target) && target !== rootDir) return null;
1215
- return target;
1216
- };
1217
- const resolveFilePath = (target, urlPath) => {
1218
- const candidate = urlPath.endsWith("/") ? (0, node_path.resolve)(target, "index.html") : target;
1219
- if (!(0, node_fs.existsSync)(candidate)) return null;
1220
- if (!(0, node_fs.statSync)(candidate).isDirectory()) return candidate;
1221
- const indexed = (0, node_path.resolve)(candidate, "index.html");
1222
- return (0, node_fs.existsSync)(indexed) ? indexed : null;
1223
- };
1224
- const fileResponse = (path) => new Response((0, node_fs.readFileSync)(path), { headers: { "content-type": mimeFor(path) } });
1225
- /**
1226
- * Build a fetch-style handler that serves files from `rootDir`.
1227
- *
1228
- * Behavior:
1229
- * - Bare `/` redirects (307) to `/{defaultLocale}/`.
1230
- * - Paths ending in `/` look for `index.html`.
1231
- * - Path traversal attempts return 403.
1232
- * - Missing files return 404.
1233
- *
1234
- * @param options - The root directory and default locale.
1235
- * @returns A `(request: Request) => Promise<Response>` handler.
1236
- */
1237
- const createStaticHandler = (options) => {
1238
- const root = (0, node_path.resolve)(options.rootDir);
1239
- return async (request) => {
1240
- const url = new URL(request.url);
1241
- if (url.pathname === "/") return new Response(null, {
1242
- status: 307,
1243
- headers: { location: `/${options.defaultLocale}/` }
1244
- });
1245
- const target = resolveTarget(root, url.pathname);
1246
- if (target === null) return new Response("Forbidden", { status: 403 });
1247
- const filePath = resolveFilePath(target, url.pathname);
1248
- if (filePath === null) return new Response("Not Found", { status: 404 });
1249
- return fileResponse(filePath);
1250
- };
1251
- };
1252
-
1253
- //#endregion
1254
- //#region src/bin/watch.ts
1255
- /**
1256
- * Create a trailing-edge debounced change batcher.
1257
- *
1258
- * Each `push(path)` (re)starts the debounce timer. When the timer fires the
1259
- * collected unique paths are passed to `onFlush` and a new batch begins.
1260
- * `flush()` drains immediately; `dispose()` cancels any pending flush without
1261
- * invoking the callback.
1262
- *
1263
- * @param options - Debounce window + flush callback.
1264
- * @returns The {@link ChangeBatcher} handle.
1265
- */
1266
- const createChangeBatcher = (options) => {
1267
- let pending = /* @__PURE__ */ new Set();
1268
- let timer = null;
1269
- const clearTimer = () => {
1270
- if (timer !== null) {
1271
- clearTimeout(timer);
1272
- timer = null;
1273
- }
1274
- };
1275
- const drain = () => {
1276
- clearTimer();
1277
- if (pending.size === 0) return;
1278
- const batch = [...pending];
1279
- pending = /* @__PURE__ */ new Set();
1280
- options.onFlush(batch);
1281
- };
1282
- return {
1283
- push: (path) => {
1284
- pending.add(path);
1285
- clearTimer();
1286
- timer = setTimeout(drain, options.debounceMs);
1287
- },
1288
- flush: drain,
1289
- dispose: () => {
1290
- clearTimer();
1291
- pending = /* @__PURE__ */ new Set();
1292
- }
1293
- };
1294
- };
1295
-
1296
- //#endregion
1297
- //#region src/bin/moku.ts
1298
- /** @file moku CLI entry — wires runtime IO to runCli. NOT a plugin. */
1299
- const findPackageJson = () => {
1300
- const candidates = [
1301
- (0, node_path.resolve)({}.dir, "..", "..", "package.json"),
1302
- (0, node_path.resolve)({}.dir, "..", "package.json"),
1303
- (0, node_path.resolve)(process.cwd(), "package.json")
1304
- ];
1305
- for (const candidate of candidates) if ((0, node_fs.existsSync)(candidate)) return JSON.parse((0, node_fs.readFileSync)(candidate, "utf8"));
1306
- return {};
1307
- };
1308
- const pkg = findPackageJson();
1309
- const stdout = (line) => {
1310
- process.stdout.write(`${line}\n`);
1311
- };
1312
- const stderr = (line) => {
1313
- process.stderr.write(`${line}\n`);
1314
- };
1315
- const serveStatic = (options) => {
1316
- const handler = createStaticHandler({
1317
- rootDir: options.rootDir,
1318
- defaultLocale: options.defaultLocale
1319
- });
1320
- const server = Bun.serve({
1321
- port: options.port,
1322
- fetch: handler
1323
- });
1324
- return {
1325
- port: server.port ?? options.port,
1326
- stop: () => server.stop()
1327
- };
1328
- };
1329
- const startWatcher = (options) => {
1330
- const batcher = createChangeBatcher({
1331
- debounceMs: 50,
1332
- onFlush: (paths) => options.onChange(paths)
1333
- });
1334
- if (!(0, node_fs.existsSync)(options.contentDir)) return { stop: () => batcher.dispose() };
1335
- const watcher = (0, node_fs.watch)(options.contentDir, { recursive: true }, (_event, filename) => {
1336
- if (filename !== null) batcher.push(filename.toString());
1337
- });
1338
- return { stop: () => {
1339
- watcher.close();
1340
- batcher.dispose();
1341
- } };
1342
- };
1343
- const main = async () => {
1344
- const result = await runCli(process.argv.slice(2), {
1345
- cwd: process.cwd(),
1346
- version: pkg.version ?? "0.0.0",
1347
- bunVersion: Bun.version,
1348
- bunEngine: pkg.engines?.bun,
1349
- stdout,
1350
- stderr,
1351
- buildCommand: (argv) => buildCommand(argv, {
1352
- cwd: process.cwd(),
1353
- stdout,
1354
- stderr,
1355
- loadApp
1356
- }),
1357
- devCommand: (argv) => devCommand(argv, {
1358
- cwd: process.cwd(),
1359
- stdout,
1360
- stderr,
1361
- loadApp,
1362
- watch: startWatcher,
1363
- serve: serveStatic
1364
- }),
1365
- previewCommand: (argv) => previewCommand(argv, {
1366
- cwd: process.cwd(),
1367
- stdout,
1368
- stderr,
1369
- loadApp,
1370
- serve: serveStatic
1371
- }),
1372
- deployCommand: (argv) => deployCommand(argv, {
1373
- cwd: process.cwd(),
1374
- stdout,
1375
- stderr,
1376
- loadApp
1377
- })
1378
- });
1379
- process.exit(result.code);
1380
- };
1381
- main();
1382
-
1383
- //#endregion