devflare 1.0.0-next.64 → 1.0.0-next.66

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 (94) hide show
  1. package/README.md +2 -1
  2. package/dist/_chunks/{account-Cflj6rw3.js → account-BHY3fvrA.js} +6 -6
  3. package/dist/_chunks/{account-resources-CsmVowJp.js → account-resources-BdKj8q1h.js} +3 -3
  4. package/dist/_chunks/{ai-CBEuLdoP.js → ai-CN_SREim.js} +1 -1
  5. package/dist/_chunks/{build-BZ6lIG7n.js → build-DCgW1i8g.js} +2 -2
  6. package/dist/_chunks/{build-artifacts-CXLdD1PE.js → build-artifacts-Btqs5gPy.js} +12 -12
  7. package/dist/_chunks/{cli-Bf3qZEGS.js → cli-CWKMIIcG.js} +63 -20
  8. package/dist/_chunks/{cloudflare-BaloqI3H.js → cloudflare-Ic5ubQza.js} +6 -6
  9. package/dist/_chunks/{command-utils-CSnoFdxM.js → command-utils-DjaaMQX7.js} +2 -2
  10. package/dist/_chunks/{compiler-CgIFPfRA.js → compiler-UxXQgEPq.js} +3 -3
  11. package/dist/_chunks/{config-path-C3ac6r66.js → config-path-CYGlwKDs.js} +2 -2
  12. package/dist/_chunks/{config-BwRb2_vD.js → config-zQd4jmXU.js} +3 -3
  13. package/dist/_chunks/{deploy-CujD7j-R.js → deploy-DqYme109.js} +13 -13
  14. package/dist/_chunks/dev-DOoDj7SJ.js +757 -0
  15. package/dist/_chunks/{doctor-n_eqvv52.js → doctor-CAsJ-rSZ.js} +7 -7
  16. package/dist/_chunks/{env-_QKteEYJ.js → env-DyL6JOXI.js} +1 -1
  17. package/dist/_chunks/{family-D3SV6I0N.js → family-BTTpTwUk.js} +1 -1
  18. package/dist/_chunks/{init-DnkJOmpW.js → init-DJ5y3mu5.js} +3 -3
  19. package/dist/_chunks/{loader-E_UGyTrJ.js → loader-BC0skbnH.js} +5 -5
  20. package/dist/_chunks/{local-secrets-6sMstHXw.js → local-secrets-C0n5oRgI.js} +1 -1
  21. package/dist/_chunks/{login-D9-EwpSA.js → login-CgNcbpEq.js} +4 -4
  22. package/dist/_chunks/{preferences-BKp_7XJx.js → preferences-CbYERA_m.js} +1 -1
  23. package/dist/_chunks/{preview-registry-ClAbLTHH.js → preview-registry-CTXU4mBc.js} +2 -2
  24. package/dist/_chunks/{preview-resources-D_aFwtZR.js → preview-resources-CQIu2-NY.js} +5 -5
  25. package/dist/_chunks/{previews-D-GPVCeq.js → previews-BIaw6NVC.js} +10 -10
  26. package/dist/_chunks/{productions-me2YvmWK.js → productions-CF8oQcX7.js} +7 -7
  27. package/dist/_chunks/{r2-presign-runtime-zTOLS0Ea.js → r2-presign-runtime-C75FkLMd.js} +4 -4
  28. package/dist/_chunks/{remote-CZnP-HLJ.js → remote-8jEHLVGg.js} +2 -2
  29. package/dist/_chunks/{resolve-package-qizBfCIm.js → resolve-package-DeJUGxoE.js} +1 -1
  30. package/dist/_chunks/{routes-BMU7ga_l.js → routes-CRXB3-iw.js} +6 -6
  31. package/dist/_chunks/{runtime-Ckf-KKc7.js → runtime-CuMe2D6n.js} +1 -1
  32. package/dist/_chunks/{secrets-CxVCXmdh.js → secrets-BJuEa9Jz.js} +1 -1
  33. package/dist/_chunks/{tail-aMFzd-2K.js → tail-DjGRt8gw.js} +4 -4
  34. package/dist/_chunks/{token-Dmi0KXyX.js → token-wmeF8-IQ.js} +4 -4
  35. package/dist/_chunks/{tokens-iVcgNXi-.js → tokens-KaEr4B8e.js} +1 -1
  36. package/dist/_chunks/{types-ByjwxoVA.js → types-BLGfxIq-.js} +9 -9
  37. package/dist/_chunks/{ui-BUoZApvE.js → ui-BhCdDC2N.js} +1 -1
  38. package/dist/_chunks/{usage-BBTGzjon.js → usage-rRusKejc.js} +2 -2
  39. package/dist/_chunks/{vite-BBiqD3k7.js → vite-Dxuhm1BP.js} +11 -11
  40. package/dist/_chunks/{dev-C3PpZ4zg.js → vite-process-BxZ8pb7i.js} +25 -766
  41. package/dist/_chunks/{worker-CtTVZeTH.js → worker-DgczaXVW.js} +6 -6
  42. package/dist/_chunks/workspace-CaTsTpDI.js +506 -0
  43. package/dist/_chunks/workspace-Ds3xMjOE.js +175 -0
  44. package/dist/browser.js +4 -4
  45. package/dist/cli/commands/workspace.d.ts +8 -0
  46. package/dist/cli/commands/workspace.d.ts.map +1 -0
  47. package/dist/cli/help-pages/pages/core.d.ts +1 -1
  48. package/dist/cli/help-pages/pages/core.d.ts.map +1 -1
  49. package/dist/cli/help-pages/shared.d.ts +1 -1
  50. package/dist/cli/help-pages/shared.d.ts.map +1 -1
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +1 -1
  53. package/dist/cloudflare/index.js +3 -3
  54. package/dist/config/index.d.ts +1 -0
  55. package/dist/config/index.d.ts.map +1 -1
  56. package/dist/config/workspace.d.ts +282 -0
  57. package/dist/config/workspace.d.ts.map +1 -0
  58. package/dist/config-entry.js +1 -1
  59. package/dist/decorators/index.js +1 -1
  60. package/dist/dev-server/miniflare-dev-config.d.ts +8 -0
  61. package/dist/dev-server/miniflare-dev-config.d.ts.map +1 -1
  62. package/dist/dev-server/workspace/merge-config.d.ts +81 -0
  63. package/dist/dev-server/workspace/merge-config.d.ts.map +1 -0
  64. package/dist/dev-server/workspace/prepare-app.d.ts +84 -0
  65. package/dist/dev-server/workspace/prepare-app.d.ts.map +1 -0
  66. package/dist/dev-server/workspace/server.d.ts +47 -0
  67. package/dist/dev-server/workspace/server.d.ts.map +1 -0
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +9 -8
  71. package/dist/runtime/index.js +3 -3
  72. package/dist/sveltekit/index.js +6 -6
  73. package/dist/test/index.js +17 -17
  74. package/dist/vite/index.js +1 -1
  75. package/package.json +1 -1
  76. package/dist/_chunks/{api-TzdliH-6.js → api-BKClUlUf.js} +0 -0
  77. package/dist/_chunks/{colors-DiK6wPJ5.js → colors-D0NVzAFN.js} +0 -0
  78. package/dist/_chunks/{context-CQkLyah9.js → context-ZQydom21.js} +0 -0
  79. package/dist/_chunks/{decorators-QmV57ixr.js → decorators-eXNZ1S0N.js} +0 -0
  80. package/dist/_chunks/{dependencies-C2oxFjU_.js → dependencies-wKqrxKV_.js} +0 -0
  81. package/dist/_chunks/{durable-object-DMtH0XYX.js → durable-object-DuMPM8re.js} +0 -0
  82. package/dist/_chunks/{generated-artifacts-dMJY4KAt.js → generated-artifacts-Bb13pGUG.js} +0 -0
  83. package/dist/_chunks/{glob-CmQOvunB.js → glob-DRw9sbfF.js} +0 -0
  84. package/dist/_chunks/{local-dev-vars-CTSa-wvF.js → local-dev-vars-BFLS-HoI.js} +1 -1
  85. package/dist/_chunks/{local-hyperdrive-CJ90j46O.js → local-hyperdrive-CcORzo76.js} +0 -0
  86. package/dist/_chunks/{local-media-bindings-DsAW4AMF.js → local-media-bindings-ZQe-epuw.js} +0 -0
  87. package/dist/_chunks/{package-metadata-DjHBLB-z.js → package-metadata-CLI488gB.js} +1 -1
  88. package/dist/_chunks/{preview-C3Cdr8an.js → preview-j9Kbx4-G.js} +1 -1
  89. /package/dist/_chunks/{remote-config-D39BY9ji.js → remote-config-Bw6qIVM4.js} +0 -0
  90. /package/dist/_chunks/{schema-normalization-Zgm4W6O6.js → schema-normalization-CTdiY17P.js} +0 -0
  91. /package/dist/_chunks/{vite-utils-OVLMV605.js → vite-utils-D8kPPxUy.js} +0 -0
  92. /package/dist/_chunks/{worker-bundler-dTTnYU3n.js → worker-bundler-Isj5jiW6.js} +0 -0
  93. /package/dist/_chunks/{worker-entrypoint-CQW77lG8.js → worker-entrypoint-C8j23t6d.js} +0 -0
  94. /package/dist/_chunks/{workerName-CFJsLZA-.js → workerName-kEXwmV2m.js} +0 -0
@@ -1,9 +1,9 @@
1
- import { r as loadConfig } from "./loader-E_UGyTrJ.js";
2
- import { o as normalizeDOBinding } from "./schema-normalization-Zgm4W6O6.js";
3
- import { t as account } from "./cloudflare-BaloqI3H.js";
4
- import { d as green, f as logLine, g as yellow, h as whiteDim, n as bold, o as dim, r as createCliTheme } from "./ui-BUoZApvE.js";
5
- import { r as resolveCloudflareAccountId, t as asOptionalString } from "./command-utils-CSnoFdxM.js";
6
- import { n as formatSupportedConfigFilenames, r as resolveConfigCandidatePath, t as findConfigPathsUnderDirectory } from "./config-path-C3ac6r66.js";
1
+ import { r as loadConfig } from "./loader-BC0skbnH.js";
2
+ import { o as normalizeDOBinding } from "./schema-normalization-CTdiY17P.js";
3
+ import { t as account } from "./cloudflare-Ic5ubQza.js";
4
+ import { d as green, f as logLine, g as yellow, h as whiteDim, n as bold, o as dim, r as createCliTheme } from "./ui-BhCdDC2N.js";
5
+ import { r as resolveCloudflareAccountId, t as asOptionalString } from "./command-utils-DjaaMQX7.js";
6
+ import { n as formatSupportedConfigFilenames, r as resolveConfigCandidatePath, t as findConfigPathsUnderDirectory } from "./config-path-CYGlwKDs.js";
7
7
  import { basename, dirname, relative, resolve } from "pathe";
8
8
  import MagicString from "magic-string";
9
9
  //#region src/cli/commands/worker.ts
@@ -0,0 +1,506 @@
1
+ import { i as resolveAppDirectSocketPort, r as loadWorkspaceManifest, t as assertSharedBindingIds } from "./workspace-Ds3xMjOE.js";
2
+ import { d as resolveConfigEnvVars } from "./preview-j9Kbx4-G.js";
3
+ import { r as loadConfig } from "./loader-BC0skbnH.js";
4
+ import { g as normalizeR2Binding, n as getLocalKVNamespaceIdentifier, r as getSingleBrowserBindingName, t as getLocalD1DatabaseIdentifier } from "./schema-normalization-CTdiY17P.js";
5
+ import { t as applyLocalDevVarsToConfig } from "./local-dev-vars-BFLS-HoI.js";
6
+ import { a as cyanBold, f as logLine, g as yellow, o as dim, r as createCliTheme } from "./ui-BhCdDC2N.js";
7
+ import { setLocalSendEmailBindings } from "../utils/send-email.js";
8
+ import { s as resolveServiceBindings, t as discoverRoutes } from "./routes-CRXB3-iw.js";
9
+ import { l as prepareComposedWorkerEntrypoint } from "./vite-Dxuhm1BP.js";
10
+ import { t as bundleWorkerEntry } from "./worker-bundler-Isj5jiW6.js";
11
+ import { _ as createMiniflareLog, c as resolveViteIntegration, d as createRuntimeStdioForwarder, f as buildMiniflareDevConfig, g as runD1Migrations, h as resolveMainWorkerSurfacePaths, p as resolveR2PresignOrigin, t as startViteProcess, u as createBrowserShim, v as createDOBundler } from "./vite-process-BxZ8pb7i.js";
12
+ import { n as bundleWorkflowEntrypointScript } from "./r2-presign-runtime-C75FkLMd.js";
13
+ import { dirname, resolve } from "pathe";
14
+ import { createConsola } from "consola";
15
+ //#region src/dev-server/workspace/merge-config.ts
16
+ /**
17
+ * Prefix `name` with `${appName}/` — but only when it is one of this app's own
18
+ * worker names. External/cross-app targets are left untouched.
19
+ */
20
+ function namespaceIfLocal(name, appName, localNames) {
21
+ return localNames.has(name) ? `${appName}/${name}` : name;
22
+ }
23
+ /** Rewrite an intra-app worker reference on a service-binding value (object or string form). */
24
+ function rewriteServiceBindingTarget(value, appName, localNames) {
25
+ if (typeof value === "string") return namespaceIfLocal(value, appName, localNames);
26
+ if (value && typeof value === "object" && typeof value.name === "string") value.name = namespaceIfLocal(value.name, appName, localNames);
27
+ return value;
28
+ }
29
+ /**
30
+ * Namespace one app's workers in place: rename each worker `${appName}/<name>`,
31
+ * rewrite every intra-app reference (service bindings, `durableObjects`
32
+ * scriptName, `outboundService`) to the new names, drop `routes`, and attach the
33
+ * app's direct socket to its gateway (entry) worker.
34
+ *
35
+ * @param appName - The app namespace (must be unique across the workspace).
36
+ * @param workers - The app's worker array (mutated). The gateway is `workers[0]`.
37
+ * @param options - `host`/`directSocketPort` for the gateway's direct socket.
38
+ * @returns The (same) workers plus the namespaced gateway worker name — the key
39
+ * for `miniflare.unsafeGetDirectURL(...)`.
40
+ */
41
+ function namespaceAppWorkers(appName, workers, options) {
42
+ const localNames = new Set(workers.map((worker) => String(worker.name)));
43
+ for (const worker of workers) {
44
+ delete worker.routes;
45
+ if (worker.serviceBindings && typeof worker.serviceBindings === "object") for (const [bindingName, target] of Object.entries(worker.serviceBindings)) worker.serviceBindings[bindingName] = rewriteServiceBindingTarget(target, appName, localNames);
46
+ if (worker.durableObjects && typeof worker.durableObjects === "object") {
47
+ for (const value of Object.values(worker.durableObjects)) if (value && typeof value === "object" && typeof value.scriptName === "string") value.scriptName = namespaceIfLocal(value.scriptName, appName, localNames);
48
+ }
49
+ if (worker.outboundService !== void 0) worker.outboundService = rewriteServiceBindingTarget(worker.outboundService, appName, localNames);
50
+ worker.name = `${appName}/${worker.name}`;
51
+ }
52
+ const gatewayWorkerName = `${appName}/gateway`;
53
+ const gatewayWorker = workers.find((worker) => worker.name === gatewayWorkerName) ?? workers[0];
54
+ gatewayWorker.unsafeDirectSockets = [{
55
+ host: options.host,
56
+ port: options.directSocketPort,
57
+ entrypoint: "default"
58
+ }];
59
+ return {
60
+ workers,
61
+ gatewayWorkerName: gatewayWorker.name
62
+ };
63
+ }
64
+ /** Compute the one persist block for the whole workspace, or `undefined` when off. */
65
+ function resolveWorkspacePersistPaths(persist, persistDir) {
66
+ if (!persist) return;
67
+ return {
68
+ kvPersist: resolve(persistDir, "kv"),
69
+ r2Persist: resolve(persistDir, "r2"),
70
+ d1Persist: resolve(persistDir, "d1"),
71
+ cachePersist: resolve(persistDir, "cache"),
72
+ durableObjectsPersist: resolve(persistDir, "do"),
73
+ workflowsPersist: resolve(persistDir, "workflows"),
74
+ imagesPersist: resolve(persistDir, "images"),
75
+ streamPersist: resolve(persistDir, "stream")
76
+ };
77
+ }
78
+ /**
79
+ * Assemble the final single-instance Miniflare config: one entry socket, one
80
+ * persist dir, and the union of every app's namespaced workers (each with its
81
+ * own direct socket).
82
+ *
83
+ * @param input - Apps, host, persist settings, and entry port.
84
+ * @returns `{ config, directSockets }` where `config` is the Miniflare options
85
+ * object and `directSockets` maps each app name to its gateway worker name
86
+ * and port (for `unsafeGetDirectURL` + logging).
87
+ * @throws When two apps share a namespace or a direct-socket port (which would
88
+ * collide inside the one instance).
89
+ */
90
+ function buildMergedWorkspaceConfig(input) {
91
+ const seenAppNames = /* @__PURE__ */ new Set();
92
+ const seenPorts = /* @__PURE__ */ new Map();
93
+ const mergedWorkers = [];
94
+ const directSockets = [];
95
+ for (const app of input.apps) {
96
+ if (seenAppNames.has(app.appName)) throw new Error(`Duplicate workspace app namespace "${app.appName}". Give each app a unique name.`);
97
+ seenAppNames.add(app.appName);
98
+ const portOwner = seenPorts.get(app.directSocketPort);
99
+ if (portOwner !== void 0) throw new Error(`Workspace apps "${portOwner}" and "${app.appName}" both use port ${app.directSocketPort}. Each app needs a distinct direct-socket port.`);
100
+ seenPorts.set(app.directSocketPort, app.appName);
101
+ const { workers, gatewayWorkerName } = namespaceAppWorkers(app.appName, app.workers, {
102
+ host: input.host,
103
+ directSocketPort: app.directSocketPort
104
+ });
105
+ mergedWorkers.push(...workers);
106
+ directSockets.push({
107
+ appName: app.appName,
108
+ gatewayWorkerName,
109
+ port: app.directSocketPort
110
+ });
111
+ }
112
+ const persistPaths = resolveWorkspacePersistPaths(input.persist, input.persistDir);
113
+ return {
114
+ config: {
115
+ host: input.host,
116
+ port: input.entryPort ?? 0,
117
+ ...persistPaths ?? {},
118
+ workers: mergedWorkers
119
+ },
120
+ directSockets
121
+ };
122
+ }
123
+ //#endregion
124
+ //#region src/dev-server/workspace/prepare-app.ts
125
+ /**
126
+ * Load an app's config and build its Miniflare worker set, mirroring the
127
+ * single-app dev startup up to (but not including) `new Miniflare(...)`.
128
+ *
129
+ * Side effects: bundles the worker/workflow/DO entrypoints under the app's
130
+ * `.devflare/`, and starts a browser-rendering shim when the app binds one. The
131
+ * returned `doBundler`/`browserShim` are disposed by the coordinator.
132
+ *
133
+ * @param input - App identity, ports, and shared instance settings.
134
+ * @returns The prepared app (see {@link PreparedWorkspaceApp}).
135
+ */
136
+ async function prepareWorkspaceApp(input) {
137
+ const { appCwd, configPath, directSocketPort, host, logger } = input;
138
+ const config = await applyLocalDevVarsToConfig(await resolveConfigEnvVars(await loadConfig({
139
+ cwd: appCwd,
140
+ configFile: configPath
141
+ }), {
142
+ cwd: appCwd,
143
+ configPath,
144
+ mode: "dev"
145
+ }), {
146
+ cwd: appCwd,
147
+ configPath
148
+ });
149
+ const appName = input.appName ?? config.name;
150
+ const serviceBindingResolution = config.bindings?.services ? await resolveServiceBindings(config, appCwd) : null;
151
+ let enableVite = false;
152
+ let generatedViteConfigPath = null;
153
+ if (input.vite) {
154
+ const viteIntegration = await resolveViteIntegration({
155
+ cwd: appCwd,
156
+ configPath,
157
+ miniflarePort: directSocketPort,
158
+ enableViteRequested: true,
159
+ logger
160
+ });
161
+ enableVite = viteIntegration.enableVite;
162
+ generatedViteConfigPath = viteIntegration.generatedViteConfigPath;
163
+ if (!enableVite) logger?.warn(`Workspace app "${appName}" set vite: true but no Vite config was found; running it worker-only.`);
164
+ }
165
+ const workflowEntrypointScript = await bundleWorkflowEntrypointScript(config, appCwd, { logger });
166
+ const mainWorkerSurfacePaths = await resolveMainWorkerSurfacePaths(appCwd, config);
167
+ const mainWorkerRoutes = await discoverRoutes(appCwd, config);
168
+ const mainWorkerScriptPath = await prepareComposedWorkerEntrypoint(appCwd, config, void 0, { devInternalEmail: true }) ?? null;
169
+ let bundledMainWorkerScriptPath = null;
170
+ if (mainWorkerScriptPath && !enableVite) bundledMainWorkerScriptPath = await bundleWorkerEntry({
171
+ cwd: appCwd,
172
+ inputFile: mainWorkerScriptPath,
173
+ outFile: resolve(appCwd, ".devflare", "worker-entrypoints", "main.js"),
174
+ rolldownOptions: config.rolldown?.options,
175
+ sourcemap: config.rolldown?.sourcemap,
176
+ minify: config.rolldown?.minify,
177
+ logger
178
+ });
179
+ let doResult = null;
180
+ let doBundler = null;
181
+ const doPattern = config.files?.durableObjects;
182
+ if (typeof doPattern === "string" && doPattern) {
183
+ doBundler = createDOBundler({
184
+ cwd: appCwd,
185
+ pattern: doPattern,
186
+ outDir: resolve(appCwd, ".devflare", "do-bundles"),
187
+ rolldownOptions: config.rolldown?.options,
188
+ sourcemap: config.rolldown?.sourcemap,
189
+ minify: config.rolldown?.minify,
190
+ logger,
191
+ onRebuild: async () => {}
192
+ });
193
+ doResult = await doBundler.build();
194
+ }
195
+ let browserShim = null;
196
+ if (getSingleBrowserBindingName(config.bindings?.browser)) {
197
+ browserShim = createBrowserShim({
198
+ port: input.browserShimPort,
199
+ host: "127.0.0.1",
200
+ logger,
201
+ verbose: input.verbose
202
+ });
203
+ await browserShim.start();
204
+ }
205
+ const workers = buildMiniflareDevConfig({
206
+ config,
207
+ cwd: appCwd,
208
+ miniflarePort: directSocketPort,
209
+ miniflareHost: host,
210
+ persist: input.persist,
211
+ enableVite,
212
+ debug: input.debug,
213
+ mainWorkerSurfacePaths,
214
+ mainWorkerRoutes,
215
+ mainWorkerScriptPath,
216
+ bundledMainWorkerScriptPath,
217
+ workflowEntrypointScript,
218
+ browserShimPort: input.browserShimPort,
219
+ doResult,
220
+ serviceBindingResolution,
221
+ r2PresignSecret: input.r2PresignSecret,
222
+ additionalInjectedVars: input.env,
223
+ logger
224
+ }).workers;
225
+ const sharedIds = {
226
+ d1: {},
227
+ r2: {},
228
+ kv: {}
229
+ };
230
+ for (const [name, cfg] of Object.entries(config.bindings?.d1 ?? {})) sharedIds.d1[name] = getLocalD1DatabaseIdentifier(cfg);
231
+ for (const [name, cfg] of Object.entries(config.bindings?.r2 ?? {})) sharedIds.r2[name] = normalizeR2Binding(cfg).bucketName;
232
+ for (const [name, cfg] of Object.entries(config.bindings?.kv ?? {})) sharedIds.kv[name] = getLocalKVNamespaceIdentifier(cfg);
233
+ return {
234
+ appName,
235
+ appCwd,
236
+ configPath,
237
+ config,
238
+ workers,
239
+ directSocketPort,
240
+ vitePort: input.vitePort,
241
+ enableVite,
242
+ generatedViteConfigPath,
243
+ doBundler,
244
+ browserShim,
245
+ sendEmailBindings: config.bindings?.sendEmail ?? {},
246
+ sharedIds
247
+ };
248
+ }
249
+ //#endregion
250
+ //#region src/dev-server/workspace/server.ts
251
+ /** The first browser-shim port; each app that binds browser rendering gets `+index`. */
252
+ const BROWSER_SHIM_BASE_PORT = 9700;
253
+ /** Delay before running migrations, matching the single-app path's stabilization wait. */
254
+ const MIGRATION_STABILIZE_DELAY_MS = 1e3;
255
+ /**
256
+ * Create a workspace dev server that co-hosts every manifest app in one
257
+ * Miniflare instance with live-shared bindings.
258
+ *
259
+ * @param options - The manifest, its directory, and logging/persist settings.
260
+ * @returns A {@link WorkspaceDevServer} handle.
261
+ */
262
+ function createWorkspaceDevServer(options) {
263
+ const { manifest, manifestDir, logger } = options;
264
+ const persist = options.persist ?? true;
265
+ const verbose = options.verbose ?? false;
266
+ const debug = options.debug ?? process.env.DEVFLARE_DEBUG === "true";
267
+ const host = manifest.host ?? "127.0.0.1";
268
+ const persistDir = resolve(manifestDir, manifest.persist ?? ".devflare/workspace-data");
269
+ const r2PresignSecret = `${crypto.randomUUID()}${crypto.randomUUID()}`;
270
+ let miniflare = null;
271
+ let preparedApps = [];
272
+ const viteChildren = [];
273
+ const appOrigins = [];
274
+ /**
275
+ * Prepare each manifest app (sequentially — config load + bundling mutate
276
+ * app-local `.devflare/` dirs and we want deterministic, non-interleaved
277
+ * work).
278
+ */
279
+ async function prepareAllApps() {
280
+ const prepared = [];
281
+ for (let index = 0; index < manifest.apps.length; index++) {
282
+ const app = manifest.apps[index];
283
+ const label = app.name ?? app.config;
284
+ const directSocketPort = resolveAppDirectSocketPort(app, label);
285
+ const configPath = resolve(manifestDir, app.config);
286
+ logger?.info(`Preparing workspace app "${label}" (port ${directSocketPort})…`);
287
+ prepared.push(await prepareWorkspaceApp({
288
+ appName: app.name,
289
+ configPath,
290
+ appCwd: dirname(configPath),
291
+ directSocketPort,
292
+ vite: app.vite ?? false,
293
+ vitePort: app.vitePort ?? 5173,
294
+ env: app.env,
295
+ persist,
296
+ debug,
297
+ verbose,
298
+ browserShimPort: BROWSER_SHIM_BASE_PORT + index,
299
+ r2PresignSecret,
300
+ host,
301
+ logger
302
+ }));
303
+ }
304
+ return prepared;
305
+ }
306
+ /** Merge sendEmail bindings from all apps and register them once (global registry). */
307
+ function registerSharedSendEmailBindings(apps) {
308
+ const merged = {};
309
+ for (const app of apps) Object.assign(merged, app.sendEmailBindings);
310
+ setLocalSendEmailBindings(merged);
311
+ }
312
+ async function startMiniflare(apps) {
313
+ const { Miniflare, Log, LogLevel } = await import("miniflare");
314
+ const { config, directSockets } = buildMergedWorkspaceConfig({
315
+ apps: apps.map((app) => ({
316
+ appName: app.appName,
317
+ workers: app.workers,
318
+ directSocketPort: app.directSocketPort
319
+ })),
320
+ host,
321
+ persist,
322
+ persistDir,
323
+ entryPort: manifest.entryPort
324
+ });
325
+ const log = createMiniflareLog(Log, LogLevel, "DEBUG", logger);
326
+ if (log) config.log = log;
327
+ config.handleRuntimeStdio = createRuntimeStdioForwarder(logger);
328
+ miniflare = new Miniflare(config);
329
+ await miniflare.ready;
330
+ for (const socket of directSockets) {
331
+ const url = await miniflare.unsafeGetDirectURL(socket.gatewayWorkerName);
332
+ const app = apps.find((candidate) => candidate.appName === socket.appName);
333
+ const isVite = app?.enableVite ?? false;
334
+ const origin = isVite ? `http://${host}:${app?.vitePort ?? socket.port}` : url.href;
335
+ appOrigins.push({
336
+ appName: socket.appName,
337
+ url: origin,
338
+ vite: isVite
339
+ });
340
+ logger?.success(isVite ? `App "${socket.appName}" → Vite ${origin} (bridge ${url.href})` : `App "${socket.appName}" → ${url.href}`);
341
+ }
342
+ }
343
+ /** Run each app's D1 migrations once against the shared store (ledger dedupes). */
344
+ async function runMigrations(apps) {
345
+ await new Promise((r) => setTimeout(r, MIGRATION_STABILIZE_DELAY_MS));
346
+ for (const app of apps) {
347
+ if (!app.config.bindings?.d1) continue;
348
+ await runD1Migrations({
349
+ cwd: app.appCwd,
350
+ config: app.config,
351
+ miniflarePort: app.directSocketPort,
352
+ logger
353
+ });
354
+ }
355
+ }
356
+ /** Spawn a Vite child per Vite app, pointed at its gateway's direct socket (bridge). */
357
+ async function startViteChildren(apps) {
358
+ for (const app of apps) {
359
+ if (!app.enableVite) continue;
360
+ const child = await startViteProcess({
361
+ cwd: app.appCwd,
362
+ configPath: app.configPath,
363
+ vitePort: app.vitePort,
364
+ miniflarePort: app.directSocketPort,
365
+ generatedViteConfigPath: app.generatedViteConfigPath,
366
+ r2Presign: app.config.bindings?.r2 ? {
367
+ secret: r2PresignSecret,
368
+ origin: resolveR2PresignOrigin(app.config.server, host, app.directSocketPort)
369
+ } : null,
370
+ logger
371
+ });
372
+ viteChildren.push(child);
373
+ }
374
+ }
375
+ async function start() {
376
+ logger?.info("Starting workspace dev server…");
377
+ preparedApps = await prepareAllApps();
378
+ if (manifest.shared) assertSharedBindingIds(manifest.shared, preparedApps.map((app) => ({
379
+ appLabel: app.appName,
380
+ d1: app.sharedIds.d1,
381
+ r2: app.sharedIds.r2,
382
+ kv: app.sharedIds.kv
383
+ })));
384
+ registerSharedSendEmailBindings(preparedApps);
385
+ await startMiniflare(preparedApps);
386
+ await startViteChildren(preparedApps);
387
+ await runMigrations(preparedApps);
388
+ logger?.success(`Workspace ready — ${preparedApps.length} app(s) sharing one instance`);
389
+ }
390
+ async function stop() {
391
+ for (const child of viteChildren) child.kill();
392
+ viteChildren.length = 0;
393
+ for (const app of preparedApps) {
394
+ try {
395
+ await app.doBundler?.close();
396
+ } catch (error) {
397
+ logger?.debug(`DO bundler close failed for ${app.appName}: ${String(error)}`);
398
+ }
399
+ try {
400
+ await app.browserShim?.stop();
401
+ } catch (error) {
402
+ logger?.debug(`Browser shim stop failed for ${app.appName}: ${String(error)}`);
403
+ }
404
+ }
405
+ if (miniflare) {
406
+ await miniflare.dispose();
407
+ miniflare = null;
408
+ }
409
+ }
410
+ return {
411
+ start,
412
+ stop,
413
+ getMiniflare: () => miniflare,
414
+ getAppOrigins: () => appOrigins
415
+ };
416
+ }
417
+ //#endregion
418
+ //#region src/cli/commands/workspace.ts
419
+ /**
420
+ * Run the `workspace` command. Only the `dev` subcommand exists today; any
421
+ * other (or missing) subcommand prints guidance and exits non-zero.
422
+ */
423
+ async function runWorkspaceCommand(parsed, logger, options) {
424
+ const subcommand = parsed.args[0] ?? "dev";
425
+ if (subcommand !== "dev") {
426
+ logger.error(`Unknown workspace subcommand: ${subcommand}`);
427
+ logger.info("Usage: devflare workspace dev");
428
+ return { exitCode: 1 };
429
+ }
430
+ const cwd = options.cwd || parsed.options.cwd || process.cwd();
431
+ const configFile = parsed.options.config;
432
+ const persist = parsed.options["no-persist"] !== true;
433
+ const debugEnabled = parsed.options.debug === true || process.env.DEVFLARE_DEBUG === "true";
434
+ const verbose = parsed.options.verbose === true || debugEnabled;
435
+ const theme = createCliTheme(parsed.options);
436
+ const devLogger = createConsola({ level: verbose ? 4 : 3 });
437
+ let loaded;
438
+ try {
439
+ loaded = await loadWorkspaceManifest({
440
+ cwd,
441
+ configFile
442
+ });
443
+ } catch (error) {
444
+ logger.error(error instanceof Error ? error.message : String(error));
445
+ return { exitCode: 1 };
446
+ }
447
+ logLine(logger);
448
+ logLine(logger, `${cyanBold("workspace", theme)} ${dim("Shared Dev Runtime", theme)}`);
449
+ logLine(logger, " ├─ One Miniflare: live-shared D1/KV/R2/Durable Objects");
450
+ logLine(logger, " ├─ Per-app direct sockets: cross-origin preserved");
451
+ logLine(logger, ` └─ ${loaded.manifest.apps.length} app(s) from the workspace manifest`);
452
+ logLine(logger);
453
+ const devServer = createWorkspaceDevServer({
454
+ manifest: loaded.manifest,
455
+ manifestDir: loaded.manifestDir,
456
+ persist,
457
+ logger: devLogger,
458
+ verbose,
459
+ debug: debugEnabled
460
+ });
461
+ let isCleaningUp = false;
462
+ const cleanupHandlers = /* @__PURE__ */ new Map();
463
+ const removeCleanupHandlers = () => {
464
+ for (const [event, handler] of cleanupHandlers) process.off(event, handler);
465
+ cleanupHandlers.clear();
466
+ };
467
+ const cleanup = async (exitCode, reason) => {
468
+ if (isCleaningUp) return;
469
+ isCleaningUp = true;
470
+ removeCleanupHandlers();
471
+ if (reason) {
472
+ const message = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
473
+ logger.error(message);
474
+ }
475
+ logLine(logger);
476
+ logLine(logger, `${yellow("workspace", theme)} ${dim("Shutting down…", theme)}`);
477
+ try {
478
+ await devServer.stop();
479
+ } finally {
480
+ process.exit(exitCode);
481
+ }
482
+ };
483
+ const registerCleanupHandler = (event, handler) => {
484
+ cleanupHandlers.set(event, handler);
485
+ process.on(event, handler);
486
+ };
487
+ registerCleanupHandler("SIGINT", () => void cleanup(0));
488
+ registerCleanupHandler("SIGTERM", () => void cleanup(0));
489
+ registerCleanupHandler("SIGHUP", () => void cleanup(0));
490
+ registerCleanupHandler("uncaughtException", (error) => void cleanup(1, error));
491
+ registerCleanupHandler("unhandledRejection", (reason) => void cleanup(1, reason));
492
+ try {
493
+ await devServer.start();
494
+ await new Promise(() => {});
495
+ return { exitCode: 0 };
496
+ } catch (error) {
497
+ if (error instanceof Error) {
498
+ logger.error("Workspace dev server failed:", error.message);
499
+ if (verbose) logger.error(error.stack);
500
+ }
501
+ await devServer.stop().catch(() => {});
502
+ return { exitCode: 1 };
503
+ }
504
+ }
505
+ //#endregion
506
+ export { runWorkspaceCommand };
@@ -0,0 +1,175 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, resolve } from "pathe";
4
+ import { z } from "zod";
5
+ //#region src/config/workspace.ts
6
+ /**
7
+ * Per-app entry in a workspace manifest.
8
+ *
9
+ * A pure worker app is reached directly on {@link WorkspaceApp.port}; a Vite app
10
+ * is served by Vite on {@link WorkspaceApp.vitePort} and reaches its bindings
11
+ * through the gateway bridge on {@link WorkspaceApp.bridgePort}.
12
+ */
13
+ const workspaceAppSchema = z.object({
14
+ config: z.string().min(1),
15
+ name: z.string().min(1).optional(),
16
+ port: z.number().int().positive().max(65535).optional(),
17
+ vite: z.boolean().optional(),
18
+ vitePort: z.number().int().positive().max(65535).optional(),
19
+ bridgePort: z.number().int().positive().max(65535).optional(),
20
+ env: z.record(z.string()).optional()
21
+ }).strict();
22
+ const workspaceManifestSchema = z.object({
23
+ apps: z.array(workspaceAppSchema).min(1),
24
+ shared: z.object({
25
+ d1: z.array(z.string()).optional(),
26
+ r2: z.array(z.string()).optional(),
27
+ kv: z.array(z.string()).optional()
28
+ }).strict().optional(),
29
+ persist: z.string().min(1).optional(),
30
+ host: z.string().min(1).optional(),
31
+ entryPort: z.number().int().nonnegative().max(65535).optional()
32
+ }).strict();
33
+ /**
34
+ * Type-safe helper for defining a devflare workspace manifest. Mirrors
35
+ * {@link defineConfig}: it only attaches types and returns the value unchanged.
36
+ *
37
+ * @param manifest - The workspace manifest.
38
+ * @returns The same manifest, typed.
39
+ *
40
+ * @example
41
+ * // devflare.workspace.ts (repo root)
42
+ * export default defineWorkspace({
43
+ * apps: [
44
+ * { config: './apps/api/devflare.config.ts', port: 8789, env: { DOC_API_DEV_SEED: '1' } },
45
+ * { config: './apps/web/devflare.config.ts', vite: true, vitePort: 5173, bridgePort: 8788 }
46
+ * ],
47
+ * shared: { d1: ['PLATFORM_DB'], r2: ['MEDIA'] }
48
+ * })
49
+ */
50
+ function defineWorkspace(manifest) {
51
+ return manifest;
52
+ }
53
+ /** Manifest file names searched at the workspace root, in priority order. */
54
+ const WORKSPACE_MANIFEST_FILES = [
55
+ "devflare.workspace.ts",
56
+ "devflare.workspace.mts",
57
+ "devflare.workspace.js",
58
+ "devflare.workspace.mjs"
59
+ ];
60
+ /**
61
+ * The browser/direct-socket port a given app is reached on inside the shared
62
+ * instance: `bridgePort` for a Vite app, `port` for a pure worker. Throws a
63
+ * descriptive error when the required port is missing.
64
+ *
65
+ * @param app - A validated workspace app entry.
66
+ * @param label - Human-readable app label for error messages.
67
+ * @returns The direct-socket port for the app's gateway worker.
68
+ * @throws When a Vite app omits `bridgePort` or a worker app omits `port`.
69
+ */
70
+ function resolveAppDirectSocketPort(app, label) {
71
+ if (app.vite) {
72
+ if (app.bridgePort === void 0) throw new Error(`Workspace app "${label}" sets vite: true but is missing "bridgePort".`);
73
+ return app.bridgePort;
74
+ }
75
+ if (app.port === void 0) throw new Error(`Workspace app "${label}" is a pure worker but is missing "port".`);
76
+ return app.port;
77
+ }
78
+ /**
79
+ * c12 resolves TypeScript/ESM manifests via jiti. Prefer the target project's
80
+ * own `c12` install (mirrors {@link loadConfig}) so a monorepo that vendors
81
+ * devflare per-app still transpiles the manifest; fall back to devflare's own.
82
+ */
83
+ function resolveC12LoadConfig(cwd) {
84
+ const requireFromCwd = createRequire(join(cwd, "__devflare_workspace__.cjs"));
85
+ try {
86
+ return requireFromCwd("c12").loadConfig;
87
+ } catch {
88
+ return createRequire(import.meta.url)("c12").loadConfig;
89
+ }
90
+ }
91
+ /**
92
+ * Load, transpile, and validate a workspace manifest.
93
+ *
94
+ * @param options - Search directory and optional explicit file.
95
+ * @returns The validated manifest plus its resolved path/dir.
96
+ * @throws {WorkspaceManifestNotFoundError} When no manifest file is found.
97
+ * @throws {WorkspaceManifestValidationError} When the manifest fails schema validation.
98
+ */
99
+ async function loadWorkspaceManifest(options = {}) {
100
+ const cwd = resolve(options.cwd ?? process.cwd());
101
+ const configFile = options.configFile ?? "devflare.workspace";
102
+ const explicitManifestPath = options.configFile ? resolve(cwd, options.configFile) : void 0;
103
+ if (!(explicitManifestPath !== void 0 && existsSync(explicitManifestPath) || WORKSPACE_MANIFEST_FILES.some((name) => existsSync(join(cwd, name))))) throw new WorkspaceManifestNotFoundError(cwd);
104
+ const { config, configFile: loadedFile } = await resolveC12LoadConfig(cwd)({
105
+ name: "devflare",
106
+ cwd,
107
+ configFile,
108
+ rcFile: false,
109
+ globalRc: false,
110
+ dotenv: false
111
+ });
112
+ if (!config || !loadedFile) throw new WorkspaceManifestNotFoundError(cwd);
113
+ const result = workspaceManifestSchema.safeParse(config);
114
+ if (!result.success) throw new WorkspaceManifestValidationError(result.error.issues, loadedFile);
115
+ return {
116
+ manifest: result.data,
117
+ manifestPath: loadedFile,
118
+ manifestDir: dirname(loadedFile)
119
+ };
120
+ }
121
+ /** Error thrown when no workspace manifest file is found. */
122
+ var WorkspaceManifestNotFoundError = class extends Error {
123
+ code = "WORKSPACE_MANIFEST_NOT_FOUND";
124
+ constructor(cwd) {
125
+ super(`No workspace manifest found in ${cwd}.\nExpected one of: ${WORKSPACE_MANIFEST_FILES.join(", ")}\nCreate one that exports \`defineWorkspace({ apps: [...] })\`.`);
126
+ this.cwd = cwd;
127
+ this.name = "WorkspaceManifestNotFoundError";
128
+ }
129
+ };
130
+ /** Error thrown when a workspace manifest fails schema validation. */
131
+ var WorkspaceManifestValidationError = class extends Error {
132
+ code = "WORKSPACE_MANIFEST_VALIDATION_ERROR";
133
+ constructor(issues, manifestPath) {
134
+ const issueMessages = issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`);
135
+ super(`Invalid workspace manifest in ${manifestPath}:\n${issueMessages.join("\n")}`);
136
+ this.issues = issues;
137
+ this.manifestPath = manifestPath;
138
+ this.name = "WorkspaceManifestValidationError";
139
+ }
140
+ };
141
+ /**
142
+ * Assert that every binding name listed in `manifest.shared` resolves to the
143
+ * SAME local identifier across the apps that declare it — otherwise the apps
144
+ * would silently get SEPARATE stores. Sharing is by id, so a mismatch is a real
145
+ * bug in the configs, surfaced here as a loud error rather than a subtle
146
+ * "writes don't appear" symptom.
147
+ *
148
+ * @param assertions - The `shared` block from the manifest.
149
+ * @param resolved - Per-app resolved identifiers: `{ appLabel, d1, r2, kv }`
150
+ * where each map is `bindingName -> localIdentifier` for that app.
151
+ * @throws When a shared binding resolves to differing ids (or is absent everywhere).
152
+ */
153
+ function assertSharedBindingIds(assertions, resolved) {
154
+ for (const kind of [
155
+ "d1",
156
+ "r2",
157
+ "kv"
158
+ ]) for (const bindingName of assertions[kind] ?? []) {
159
+ const seen = [];
160
+ for (const app of resolved) {
161
+ const id = app[kind][bindingName];
162
+ if (id !== void 0) seen.push({
163
+ appLabel: app.appLabel,
164
+ id
165
+ });
166
+ }
167
+ if (seen.length === 0) throw new Error(`Workspace "shared.${kind}" lists "${bindingName}", but no app declares that ${kind.toUpperCase()} binding.`);
168
+ if (new Set(seen.map((entry) => entry.id)).size > 1) {
169
+ const detail = seen.map((entry) => `${entry.appLabel} → ${entry.id}`).join(", ");
170
+ throw new Error(`Workspace "shared.${kind}" binding "${bindingName}" resolves to different ids across apps (${detail}); they would NOT share a store. Align the ${kind.toUpperCase()} id in the app configs.`);
171
+ }
172
+ }
173
+ }
174
+ //#endregion
175
+ export { resolveAppDirectSocketPort as i, defineWorkspace as n, loadWorkspaceManifest as r, assertSharedBindingIds as t };