@moku-labs/web 0.1.0-alpha.1 → 0.1.0-alpha.3

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.
@@ -25,7 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  }) : target, mod));
26
26
 
27
27
  //#endregion
28
- const require_factory = require('./factory-CixCpR9C.cjs');
28
+ const require_factory = require('./factory-DVcAQYEZ.cjs');
29
+ const require_wrangler = require('./wrangler-BHdkyMRj.cjs');
29
30
  let node_fs = require("node:fs");
30
31
  let node_fs_promises = require("node:fs/promises");
31
32
  let node_path = require("node:path");
@@ -442,7 +443,7 @@ const wireContentApi = (ctx) => {
442
443
  //#endregion
443
444
  //#region src/plugins/content/index.ts
444
445
  /** @file content plugin: markdown pipeline + invalidate(). Complex tier. */
445
- const defaultConfig$1 = {
446
+ const defaultConfig$2 = {
446
447
  dir: "",
447
448
  trustedContent: false,
448
449
  rehypePlugins: [],
@@ -450,7 +451,7 @@ const defaultConfig$1 = {
450
451
  };
451
452
  const content = require_factory.createPlugin("content", {
452
453
  depends: [require_factory.i18n],
453
- config: defaultConfig$1,
454
+ config: defaultConfig$2,
454
455
  events: (register) => ({
455
456
  "content:ready": register("Articles loaded"),
456
457
  "content:invalidated": register("Paths invalidated")
@@ -981,7 +982,7 @@ const validateBuildConfig = (ctx) => {
981
982
  //#endregion
982
983
  //#region src/plugins/build/index.ts
983
984
  /** @file build plugin: app.build.run() orchestrator + in-memory manifest. Complex tier. */
984
- const defaultConfig = {
985
+ const defaultConfig$1 = {
985
986
  outdir: "dist",
986
987
  mode: "production",
987
988
  renderMode: "hybrid"
@@ -993,7 +994,7 @@ const build = require_factory.createPlugin("build", {
993
994
  require_factory.router,
994
995
  require_factory.head
995
996
  ],
996
- config: defaultConfig,
997
+ config: defaultConfig$1,
997
998
  events: (register) => ({
998
999
  "build:phase": register("Build phase started"),
999
1000
  "build:complete": register("Build pipeline complete")
@@ -1003,6 +1004,188 @@ const build = require_factory.createPlugin("build", {
1003
1004
  onInit: validateBuildConfig
1004
1005
  });
1005
1006
 
1007
+ //#endregion
1008
+ //#region src/plugins/deploy/api.ts
1009
+ /** @file deploy plugin API factory — orchestrates app.deploy.run(). */
1010
+ /** Free-tier Cloudflare Pages limit. The paid tier (100,000) requires extra env. */
1011
+ const FREE_TIER_FILE_LIMIT = 2e4;
1012
+ /** Per-file size cap for Cloudflare Pages (25 MiB). */
1013
+ const MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
1014
+ const sanitizedProcessEnv = () => {
1015
+ const out = {};
1016
+ for (const [k, v] of Object.entries(process.env)) if (typeof v === "string") out[k] = v;
1017
+ return out;
1018
+ };
1019
+ const checkFile = (path, state) => {
1020
+ state.count += 1;
1021
+ if ((0, node_fs.statSync)(path).size > MAX_FILE_SIZE_BYTES) state.oversize = path;
1022
+ };
1023
+ const walkOutdir = (current, state) => {
1024
+ for (const entry of (0, node_fs.readdirSync)(current, { withFileTypes: true })) {
1025
+ if (state.oversize !== null) return;
1026
+ const path = (0, node_path.join)(current, entry.name);
1027
+ if (entry.isDirectory()) walkOutdir(path, state);
1028
+ else if (entry.isFile()) checkFile(path, state);
1029
+ }
1030
+ };
1031
+ /** Recursively count files and check max size under `dir`. Short-circuits on first oversize file. */
1032
+ const inspectOutdir = (dir) => {
1033
+ const state = {
1034
+ count: 0,
1035
+ oversize: null
1036
+ };
1037
+ walkOutdir(dir, state);
1038
+ return {
1039
+ fileCount: state.count,
1040
+ oversize: state.oversize
1041
+ };
1042
+ };
1043
+ /** Throw if any pre-flight constraint on the resolved outdir fails. */
1044
+ const assertOutdirReady = (outdir, outdirAbsolute) => {
1045
+ if (!(0, node_fs.existsSync)(outdirAbsolute)) throw new Error(`deploy: outdir "${outdir}" does not exist — run \`moku build\` first (or pass --build).`);
1046
+ const inspection = inspectOutdir(outdirAbsolute);
1047
+ if (inspection.fileCount === 0) throw new Error(`deploy: outdir "${outdir}" is empty — nothing to deploy.`);
1048
+ if (inspection.fileCount > FREE_TIER_FILE_LIMIT) throw new Error(`deploy: outdir contains ${inspection.fileCount} files; free tier limit is ${FREE_TIER_FILE_LIMIT}. Paid tier supports up to 100,000 files (requires PAGES_WRANGLER_MAJOR_VERSION=4 in CI env).`);
1049
+ if (inspection.oversize !== null) throw new Error(`deploy: file "${inspection.oversize}" exceeds Cloudflare Pages per-file limit (25 MiB).`);
1050
+ return inspection;
1051
+ };
1052
+ /** Resolve outdir from `wrangler.jsonc#pages_build_output_dir` or throw if missing. */
1053
+ const resolveOutdir = (cwd) => {
1054
+ const wrangler = require_wrangler.readWranglerConfig(cwd);
1055
+ if (wrangler === null) throw new Error("deploy: wrangler.jsonc not found — run `moku deploy init` first");
1056
+ return wrangler.pages_build_output_dir;
1057
+ };
1058
+ /**
1059
+ * Build the public `app.deploy` API surface.
1060
+ *
1061
+ * `run()` reads outdir from `wrangler.jsonc#pages_build_output_dir` (the
1062
+ * deploy-time SSoT — `DeployConfig.outdir` is only the init-time default).
1063
+ * Errors with a "run `moku deploy init` first" message if `wrangler.jsonc` is
1064
+ * missing.
1065
+ *
1066
+ * @param ctx - The deploy plugin context.
1067
+ * @param deps - Optional injectables for testing.
1068
+ * @returns The {@link DeployApi}.
1069
+ */
1070
+ /** Trigger the optional pre-build hook for `--build`. */
1071
+ const maybeRunBuild = async (deps, build) => {
1072
+ if (build !== true) return;
1073
+ if (deps.runBuild === void 0) throw new Error("deploy: --build flag set but no `runBuild` hook provided to createDeployApi. Wire `app.build.run` through the plugin index.");
1074
+ await deps.runBuild();
1075
+ };
1076
+ /** Invoke wrangler and shape its output into a {@link DeployResult}. */
1077
+ const invokeWranglerForDeploy = async (ctx, deps, outdir, branch, startedAt, now) => {
1078
+ const env = deps.env ?? sanitizedProcessEnv();
1079
+ const info = require_wrangler.extractDeploymentInfo((await require_wrangler.runWrangler(require_wrangler.buildWranglerArgs(ctx.config.target, outdir, branch), env, deps.spawn)).ndJson);
1080
+ if (info === null) ctx.log.warn("deploy:no-deployment-info", { note: "wrangler exited 0 but no deployment URL/id was found in ND-JSON output." });
1081
+ return {
1082
+ url: info?.url ?? "",
1083
+ deploymentId: info?.deploymentId ?? "",
1084
+ branch,
1085
+ durationMs: now() - startedAt
1086
+ };
1087
+ };
1088
+ /**
1089
+ * Build the public `app.deploy` API surface.
1090
+ *
1091
+ * `run()` reads outdir from `wrangler.jsonc#pages_build_output_dir` (the
1092
+ * deploy-time SSoT — `DeployConfig.outdir` is only the init-time default).
1093
+ * Errors with a "run `moku deploy init` first" message if `wrangler.jsonc` is
1094
+ * missing.
1095
+ *
1096
+ * @param ctx - The deploy plugin context.
1097
+ * @param deps - Optional injectables for testing.
1098
+ * @returns The {@link DeployApi}.
1099
+ */
1100
+ const createDeployApi = (ctx, deps = {}) => {
1101
+ const cwd = deps.cwd ?? process.cwd();
1102
+ const now = deps.now ?? Date.now;
1103
+ const run = async (options = {}) => {
1104
+ await maybeRunBuild(deps, options.build);
1105
+ const outdir = resolveOutdir(cwd);
1106
+ const branch = options.branch ?? ctx.config.productionBranch;
1107
+ const inspection = assertOutdirReady(outdir, (0, node_path.join)(cwd, outdir));
1108
+ const deployResult = await invokeWranglerForDeploy(ctx, deps, outdir, branch, now(), now);
1109
+ ctx.state.lastDeployment = deployResult;
1110
+ ctx.log.info("deploy:complete", {
1111
+ url: deployResult.url,
1112
+ branch,
1113
+ durationMs: deployResult.durationMs,
1114
+ files: inspection.fileCount
1115
+ });
1116
+ return deployResult;
1117
+ };
1118
+ const getLastDeployment = () => ctx.state.lastDeployment;
1119
+ return {
1120
+ run,
1121
+ getLastDeployment
1122
+ };
1123
+ };
1124
+
1125
+ //#endregion
1126
+ //#region src/plugins/deploy/state.ts
1127
+ /**
1128
+ * Create a fresh deploy state.
1129
+ *
1130
+ * `lastDeployment` starts as `null` and is set after the first successful
1131
+ * `app.deploy.run()` call. State is per-`createApp` instance — never a module
1132
+ * singleton — so parallel vitest workers do not share state.
1133
+ *
1134
+ * @returns A new {@link DeployState}.
1135
+ */
1136
+ const createDeployState = () => ({ lastDeployment: null });
1137
+
1138
+ //#endregion
1139
+ //#region src/plugins/deploy/validate.ts
1140
+ /** @file deploy plugin onInit hook — config-shape validation only. NEVER spawns wrangler. */
1141
+ const VALID_TARGETS = new Set(["pages", "workers"]);
1142
+ /**
1143
+ * Validate `DeployConfig` shape at `onInit` time.
1144
+ *
1145
+ * Checks: `target` is in the valid enum; `outdir` is a non-empty string and
1146
+ * does not escape the current working directory via `..` traversal;
1147
+ * `productionBranch` is a non-empty string.
1148
+ *
1149
+ * Does NOT check wrangler presence — `ensureWrangler()` in `wrangler.ts` is
1150
+ * lazy and called only on first `run()`/`init()` invocation. This keeps
1151
+ * `moku dev`, `moku build`, and `moku preview` unaffected by wrangler absence.
1152
+ *
1153
+ * @param ctx - The deploy plugin context.
1154
+ * @throws Error when any config field is structurally invalid.
1155
+ */
1156
+ const validateDeployConfig = (ctx) => {
1157
+ const { target, outdir, productionBranch } = ctx.config;
1158
+ if (!VALID_TARGETS.has(target)) throw new Error(`deploy: target must be "pages" or "workers" (got "${target}")`);
1159
+ if (typeof outdir !== "string" || outdir === "") throw new Error("deploy: outdir must be a non-empty string");
1160
+ const resolved = (0, node_path.isAbsolute)(outdir) ? outdir : (0, node_path.resolve)(process.cwd(), outdir);
1161
+ const cwd = process.cwd();
1162
+ if (!resolved.startsWith(cwd)) throw new Error(`deploy: outdir "${outdir}" must resolve inside the project root (got "${resolved}")`);
1163
+ if (typeof productionBranch !== "string" || productionBranch === "") throw new Error("deploy: productionBranch must be a non-empty string");
1164
+ };
1165
+
1166
+ //#endregion
1167
+ //#region src/plugins/deploy/index.ts
1168
+ /** @file deploy plugin: Cloudflare Pages publishing via wrangler subprocess. Standard tier.
1169
+ *
1170
+ * Wave 4 placement alongside `build`. `depends: []` — no plugin-graph edge to `build`;
1171
+ * ordering via the framework's plugin registration array is sufficient. Consumers who want
1172
+ * auto-outdir resolution must register the `build` plugin alongside this one; otherwise the
1173
+ * `'dist'` default fallback applies.
1174
+ *
1175
+ * @see README.md
1176
+ */
1177
+ const defaultConfig = {
1178
+ target: "pages",
1179
+ outdir: "dist",
1180
+ productionBranch: "main"
1181
+ };
1182
+ const deploy = require_factory.createPlugin("deploy", {
1183
+ config: defaultConfig,
1184
+ createState: createDeployState,
1185
+ api: createDeployApi,
1186
+ onInit: validateDeployConfig
1187
+ });
1188
+
1006
1189
  //#endregion
1007
1190
  //#region src/project.ts
1008
1191
  /**
@@ -1073,6 +1256,12 @@ Object.defineProperty(exports, 'content', {
1073
1256
  return content;
1074
1257
  }
1075
1258
  });
1259
+ Object.defineProperty(exports, 'deploy', {
1260
+ enumerable: true,
1261
+ get: function () {
1262
+ return deploy;
1263
+ }
1264
+ });
1076
1265
  Object.defineProperty(exports, 'project', {
1077
1266
  enumerable: true,
1078
1267
  get: function () {
@@ -1,7 +1,8 @@
1
1
  import { l as site, o as head, p as createPlugin, s as router, u as i18n } from "./factory-DwpBwjDk.mjs";
2
- import { existsSync, statSync } from "node:fs";
2
+ import { a as runWrangler, c as readWranglerConfig, i as extractDeploymentInfo, r as buildWranglerArgs } from "./wrangler-Coyrznz4.mjs";
3
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
4
  import { readFile, readdir } from "node:fs/promises";
4
- import { resolve, sep } from "node:path";
5
+ import { isAbsolute, join, resolve, sep } from "node:path";
5
6
  import matter from "gray-matter";
6
7
  import rehypeShiki from "@shikijs/rehype";
7
8
  import rehypeRaw from "rehype-raw";
@@ -404,7 +405,7 @@ const wireContentApi = (ctx) => {
404
405
  //#endregion
405
406
  //#region src/plugins/content/index.ts
406
407
  /** @file content plugin: markdown pipeline + invalidate(). Complex tier. */
407
- const defaultConfig$1 = {
408
+ const defaultConfig$2 = {
408
409
  dir: "",
409
410
  trustedContent: false,
410
411
  rehypePlugins: [],
@@ -412,7 +413,7 @@ const defaultConfig$1 = {
412
413
  };
413
414
  const content = createPlugin("content", {
414
415
  depends: [i18n],
415
- config: defaultConfig$1,
416
+ config: defaultConfig$2,
416
417
  events: (register) => ({
417
418
  "content:ready": register("Articles loaded"),
418
419
  "content:invalidated": register("Paths invalidated")
@@ -943,7 +944,7 @@ const validateBuildConfig = (ctx) => {
943
944
  //#endregion
944
945
  //#region src/plugins/build/index.ts
945
946
  /** @file build plugin: app.build.run() orchestrator + in-memory manifest. Complex tier. */
946
- const defaultConfig = {
947
+ const defaultConfig$1 = {
947
948
  outdir: "dist",
948
949
  mode: "production",
949
950
  renderMode: "hybrid"
@@ -955,7 +956,7 @@ const build = createPlugin("build", {
955
956
  router,
956
957
  head
957
958
  ],
958
- config: defaultConfig,
959
+ config: defaultConfig$1,
959
960
  events: (register) => ({
960
961
  "build:phase": register("Build phase started"),
961
962
  "build:complete": register("Build pipeline complete")
@@ -965,6 +966,188 @@ const build = createPlugin("build", {
965
966
  onInit: validateBuildConfig
966
967
  });
967
968
 
969
+ //#endregion
970
+ //#region src/plugins/deploy/api.ts
971
+ /** @file deploy plugin API factory — orchestrates app.deploy.run(). */
972
+ /** Free-tier Cloudflare Pages limit. The paid tier (100,000) requires extra env. */
973
+ const FREE_TIER_FILE_LIMIT = 2e4;
974
+ /** Per-file size cap for Cloudflare Pages (25 MiB). */
975
+ const MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
976
+ const sanitizedProcessEnv = () => {
977
+ const out = {};
978
+ for (const [k, v] of Object.entries(process.env)) if (typeof v === "string") out[k] = v;
979
+ return out;
980
+ };
981
+ const checkFile = (path, state) => {
982
+ state.count += 1;
983
+ if (statSync(path).size > MAX_FILE_SIZE_BYTES) state.oversize = path;
984
+ };
985
+ const walkOutdir = (current, state) => {
986
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
987
+ if (state.oversize !== null) return;
988
+ const path = join(current, entry.name);
989
+ if (entry.isDirectory()) walkOutdir(path, state);
990
+ else if (entry.isFile()) checkFile(path, state);
991
+ }
992
+ };
993
+ /** Recursively count files and check max size under `dir`. Short-circuits on first oversize file. */
994
+ const inspectOutdir = (dir) => {
995
+ const state = {
996
+ count: 0,
997
+ oversize: null
998
+ };
999
+ walkOutdir(dir, state);
1000
+ return {
1001
+ fileCount: state.count,
1002
+ oversize: state.oversize
1003
+ };
1004
+ };
1005
+ /** Throw if any pre-flight constraint on the resolved outdir fails. */
1006
+ const assertOutdirReady = (outdir, outdirAbsolute) => {
1007
+ if (!existsSync(outdirAbsolute)) throw new Error(`deploy: outdir "${outdir}" does not exist — run \`moku build\` first (or pass --build).`);
1008
+ const inspection = inspectOutdir(outdirAbsolute);
1009
+ if (inspection.fileCount === 0) throw new Error(`deploy: outdir "${outdir}" is empty — nothing to deploy.`);
1010
+ if (inspection.fileCount > FREE_TIER_FILE_LIMIT) throw new Error(`deploy: outdir contains ${inspection.fileCount} files; free tier limit is ${FREE_TIER_FILE_LIMIT}. Paid tier supports up to 100,000 files (requires PAGES_WRANGLER_MAJOR_VERSION=4 in CI env).`);
1011
+ if (inspection.oversize !== null) throw new Error(`deploy: file "${inspection.oversize}" exceeds Cloudflare Pages per-file limit (25 MiB).`);
1012
+ return inspection;
1013
+ };
1014
+ /** Resolve outdir from `wrangler.jsonc#pages_build_output_dir` or throw if missing. */
1015
+ const resolveOutdir = (cwd) => {
1016
+ const wrangler = readWranglerConfig(cwd);
1017
+ if (wrangler === null) throw new Error("deploy: wrangler.jsonc not found — run `moku deploy init` first");
1018
+ return wrangler.pages_build_output_dir;
1019
+ };
1020
+ /**
1021
+ * Build the public `app.deploy` API surface.
1022
+ *
1023
+ * `run()` reads outdir from `wrangler.jsonc#pages_build_output_dir` (the
1024
+ * deploy-time SSoT — `DeployConfig.outdir` is only the init-time default).
1025
+ * Errors with a "run `moku deploy init` first" message if `wrangler.jsonc` is
1026
+ * missing.
1027
+ *
1028
+ * @param ctx - The deploy plugin context.
1029
+ * @param deps - Optional injectables for testing.
1030
+ * @returns The {@link DeployApi}.
1031
+ */
1032
+ /** Trigger the optional pre-build hook for `--build`. */
1033
+ const maybeRunBuild = async (deps, build) => {
1034
+ if (build !== true) return;
1035
+ if (deps.runBuild === void 0) throw new Error("deploy: --build flag set but no `runBuild` hook provided to createDeployApi. Wire `app.build.run` through the plugin index.");
1036
+ await deps.runBuild();
1037
+ };
1038
+ /** Invoke wrangler and shape its output into a {@link DeployResult}. */
1039
+ const invokeWranglerForDeploy = async (ctx, deps, outdir, branch, startedAt, now) => {
1040
+ const env = deps.env ?? sanitizedProcessEnv();
1041
+ const info = extractDeploymentInfo((await runWrangler(buildWranglerArgs(ctx.config.target, outdir, branch), env, deps.spawn)).ndJson);
1042
+ if (info === null) ctx.log.warn("deploy:no-deployment-info", { note: "wrangler exited 0 but no deployment URL/id was found in ND-JSON output." });
1043
+ return {
1044
+ url: info?.url ?? "",
1045
+ deploymentId: info?.deploymentId ?? "",
1046
+ branch,
1047
+ durationMs: now() - startedAt
1048
+ };
1049
+ };
1050
+ /**
1051
+ * Build the public `app.deploy` API surface.
1052
+ *
1053
+ * `run()` reads outdir from `wrangler.jsonc#pages_build_output_dir` (the
1054
+ * deploy-time SSoT — `DeployConfig.outdir` is only the init-time default).
1055
+ * Errors with a "run `moku deploy init` first" message if `wrangler.jsonc` is
1056
+ * missing.
1057
+ *
1058
+ * @param ctx - The deploy plugin context.
1059
+ * @param deps - Optional injectables for testing.
1060
+ * @returns The {@link DeployApi}.
1061
+ */
1062
+ const createDeployApi = (ctx, deps = {}) => {
1063
+ const cwd = deps.cwd ?? process.cwd();
1064
+ const now = deps.now ?? Date.now;
1065
+ const run = async (options = {}) => {
1066
+ await maybeRunBuild(deps, options.build);
1067
+ const outdir = resolveOutdir(cwd);
1068
+ const branch = options.branch ?? ctx.config.productionBranch;
1069
+ const inspection = assertOutdirReady(outdir, join(cwd, outdir));
1070
+ const deployResult = await invokeWranglerForDeploy(ctx, deps, outdir, branch, now(), now);
1071
+ ctx.state.lastDeployment = deployResult;
1072
+ ctx.log.info("deploy:complete", {
1073
+ url: deployResult.url,
1074
+ branch,
1075
+ durationMs: deployResult.durationMs,
1076
+ files: inspection.fileCount
1077
+ });
1078
+ return deployResult;
1079
+ };
1080
+ const getLastDeployment = () => ctx.state.lastDeployment;
1081
+ return {
1082
+ run,
1083
+ getLastDeployment
1084
+ };
1085
+ };
1086
+
1087
+ //#endregion
1088
+ //#region src/plugins/deploy/state.ts
1089
+ /**
1090
+ * Create a fresh deploy state.
1091
+ *
1092
+ * `lastDeployment` starts as `null` and is set after the first successful
1093
+ * `app.deploy.run()` call. State is per-`createApp` instance — never a module
1094
+ * singleton — so parallel vitest workers do not share state.
1095
+ *
1096
+ * @returns A new {@link DeployState}.
1097
+ */
1098
+ const createDeployState = () => ({ lastDeployment: null });
1099
+
1100
+ //#endregion
1101
+ //#region src/plugins/deploy/validate.ts
1102
+ /** @file deploy plugin onInit hook — config-shape validation only. NEVER spawns wrangler. */
1103
+ const VALID_TARGETS = new Set(["pages", "workers"]);
1104
+ /**
1105
+ * Validate `DeployConfig` shape at `onInit` time.
1106
+ *
1107
+ * Checks: `target` is in the valid enum; `outdir` is a non-empty string and
1108
+ * does not escape the current working directory via `..` traversal;
1109
+ * `productionBranch` is a non-empty string.
1110
+ *
1111
+ * Does NOT check wrangler presence — `ensureWrangler()` in `wrangler.ts` is
1112
+ * lazy and called only on first `run()`/`init()` invocation. This keeps
1113
+ * `moku dev`, `moku build`, and `moku preview` unaffected by wrangler absence.
1114
+ *
1115
+ * @param ctx - The deploy plugin context.
1116
+ * @throws Error when any config field is structurally invalid.
1117
+ */
1118
+ const validateDeployConfig = (ctx) => {
1119
+ const { target, outdir, productionBranch } = ctx.config;
1120
+ if (!VALID_TARGETS.has(target)) throw new Error(`deploy: target must be "pages" or "workers" (got "${target}")`);
1121
+ if (typeof outdir !== "string" || outdir === "") throw new Error("deploy: outdir must be a non-empty string");
1122
+ const resolved = isAbsolute(outdir) ? outdir : resolve(process.cwd(), outdir);
1123
+ const cwd = process.cwd();
1124
+ if (!resolved.startsWith(cwd)) throw new Error(`deploy: outdir "${outdir}" must resolve inside the project root (got "${resolved}")`);
1125
+ if (typeof productionBranch !== "string" || productionBranch === "") throw new Error("deploy: productionBranch must be a non-empty string");
1126
+ };
1127
+
1128
+ //#endregion
1129
+ //#region src/plugins/deploy/index.ts
1130
+ /** @file deploy plugin: Cloudflare Pages publishing via wrangler subprocess. Standard tier.
1131
+ *
1132
+ * Wave 4 placement alongside `build`. `depends: []` — no plugin-graph edge to `build`;
1133
+ * ordering via the framework's plugin registration array is sufficient. Consumers who want
1134
+ * auto-outdir resolution must register the `build` plugin alongside this one; otherwise the
1135
+ * `'dist'` default fallback applies.
1136
+ *
1137
+ * @see README.md
1138
+ */
1139
+ const defaultConfig = {
1140
+ target: "pages",
1141
+ outdir: "dist",
1142
+ productionBranch: "main"
1143
+ };
1144
+ const deploy = createPlugin("deploy", {
1145
+ config: defaultConfig,
1146
+ createState: createDeployState,
1147
+ api: createDeployApi,
1148
+ onInit: validateDeployConfig
1149
+ });
1150
+
968
1151
  //#endregion
969
1152
  //#region src/project.ts
970
1153
  /**
@@ -1017,4 +1200,4 @@ function extractSpecs(routes) {
1017
1200
  }
1018
1201
 
1019
1202
  //#endregion
1020
- export { build as n, content as r, project as t };
1203
+ export { content as i, deploy as n, build as r, project as t };
@@ -150,6 +150,54 @@ type I18nApi<TLocales extends readonly string[] = readonly string[]> = {
150
150
  t(locale: TLocales[number], key: string): string;
151
151
  };
152
152
  //#endregion
153
+ //#region src/plugins/content/types.d.ts
154
+ type Frontmatter = {
155
+ title: string;
156
+ date: string;
157
+ description: string;
158
+ tags: string[];
159
+ language: string;
160
+ author?: string;
161
+ draft?: boolean;
162
+ [key: string]: unknown;
163
+ };
164
+ type ComputedFields = {
165
+ contentId: string;
166
+ readingTimeMinutes: number;
167
+ wordCount: number;
168
+ };
169
+ type Article = {
170
+ slug: string;
171
+ locale: string;
172
+ frontmatter: Frontmatter;
173
+ html: string;
174
+ computed: ComputedFields;
175
+ };
176
+ type RehypePluginEntry = readonly [unknown, Record<string, unknown>?];
177
+ type RemarkPluginEntry = readonly [unknown, Record<string, unknown>?];
178
+ type ContentConfig = {
179
+ dir: string;
180
+ defaultAuthor?: string;
181
+ trustedContent: boolean;
182
+ rehypePlugins?: RehypePluginEntry[];
183
+ remarkPlugins?: RemarkPluginEntry[];
184
+ };
185
+ type ContentState = {
186
+ processor: Processor | null;
187
+ articles: Map<string, Map<string, Article>>;
188
+ slugs: string[] | null;
189
+ lastPaths: Set<string>; /** Paths marked stale by `invalidate()` — re-read on next `loadAll()` then cleared. */
190
+ dirtyPaths: Set<string>;
191
+ };
192
+ type ContentApi = {
193
+ loadAll(): Promise<Map<string, Article[]>>;
194
+ load(slug: string, locale: string): Promise<Article | null>;
195
+ discoverSlugs(): Promise<string[]>;
196
+ render(markdown: string): Promise<string>;
197
+ invalidate(paths: string[]): void;
198
+ reset(): void;
199
+ };
200
+ //#endregion
153
201
  //#region src/plugins/router/types.d.ts
154
202
  /** @file router plugin types — RouteSpec (non-accumulating), RouteBuilder, RouterApi. */
155
203
  /** Render context passed to route handlers. */
@@ -222,54 +270,6 @@ type RouterApi<Routes extends Record<string, RouteSpec> = Record<string, RouteSp
222
270
  entries(): ReadonlyArray<RouteEntry>;
223
271
  };
224
272
  //#endregion
225
- //#region src/plugins/content/types.d.ts
226
- type Frontmatter = {
227
- title: string;
228
- date: string;
229
- description: string;
230
- tags: string[];
231
- language: string;
232
- author?: string;
233
- draft?: boolean;
234
- [key: string]: unknown;
235
- };
236
- type ComputedFields = {
237
- contentId: string;
238
- readingTimeMinutes: number;
239
- wordCount: number;
240
- };
241
- type Article = {
242
- slug: string;
243
- locale: string;
244
- frontmatter: Frontmatter;
245
- html: string;
246
- computed: ComputedFields;
247
- };
248
- type RehypePluginEntry = readonly [unknown, Record<string, unknown>?];
249
- type RemarkPluginEntry = readonly [unknown, Record<string, unknown>?];
250
- type ContentConfig = {
251
- dir: string;
252
- defaultAuthor?: string;
253
- trustedContent: boolean;
254
- rehypePlugins?: RehypePluginEntry[];
255
- remarkPlugins?: RemarkPluginEntry[];
256
- };
257
- type ContentState = {
258
- processor: Processor | null;
259
- articles: Map<string, Map<string, Article>>;
260
- slugs: string[] | null;
261
- lastPaths: Set<string>; /** Paths marked stale by `invalidate()` — re-read on next `loadAll()` then cleared. */
262
- dirtyPaths: Set<string>;
263
- };
264
- type ContentApi = {
265
- loadAll(): Promise<Map<string, Article[]>>;
266
- load(slug: string, locale: string): Promise<Article | null>;
267
- discoverSlugs(): Promise<string[]>;
268
- render(markdown: string): Promise<string>;
269
- invalidate(paths: string[]): void;
270
- reset(): void;
271
- };
272
- //#endregion
273
273
  //#region src/plugins/build/types.d.ts
274
274
  type BuildConfig = {
275
275
  outdir: string;
@@ -299,6 +299,70 @@ type BuildApi = {
299
299
  getManifest(): Readonly<BundleManifest> | null;
300
300
  };
301
301
  //#endregion
302
+ //#region src/plugins/deploy/types.d.ts
303
+ /** @file deploy plugin types — DeployConfig, DeployState, DeployResult, RunOptions, DeployApi. */
304
+ /**
305
+ * Deployment target. Phase 1 ships `'pages'` only; `'workers'` is reserved
306
+ * for the Workers Static Assets expansion path and `buildWranglerArgs`
307
+ * throws "not implemented in Phase 1" if invoked with it.
308
+ */
309
+ type DeployTarget = 'pages' | 'workers';
310
+ /**
311
+ * deploy plugin config.
312
+ *
313
+ * `outdir` precedence at deploy time: `wrangler.jsonc#pages_build_output_dir`
314
+ * (written by `moku deploy init`) — this is the deploy-time single source of
315
+ * truth, NOT `DeployConfig.outdir`. The config field is the *init-time* default;
316
+ * once `wrangler.jsonc` exists, it owns the value.
317
+ *
318
+ * Forbidden: reading the `build` plugin's config via `ctx.has(build)` /
319
+ * `ctx.require(build).config.outdir`. The core `HasFunction` takes a name
320
+ * string and returns a non-narrowing boolean; `RequireFunction` returns the
321
+ * API surface only (`BuildApi = { run, getManifest }`). The optional coupling
322
+ * is a `ctx.log.warn` at init time when divergence is detectable through the
323
+ * directly-imported `build` plugin instance, never via the core API.
324
+ */
325
+ type DeployConfig = {
326
+ /** Deployment target. Default: `'pages'`. */target: DeployTarget; /** Build output directory used at init only. Default: `'dist'` (matches build plugin default). */
327
+ outdir: string; /** Production branch name. Default: `'main'`. Overwritten by `init` via `git symbolic-ref`. */
328
+ productionBranch: string; /** Optional explicit override for the Cloudflare Pages project name. Default: derived from `site.name`. */
329
+ projectName?: string;
330
+ };
331
+ /**
332
+ * In-memory deploy plugin state.
333
+ *
334
+ * `lastDeployment` is reset per `createApp()` call and captures the most
335
+ * recent deploy's metadata (URL, deployment ID, branch, duration). Used by
336
+ * the future REST-API-based rollback command (deferred from Phase 1).
337
+ */
338
+ type DeployState = {
339
+ lastDeployment: DeployResult | null;
340
+ };
341
+ /** Result of a successful `app.deploy.run()` invocation. */
342
+ type DeployResult = {
343
+ /** Production URL (e.g., `https://<project>.pages.dev`) or branch-alias preview URL. */url: string; /** Cloudflare deployment ID — captured for future rollback support. */
344
+ deploymentId: string; /** Branch the deploy was published to. */
345
+ branch: string; /** Total subprocess duration in milliseconds (init + upload + finalize). */
346
+ durationMs: number;
347
+ };
348
+ /**
349
+ * Options accepted by `app.deploy.run()`.
350
+ *
351
+ * `branch` overrides the production branch from wrangler.jsonc (defaults to
352
+ * `DeployConfig.productionBranch`). `build` runs `app.build.run()` first
353
+ * when true — used only for local convenience; the generated CI workflow
354
+ * always issues the two-step `moku build` then `moku deploy` pattern.
355
+ */
356
+ type RunOptions = {
357
+ /** Override the production branch for this deploy. */branch?: string; /** Run `app.build.run()` first (local convenience). Default: `false`. */
358
+ build?: boolean;
359
+ };
360
+ /** Public deploy plugin API surface, attached as `app.deploy`. */
361
+ type DeployApi = {
362
+ /** Run a deploy. Reads outdir from wrangler.jsonc; errors if missing. */run(options?: RunOptions): Promise<DeployResult>; /** Read the last deployment result without re-running. */
363
+ getLastDeployment(): Readonly<DeployResult> | null;
364
+ };
365
+ //#endregion
302
366
  //#region src/project.d.ts
303
367
  /**
304
368
  * Map each consumer `RouteBuilder` to the `RouteSpec` shape the router stores.
@@ -346,4 +410,4 @@ type WebAppConfig<Routes extends Record<string, RouteBuilder>> = {
346
410
  */
347
411
  declare function route(pattern: string): RouteBuilder;
348
412
  //#endregion
349
- export { EnvApi as C, EnvVarSpec as D, EnvState as E, LogApi as O, Events as S, EnvProvider as T, I18nConfig as _, BuildConfig as a, SiteState as b, ContentApi as c, RouteBuilder as d, RouteSpec as f, I18nApi as g, RouterState as h, BuildApi as i, LogState as k, ContentConfig as l, RouterConfig as m, RouteSpecMap as n, BuildState as o, RouterApi as p, WebAppConfig as r, Article as s, route as t, ContentState as u, I18nState as v, EnvConfig as w, Config as x, SiteApi as y };
413
+ export { EnvVarSpec as A, SiteState as C, EnvConfig as D, EnvApi as E, LogState as M, EnvProvider as O, SiteApi as S, Events as T, ContentConfig as _, DeployConfig as a, I18nConfig as b, BuildConfig as c, RouteSpec as d, RouterApi as f, ContentApi as g, Article as h, DeployApi as i, LogApi as j, EnvState as k, BuildState as l, RouterState as m, RouteSpecMap as n, DeployState as o, RouterConfig as p, WebAppConfig as r, BuildApi as s, route as t, RouteBuilder as u, ContentState as v, Config as w, I18nState as x, I18nApi as y };
package/dist/test.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_project = require('./project-C1vtMxE8.cjs');
3
- const require_factory = require('./factory-CixCpR9C.cjs');
2
+ const require_project = require('./project-1pAh4RxJ.cjs');
3
+ const require_factory = require('./factory-DVcAQYEZ.cjs');
4
4
  const require_index = require('./index.cjs');
5
5
  let _moku_labs_core = require("@moku-labs/core");
6
6