mates-fullstack 1.0.0-beta.1

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 (202) hide show
  1. package/README.md +311 -0
  2. package/dist/arctic-auth.d.ts +101 -0
  3. package/dist/arctic-auth.d.ts.map +1 -0
  4. package/dist/arctic-auth.js +538 -0
  5. package/dist/arctic-auth.js.map +1 -0
  6. package/dist/asset-manifest.d.ts +14 -0
  7. package/dist/asset-manifest.d.ts.map +1 -0
  8. package/dist/asset-manifest.js +102 -0
  9. package/dist/asset-manifest.js.map +1 -0
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/browser.d.ts.map +1 -0
  12. package/dist/browser.js +25 -0
  13. package/dist/browser.js.map +1 -0
  14. package/dist/build-esbuild.d.ts +29 -0
  15. package/dist/build-esbuild.d.ts.map +1 -0
  16. package/dist/build-esbuild.js +699 -0
  17. package/dist/build-esbuild.js.map +1 -0
  18. package/dist/build-prod.d.ts +126 -0
  19. package/dist/build-prod.d.ts.map +1 -0
  20. package/dist/build-prod.js +1014 -0
  21. package/dist/build-prod.js.map +1 -0
  22. package/dist/cli-new.d.ts +14 -0
  23. package/dist/cli-new.d.ts.map +1 -0
  24. package/dist/cli-new.js +637 -0
  25. package/dist/cli-new.js.map +1 -0
  26. package/dist/client.d.ts +43 -0
  27. package/dist/client.d.ts.map +1 -0
  28. package/dist/client.js +130 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/cors.d.ts +16 -0
  31. package/dist/cors.d.ts.map +1 -0
  32. package/dist/cors.js +60 -0
  33. package/dist/cors.js.map +1 -0
  34. package/dist/ctx.d.ts +78 -0
  35. package/dist/ctx.d.ts.map +1 -0
  36. package/dist/ctx.js +280 -0
  37. package/dist/ctx.js.map +1 -0
  38. package/dist/dev-watcher.d.ts +23 -0
  39. package/dist/dev-watcher.d.ts.map +1 -0
  40. package/dist/dev-watcher.js +136 -0
  41. package/dist/dev-watcher.js.map +1 -0
  42. package/dist/docs-generator.d.ts +69 -0
  43. package/dist/docs-generator.d.ts.map +1 -0
  44. package/dist/docs-generator.js +557 -0
  45. package/dist/docs-generator.js.map +1 -0
  46. package/dist/docs-page.d.ts +20 -0
  47. package/dist/docs-page.d.ts.map +1 -0
  48. package/dist/docs-page.js +1152 -0
  49. package/dist/docs-page.js.map +1 -0
  50. package/dist/download.d.ts +78 -0
  51. package/dist/download.d.ts.map +1 -0
  52. package/dist/download.js +202 -0
  53. package/dist/download.js.map +1 -0
  54. package/dist/env-loader.d.ts +76 -0
  55. package/dist/env-loader.d.ts.map +1 -0
  56. package/dist/env-loader.js +213 -0
  57. package/dist/env-loader.js.map +1 -0
  58. package/dist/errors.d.ts +146 -0
  59. package/dist/errors.d.ts.map +1 -0
  60. package/dist/errors.js +386 -0
  61. package/dist/errors.js.map +1 -0
  62. package/dist/head.d.ts +31 -0
  63. package/dist/head.d.ts.map +1 -0
  64. package/dist/head.js +245 -0
  65. package/dist/head.js.map +1 -0
  66. package/dist/index.d.ts +30 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +30 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/internal-prefixes.d.ts +16 -0
  71. package/dist/internal-prefixes.d.ts.map +1 -0
  72. package/dist/internal-prefixes.js +16 -0
  73. package/dist/internal-prefixes.js.map +1 -0
  74. package/dist/internal.d.ts +25 -0
  75. package/dist/internal.d.ts.map +1 -0
  76. package/dist/internal.js +25 -0
  77. package/dist/internal.js.map +1 -0
  78. package/dist/jwt.d.ts +166 -0
  79. package/dist/jwt.d.ts.map +1 -0
  80. package/dist/jwt.js +261 -0
  81. package/dist/jwt.js.map +1 -0
  82. package/dist/log.d.ts +44 -0
  83. package/dist/log.d.ts.map +1 -0
  84. package/dist/log.js +66 -0
  85. package/dist/log.js.map +1 -0
  86. package/dist/logger.d.ts +76 -0
  87. package/dist/logger.d.ts.map +1 -0
  88. package/dist/logger.js +138 -0
  89. package/dist/logger.js.map +1 -0
  90. package/dist/main-runner.d.ts +59 -0
  91. package/dist/main-runner.d.ts.map +1 -0
  92. package/dist/main-runner.js +157 -0
  93. package/dist/main-runner.js.map +1 -0
  94. package/dist/mates-auth.d.ts +82 -0
  95. package/dist/mates-auth.d.ts.map +1 -0
  96. package/dist/mates-auth.js +323 -0
  97. package/dist/mates-auth.js.map +1 -0
  98. package/dist/middleware.d.ts +30 -0
  99. package/dist/middleware.d.ts.map +1 -0
  100. package/dist/middleware.js +67 -0
  101. package/dist/middleware.js.map +1 -0
  102. package/dist/project-resolver.d.ts +102 -0
  103. package/dist/project-resolver.d.ts.map +1 -0
  104. package/dist/project-resolver.js +271 -0
  105. package/dist/project-resolver.js.map +1 -0
  106. package/dist/rate-limit.d.ts +37 -0
  107. package/dist/rate-limit.d.ts.map +1 -0
  108. package/dist/rate-limit.js +109 -0
  109. package/dist/rate-limit.js.map +1 -0
  110. package/dist/redirect.d.ts +84 -0
  111. package/dist/redirect.d.ts.map +1 -0
  112. package/dist/redirect.js +105 -0
  113. package/dist/redirect.js.map +1 -0
  114. package/dist/renderer.d.ts +91 -0
  115. package/dist/renderer.d.ts.map +1 -0
  116. package/dist/renderer.js +630 -0
  117. package/dist/renderer.js.map +1 -0
  118. package/dist/request-logger.d.ts +12 -0
  119. package/dist/request-logger.d.ts.map +1 -0
  120. package/dist/request-logger.js +55 -0
  121. package/dist/request-logger.js.map +1 -0
  122. package/dist/rest.d.ts +25 -0
  123. package/dist/rest.d.ts.map +1 -0
  124. package/dist/rest.js +93 -0
  125. package/dist/rest.js.map +1 -0
  126. package/dist/router.d.ts +71 -0
  127. package/dist/router.d.ts.map +1 -0
  128. package/dist/router.js +222 -0
  129. package/dist/router.js.map +1 -0
  130. package/dist/rpc-registry.d.ts +84 -0
  131. package/dist/rpc-registry.d.ts.map +1 -0
  132. package/dist/rpc-registry.js +271 -0
  133. package/dist/rpc-registry.js.map +1 -0
  134. package/dist/rpc-runner.d.ts +82 -0
  135. package/dist/rpc-runner.d.ts.map +1 -0
  136. package/dist/rpc-runner.js +564 -0
  137. package/dist/rpc-runner.js.map +1 -0
  138. package/dist/sanitize.d.ts +61 -0
  139. package/dist/sanitize.d.ts.map +1 -0
  140. package/dist/sanitize.js +193 -0
  141. package/dist/sanitize.js.map +1 -0
  142. package/dist/security-headers.d.ts +114 -0
  143. package/dist/security-headers.d.ts.map +1 -0
  144. package/dist/security-headers.js +121 -0
  145. package/dist/security-headers.js.map +1 -0
  146. package/dist/server-fn.d.ts +323 -0
  147. package/dist/server-fn.d.ts.map +1 -0
  148. package/dist/server-fn.js +373 -0
  149. package/dist/server-fn.js.map +1 -0
  150. package/dist/server-public.d.ts +13 -0
  151. package/dist/server-public.d.ts.map +1 -0
  152. package/dist/server-public.js +12 -0
  153. package/dist/server-public.js.map +1 -0
  154. package/dist/server-timeout.d.ts +38 -0
  155. package/dist/server-timeout.d.ts.map +1 -0
  156. package/dist/server-timeout.js +46 -0
  157. package/dist/server-timeout.js.map +1 -0
  158. package/dist/server.d.ts +100 -0
  159. package/dist/server.d.ts.map +1 -0
  160. package/dist/server.js +1218 -0
  161. package/dist/server.js.map +1 -0
  162. package/dist/socket-router.d.ts +153 -0
  163. package/dist/socket-router.d.ts.map +1 -0
  164. package/dist/socket-router.js +612 -0
  165. package/dist/socket-router.js.map +1 -0
  166. package/dist/sso.d.ts +90 -0
  167. package/dist/sso.d.ts.map +1 -0
  168. package/dist/sso.js +261 -0
  169. package/dist/sso.js.map +1 -0
  170. package/dist/ssr-context.d.ts +49 -0
  171. package/dist/ssr-context.d.ts.map +1 -0
  172. package/dist/ssr-context.js +85 -0
  173. package/dist/ssr-context.js.map +1 -0
  174. package/dist/ssr-globals.d.ts +32 -0
  175. package/dist/ssr-globals.d.ts.map +1 -0
  176. package/dist/ssr-globals.js +1010 -0
  177. package/dist/ssr-globals.js.map +1 -0
  178. package/dist/ssr-template.d.ts +73 -0
  179. package/dist/ssr-template.d.ts.map +1 -0
  180. package/dist/ssr-template.js +507 -0
  181. package/dist/ssr-template.js.map +1 -0
  182. package/dist/stack-mapper.d.ts +25 -0
  183. package/dist/stack-mapper.d.ts.map +1 -0
  184. package/dist/stack-mapper.js +139 -0
  185. package/dist/stack-mapper.js.map +1 -0
  186. package/dist/stream.d.ts +89 -0
  187. package/dist/stream.d.ts.map +1 -0
  188. package/dist/stream.js +299 -0
  189. package/dist/stream.js.map +1 -0
  190. package/dist/upload.d.ts +69 -0
  191. package/dist/upload.d.ts.map +1 -0
  192. package/dist/upload.js +110 -0
  193. package/dist/upload.js.map +1 -0
  194. package/dist/validate.d.ts +58 -0
  195. package/dist/validate.d.ts.map +1 -0
  196. package/dist/validate.js +89 -0
  197. package/dist/validate.js.map +1 -0
  198. package/dist/verify-package.d.ts +3 -0
  199. package/dist/verify-package.d.ts.map +1 -0
  200. package/dist/verify-package.js +128 -0
  201. package/dist/verify-package.js.map +1 -0
  202. package/package.json +79 -0
@@ -0,0 +1,1014 @@
1
+ /**
2
+ * mates-fullstack — build-prod.ts
3
+ *
4
+ * Production bundler using esbuild.
5
+ * Same tool as dev (build-esbuild.ts) but runs a full bundle pass
6
+ * with minification, tree-shaking, content-hashed output, and
7
+ * the RPC stub + server guard plugins.
8
+ *
9
+ * Plugins:
10
+ * 1. rpcStubPlugin — replaces server/api/ imports with RPC stubs
11
+ * 2. serverGuardPlugin — build error if server/helpers/ imported from client
12
+ * 3. emptyCssPlugin — removes JS-side CSS imports; CSS is linked separately
13
+ * 4. dedupePlugin — forces single mates + lit-html instance
14
+ *
15
+ * CSS:
16
+ * CSS is bundled separately into one stylesheet by buildStyles() and linked
17
+ * in the HTML shell. Client JS imports of .css files are compatibility no-ops.
18
+ *
19
+ * Output:
20
+ * dist/assets/client-[hash].js
21
+ * dist/assets/styles-[hash].css
22
+ */
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+ import crypto from "node:crypto";
26
+ import * as esbuild from "esbuild";
27
+ import { findMatesPath } from "./project-resolver.js";
28
+ import { getBundlerDefines } from "./env-loader.js";
29
+ import { devWarn } from "./log.js";
30
+ import { INTERNAL_MATES_PREFIX } from "./internal-prefixes.js";
31
+ // ─── Reserved export names (matches rpc-registry.ts) ─────────────────────────
32
+ const RESERVED_NAMES = new Set([
33
+ "default",
34
+ "loader",
35
+ "meta",
36
+ "layout",
37
+ "staticPaths",
38
+ "onConnect",
39
+ "onDisconnect",
40
+ "then",
41
+ "catch",
42
+ "finally",
43
+ ]);
44
+ // ─── Main build function ──────────────────────────────────────────────────────
45
+ /**
46
+ * Build the client bundle for production using esbuild.
47
+ * Writes output to dist/assets/.
48
+ *
49
+ * @throws Error if the build fails or no client entry is found.
50
+ */
51
+ export async function buildProduction(options) {
52
+ const { paths, minify = true, sourcemap = false, dev = false } = options;
53
+ const devBuild = dev || !minify;
54
+ if (!paths.clientEntry) {
55
+ throw new Error("[mates-fullstack] No client entry found.\n" +
56
+ "Create client/client.ts that calls renderX(App, container).");
57
+ }
58
+ // Ensure output directory exists
59
+ const assetsDir = path.join(paths.outDir, "assets");
60
+ fs.mkdirSync(assetsDir, { recursive: true });
61
+ // Remove stale entry/chunk/css files before each build. Production hashes are
62
+ // content-addressed, but start() reads a manifest; this cleanup keeps the
63
+ // output easy to inspect and prevents old dev assets from being served.
64
+ for (const file of fs.readdirSync(assetsDir)) {
65
+ if (/^(client|chunk)-?.*\.(js|css|map)$/.test(file) ||
66
+ file === "manifest.json") {
67
+ try {
68
+ fs.unlinkSync(path.join(assetsDir, file));
69
+ }
70
+ catch {
71
+ /* ignore */
72
+ }
73
+ }
74
+ }
75
+ // Run esbuild
76
+ const result = await esbuild.build(createClientBuildOptions({ paths, minify, sourcemap, dev }));
77
+ return clientBuildResultFromMetafile(result, paths);
78
+ }
79
+ /**
80
+ * Build the client bundle for development.
81
+ * Same as production but no minification and inline sourcemaps
82
+ * so the browser debugger shows original .ts source.
83
+ */
84
+ export async function buildDev(options) {
85
+ return buildProduction({
86
+ ...options,
87
+ minify: false,
88
+ // External sourcemaps keep client.js small in bundled dev mode. The browser
89
+ // fetches the .map file separately on demand. No tradeoff — same debugging
90
+ // quality, vastly smaller JS file for page load.
91
+ sourcemap: true,
92
+ dev: true,
93
+ });
94
+ }
95
+ /**
96
+ * Create incremental esbuild contexts for the dev server.
97
+ *
98
+ * `esbuild.context().rebuild()` keeps esbuild's parsed module graph warm across
99
+ * file changes. The production path keeps using one-shot builds so CLI build
100
+ * output remains deterministic and contexts never leak after completion.
101
+ */
102
+ export async function createDevBuildContext(paths) {
103
+ let clientCtx = null;
104
+ let serverCtx = null;
105
+ let serverEntriesKey = "";
106
+ async function ensureClientCtx() {
107
+ if (clientCtx)
108
+ return clientCtx;
109
+ if (!paths.clientEntry) {
110
+ throw new Error("[mates-fullstack] No client entry found.\n" +
111
+ "Create client/client.ts that calls renderX(App, container).");
112
+ }
113
+ prepareClientAssetsDir(paths);
114
+ clientCtx = await esbuild.context(createClientBuildOptions({
115
+ paths,
116
+ // External sourcemaps keep client.js small. The .map is a separate file
117
+ // loaded by the browser devtools on demand.
118
+ minify: false,
119
+ sourcemap: true,
120
+ dev: true,
121
+ }));
122
+ return clientCtx;
123
+ }
124
+ async function ensureContext(forceRecreate = false) {
125
+ const entryPoints = collectServerRuntimeEntries(paths);
126
+ const nextKey = JSON.stringify(Object.entries(entryPoints).sort());
127
+ const serverDir = path.join(paths.outDir, "server");
128
+ if (forceRecreate || nextKey !== serverEntriesKey) {
129
+ await serverCtx?.dispose();
130
+ serverCtx = null;
131
+ serverEntriesKey = nextKey;
132
+ fs.rmSync(serverDir, { recursive: true, force: true });
133
+ fs.mkdirSync(serverDir, { recursive: true });
134
+ }
135
+ if (Object.keys(entryPoints).length === 0) {
136
+ return { ctx: null, result: getServerRuntimeResult(paths) };
137
+ }
138
+ if (!serverCtx) {
139
+ fs.mkdirSync(serverDir, { recursive: true });
140
+ serverCtx = await esbuild.context(createServerRuntimeBuildOptions(paths, entryPoints));
141
+ }
142
+ return { ctx: serverCtx, result: getServerRuntimeResult(paths) };
143
+ }
144
+ return {
145
+ async rebuildClient() {
146
+ const ctx = await ensureClientCtx();
147
+ const result = await ctx.rebuild();
148
+ return clientBuildResultFromMetafile(result, paths);
149
+ },
150
+ async rebuildServerRuntime(options = {}) {
151
+ const { ctx } = await ensureContext(options.forceRecreate ?? false);
152
+ if (ctx)
153
+ await ctx.rebuild();
154
+ return getServerRuntimeResult(paths);
155
+ },
156
+ async dispose() {
157
+ await Promise.all([
158
+ clientCtx?.dispose() ?? Promise.resolve(),
159
+ serverCtx?.dispose() ?? Promise.resolve(),
160
+ ]);
161
+ clientCtx = null;
162
+ serverCtx = null;
163
+ },
164
+ };
165
+ }
166
+ // ─── Server runtime build (Model B) ────────────────────────────────────────────
167
+ export async function buildServerRuntime(paths) {
168
+ const serverDir = path.join(paths.outDir, "server");
169
+ fs.rmSync(serverDir, { recursive: true, force: true });
170
+ fs.mkdirSync(serverDir, { recursive: true });
171
+ const entryPoints = collectServerRuntimeEntries(paths);
172
+ if (Object.keys(entryPoints).length > 0) {
173
+ await esbuild.build(createServerRuntimeBuildOptions(paths, entryPoints));
174
+ }
175
+ return getServerRuntimeResult(paths);
176
+ }
177
+ function prepareClientAssetsDir(paths) {
178
+ const assetsDir = path.join(paths.outDir, "assets");
179
+ fs.mkdirSync(assetsDir, { recursive: true });
180
+ for (const file of fs.readdirSync(assetsDir)) {
181
+ if (/^(client|chunk)-?.*\.(js|css|map)$/.test(file) ||
182
+ file === "manifest.json") {
183
+ try {
184
+ fs.unlinkSync(path.join(assetsDir, file));
185
+ }
186
+ catch {
187
+ /* ignore */
188
+ }
189
+ }
190
+ }
191
+ return assetsDir;
192
+ }
193
+ function clientBuildResultFromMetafile(result, paths) {
194
+ const assetsDir = path.join(paths.outDir, "assets");
195
+ if (!paths.clientEntry) {
196
+ throw new Error("[mates-fullstack] No client entry found.");
197
+ }
198
+ const clientFilename = _findEntryOutput(result, paths.clientEntry, assetsDir);
199
+ if (!clientFilename) {
200
+ throw new Error("[mates-fullstack] Client build produced no output file.");
201
+ }
202
+ const clientBundlePath = path.join(assetsDir, clientFilename);
203
+ const cssFilename = _findCssOutput(result, assetsDir);
204
+ fs.writeFileSync(path.join(assetsDir, "manifest.json"), JSON.stringify({ client: clientFilename, css: cssFilename }, null, 2), "utf-8");
205
+ _warnBundleSize(clientBundlePath);
206
+ return { clientBundlePath, clientFilename, cssFilename };
207
+ }
208
+ function createClientBuildOptions(options) {
209
+ const { paths, minify = true, sourcemap = false, dev = false } = options;
210
+ const devBuild = dev || !minify;
211
+ const projectRoot = paths.projectRoot;
212
+ const apiDir = paths.apiDir ? path.resolve(paths.apiDir) : null;
213
+ const serverDir = path.join(projectRoot, "server");
214
+ const matesPath = findMatesPath(projectRoot);
215
+ const defines = getBundlerDefines(devBuild ? "development" : "production");
216
+ const assetsDir = path.join(paths.outDir, "assets");
217
+ if (!paths.clientEntry) {
218
+ throw new Error("[mates-fullstack] No client entry found.\n" +
219
+ "Create client/client.ts that calls renderX(App, container).");
220
+ }
221
+ return {
222
+ entryPoints: [paths.clientEntry],
223
+ outdir: assetsDir,
224
+ entryNames: devBuild ? "client" : "client-[hash]",
225
+ chunkNames: devBuild ? "chunk-[name]" : "chunk-[hash]",
226
+ assetNames: devBuild ? "[name]" : "[name]-[hash]",
227
+ // File-loader assets are emitted next to the JS in dist/assets, but their
228
+ // runtime URL must be absolute from the server root. Without publicPath,
229
+ // esbuild emits "./s-HASH.svg" and <img src> resolves it against the page
230
+ // URL ("/s-HASH.svg") instead of the static asset route.
231
+ publicPath: "/assets",
232
+ format: "esm",
233
+ platform: "browser",
234
+ target: ["es2020", "chrome90", "firefox90", "safari14"],
235
+ bundle: true,
236
+ splitting: !devBuild,
237
+ minify,
238
+ sourcemap,
239
+ metafile: true,
240
+ external: ["node:*", "*.node", `${INTERNAL_MATES_PREFIX}/*`],
241
+ loader: {
242
+ ".png": "file",
243
+ ".jpg": "file",
244
+ ".jpeg": "file",
245
+ ".gif": "file",
246
+ ".webp": "file",
247
+ ".avif": "file",
248
+ ".ico": "file",
249
+ ".bmp": "file",
250
+ ".tiff": "file",
251
+ ".svg": "file",
252
+ ".woff": "file",
253
+ ".woff2": "file",
254
+ ".ttf": "file",
255
+ ".otf": "file",
256
+ ".eot": "file",
257
+ ".mp4": "file",
258
+ ".webm": "file",
259
+ ".ogg": "file",
260
+ ".mov": "file",
261
+ ".avi": "file",
262
+ ".mkv": "file",
263
+ ".mp3": "file",
264
+ ".wav": "file",
265
+ ".flac": "file",
266
+ ".aac": "file",
267
+ ".m4a": "file",
268
+ ".opus": "file",
269
+ ".pdf": "file",
270
+ ".doc": "file",
271
+ ".docx": "file",
272
+ ".xls": "file",
273
+ ".xlsx": "file",
274
+ ".ppt": "file",
275
+ ".pptx": "file",
276
+ ".txt": "file",
277
+ ".csv": "file",
278
+ ".glb": "file",
279
+ ".gltf": "file",
280
+ ".obj": "file",
281
+ ".fbx": "file",
282
+ ".wasm": "file",
283
+ ".json5": "file",
284
+ ".geojson": "file",
285
+ ".toml": "file",
286
+ ".yaml": "file",
287
+ ".yml": "file",
288
+ ".zip": "file",
289
+ ".tar": "file",
290
+ ".gz": "file",
291
+ },
292
+ plugins: [
293
+ _emptyCssImportPlugin(),
294
+ _rpcStubPlugin(apiDir, projectRoot),
295
+ _serverBoundaryPlugin(serverDir, apiDir, projectRoot),
296
+ _matesFullstackBrowserPlugin(projectRoot),
297
+ _dedupePlugin(matesPath, projectRoot),
298
+ ],
299
+ define: defines,
300
+ logLevel: "warning",
301
+ };
302
+ }
303
+ function _emptyCssImportPlugin() {
304
+ const filter = /\.css$/i;
305
+ return {
306
+ name: "mates-empty-css-imports",
307
+ setup(build) {
308
+ build.onLoad({ filter }, () => ({
309
+ contents: "// CSS is bundled into one linked stylesheet by mates-fullstack.\n" +
310
+ "export default {};\n",
311
+ loader: "js",
312
+ }));
313
+ },
314
+ };
315
+ }
316
+ function createServerRuntimeBuildOptions(paths, entryPoints) {
317
+ return {
318
+ entryPoints,
319
+ outdir: path.join(paths.outDir, "server"),
320
+ outbase: paths.projectRoot,
321
+ bundle: true,
322
+ platform: "node",
323
+ format: "esm",
324
+ target: "node20",
325
+ sourcemap: true,
326
+ packages: "external",
327
+ external: [
328
+ "node:*",
329
+ "*.node",
330
+ "mates",
331
+ "lit-html",
332
+ "mates-fullstack",
333
+ `${_getOwnPackageName()}`,
334
+ `${_getOwnPackageName()}/*`,
335
+ ],
336
+ plugins: [_ssrRuntimeRpcStubPlugin(paths.apiDir), _assetStubPlugin()],
337
+ logLevel: "warning",
338
+ };
339
+ }
340
+ function getServerRuntimeResult(paths) {
341
+ const serverDir = path.join(paths.outDir, "server");
342
+ return {
343
+ serverDir,
344
+ appFile: fileIfExists(path.join(serverDir, "App.js")),
345
+ apiDir: dirIfExists(path.join(serverDir, "api")),
346
+ mainFile: fileIfExists(path.join(serverDir, "main.js")),
347
+ socketDir: dirIfExists(path.join(serverDir, "socket")),
348
+ };
349
+ }
350
+ function collectServerRuntimeEntries(paths) {
351
+ const entries = {};
352
+ if (paths.appFile)
353
+ entries.App = paths.appFile;
354
+ if (paths.mainFile)
355
+ entries.main = paths.mainFile;
356
+ addDirEntries(entries, paths.apiDir, "api");
357
+ addDirEntries(entries, paths.socketDir, "socket");
358
+ addDirEntries(entries, paths.sharedDir, "shared");
359
+ return entries;
360
+ }
361
+ function addDirEntries(entries, dir, prefix) {
362
+ if (!dir || !fs.existsSync(dir))
363
+ return;
364
+ for (const file of listRuntimeSourceFiles(dir)) {
365
+ const rel = path.relative(dir, file).replace(/\.(ts|tsx|js|jsx|mjs)$/, "");
366
+ entries[path.posix.join(prefix, rel.split(path.sep).join("/"))] = file;
367
+ }
368
+ }
369
+ function listRuntimeSourceFiles(dir) {
370
+ const files = [];
371
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
372
+ const full = path.join(dir, entry.name);
373
+ if (entry.isDirectory()) {
374
+ files.push(...listRuntimeSourceFiles(full));
375
+ continue;
376
+ }
377
+ if (/\.(ts|tsx|js|jsx|mjs)$/.test(entry.name))
378
+ files.push(full);
379
+ }
380
+ return files;
381
+ }
382
+ function fileIfExists(filePath) {
383
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile()
384
+ ? filePath
385
+ : null;
386
+ }
387
+ function dirIfExists(dirPath) {
388
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()
389
+ ? dirPath
390
+ : null;
391
+ }
392
+ // ─── esbuild plugins ──────────────────────────────────────────────────────────
393
+ /**
394
+ * Ignore stylesheet imports in the Node/SSR server runtime bundle.
395
+ * CSS is handled separately by buildStyles() and linked in the HTML shell.
396
+ */
397
+ function _ssrRuntimeRpcStubPlugin(apiDir) {
398
+ if (!apiDir)
399
+ return { name: "mates-ssr-runtime-rpc-stub", setup() { } };
400
+ const normalApiDir = path.resolve(apiDir);
401
+ const extensions = [
402
+ "",
403
+ ".ts",
404
+ ".js",
405
+ ".tsx",
406
+ ".jsx",
407
+ ".mjs",
408
+ "/index.ts",
409
+ "/index.js",
410
+ "/index.mjs",
411
+ ];
412
+ function resolveImport(importer, specifier) {
413
+ if (!specifier.startsWith("."))
414
+ return null;
415
+ const base = path.resolve(path.dirname(importer), specifier);
416
+ for (const ext of extensions) {
417
+ const candidate = base + ext;
418
+ try {
419
+ if (fs.statSync(candidate).isFile())
420
+ return path.resolve(candidate);
421
+ }
422
+ catch {
423
+ /* try next */
424
+ }
425
+ }
426
+ return null;
427
+ }
428
+ return {
429
+ name: "mates-ssr-runtime-rpc-stub",
430
+ setup(build) {
431
+ build.onResolve({ filter: /^\.\.?\// }, (args) => {
432
+ if (!args.importer)
433
+ return undefined;
434
+ const resolved = resolveImport(args.importer, args.path);
435
+ if (!resolved)
436
+ return undefined;
437
+ if (resolved !== normalApiDir &&
438
+ !resolved.startsWith(normalApiDir + path.sep)) {
439
+ return undefined;
440
+ }
441
+ const importer = path.resolve(args.importer);
442
+ if (importer.startsWith(normalApiDir + path.sep))
443
+ return undefined;
444
+ return { path: resolved, namespace: "mates-ssr-rpc-stub" };
445
+ });
446
+ build.onLoad({ filter: /.*/, namespace: "mates-ssr-rpc-stub" }, (args) => {
447
+ const source = fs.readFileSync(args.path, "utf-8");
448
+ const names = _extractExportNames(source).filter((n) => !RESERVED_NAMES.has(n));
449
+ const lines = [
450
+ `import { lookupRpcFn } from ${JSON.stringify(`${_getOwnPackageName()}/internal`)};`,
451
+ `import { getSSRContext } from ${JSON.stringify(`${_getOwnPackageName()}/internal`)};`,
452
+ "",
453
+ ];
454
+ for (const name of names) {
455
+ const url = _deriveRuntimeRpcUrl(normalApiDir, args.path, name);
456
+ lines.push(`export async function ${name}(payload = {}) {`, ` const entry = lookupRpcFn(${JSON.stringify(url)});`, ` if (!entry) throw new Error(${JSON.stringify(`[mates-fullstack] SSR RPC function not found: ${url}`)});`, ` const ssrCtx = getSSRContext();`, ` const auth = ssrCtx?.auth ?? null;`, ` return entry.fn(payload, { auth });`, `}`, "");
457
+ }
458
+ return { contents: lines.join("\n"), loader: "js" };
459
+ });
460
+ },
461
+ };
462
+ }
463
+ function _deriveRuntimeRpcUrl(apiDir, filePath, fnName) {
464
+ const rel = path.relative(apiDir, filePath);
465
+ const withoutExt = rel.replace(/\.(ts|js|tsx|jsx|mjs)$/, "");
466
+ let modulePath = withoutExt.split(path.sep).join("/");
467
+ if (modulePath.endsWith("/index"))
468
+ modulePath = modulePath.slice(0, -6);
469
+ else if (modulePath === "index")
470
+ modulePath = "";
471
+ return modulePath ? `/api/${modulePath}/${fnName}` : `/api/${fnName}`;
472
+ }
473
+ function _assetStubPlugin() {
474
+ return {
475
+ name: "mates-asset-stub",
476
+ setup(build) {
477
+ build.onLoad({
478
+ filter: /\.(css|svg|png|jpe?g|gif|webp|avif|ico|bmp|tiff|woff2?|ttf|otf|eot|mp4|webm|ogg|mov|avi|mkv|mp3|wav|flac|aac|m4a|opus|pdf|docx?|xlsx?|pptx?|txt|csv|glb|gltf|obj|fbx|wasm|json5|geojson|toml|ya?ml|zip|tar|gz)$/i,
479
+ }, () => ({
480
+ contents: "export default {};",
481
+ loader: "js",
482
+ }));
483
+ },
484
+ };
485
+ }
486
+ /**
487
+ * mates-fullstack browser plugin.
488
+ *
489
+ * Replaces any import of "mates-fullstack" from client code with the
490
+ * browser-safe subset (errors, redirect helpers, etc.).
491
+ * This prevents Node.js-specific code (node:fs, node:http, etc.) from
492
+ * entering the browser bundle.
493
+ */
494
+ function _getOwnPackageName() {
495
+ const thisDir = path.dirname(new URL(import.meta.url).pathname);
496
+ try {
497
+ const pkgJson = path.resolve(thisDir, "..", "package.json");
498
+ if (fs.existsSync(pkgJson)) {
499
+ const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
500
+ if (pkg.name)
501
+ return pkg.name;
502
+ }
503
+ }
504
+ catch {
505
+ /* use default */
506
+ }
507
+ return "mates-fullstack";
508
+ }
509
+ function _matesFullstackBrowserPlugin(_projectRoot) {
510
+ // Resolve the browser.ts path relative to this file (build-prod.ts)
511
+ const thisDir = path.dirname(new URL(import.meta.url).pathname);
512
+ const browserFile = path.resolve(thisDir, "browser.js");
513
+ const browserFileDev = path.resolve(thisDir, "browser.ts");
514
+ const browserEntry = fs.existsSync(browserFile)
515
+ ? browserFile
516
+ : browserFileDev;
517
+ // Detect own package name from package.json so the filter works
518
+ // whether installed as "mates-fullstack", etc.
519
+ const ownName = _getOwnPackageName();
520
+ // Escape for use in regex
521
+ const escaped = ownName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
522
+ const filter = new RegExp(`^${escaped}$`);
523
+ return {
524
+ name: "mates-fullstack-browser",
525
+ setup(build) {
526
+ build.onResolve({ filter }, () => ({
527
+ path: browserEntry,
528
+ }));
529
+ },
530
+ };
531
+ }
532
+ /**
533
+ * RPC stub plugin.
534
+ *
535
+ * Intercepts every import of a file inside server/api/.
536
+ * Replaces the ENTIRE file with a lightweight stub module that
537
+ * calls __rpcCall() — the real server code never enters the bundle.
538
+ *
539
+ * server/api/users.ts → import { __rpcCall } from "/_mates/rpc-client.js";
540
+ * export async function getUsers(payload = {}) {
541
+ * return __rpcCall("/api/users/getUsers", payload);
542
+ * }
543
+ */
544
+ function _rpcStubPlugin(apiDir, projectRoot) {
545
+ if (!apiDir)
546
+ return { name: "mates-rpc-stub", setup() { } };
547
+ // Build a regex that matches absolute paths inside apiDir only.
548
+ // Escape path separators for regex safety.
549
+ const escaped = apiDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
550
+ const filter = new RegExp(`^${escaped}[/\\\\].+\\.(ts|js|tsx|jsx)$`);
551
+ return {
552
+ name: "mates-rpc-stub",
553
+ setup(build) {
554
+ build.onLoad({ filter }, (args) => {
555
+ let source;
556
+ try {
557
+ source = fs.readFileSync(args.path, "utf-8");
558
+ }
559
+ catch {
560
+ return undefined; // let esbuild handle the error
561
+ }
562
+ const stub = _generateRpcStub(source, args.path, apiDir, `${_getOwnPackageName()}/client`);
563
+ return {
564
+ contents: stub,
565
+ loader: "js",
566
+ };
567
+ });
568
+ },
569
+ };
570
+ }
571
+ /**
572
+ * Server boundary plugin.
573
+ *
574
+ * Client code may import server/api/** only; those imports are replaced by RPC
575
+ * stubs by _rpcStubPlugin. Any other server/** import is a build error.
576
+ */
577
+ function _serverBoundaryPlugin(serverDir, apiDir, projectRoot) {
578
+ if (!fs.existsSync(serverDir)) {
579
+ return { name: "mates-server-boundary", setup() { } };
580
+ }
581
+ const extensions = [
582
+ "",
583
+ ".ts",
584
+ ".js",
585
+ ".tsx",
586
+ ".jsx",
587
+ "/index.ts",
588
+ "/index.js",
589
+ ];
590
+ function resolveImport(importer, specifier) {
591
+ if (!specifier.startsWith("."))
592
+ return null;
593
+ const base = path.resolve(path.dirname(importer), specifier);
594
+ for (const ext of extensions) {
595
+ const candidate = base + ext;
596
+ try {
597
+ if (fs.statSync(candidate).isFile())
598
+ return path.resolve(candidate);
599
+ }
600
+ catch {
601
+ /* try next */
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ return {
607
+ name: "mates-server-boundary",
608
+ setup(build) {
609
+ build.onResolve({ filter: /.*/ }, (args) => {
610
+ if (!args.importer)
611
+ return undefined;
612
+ const resolved = resolveImport(args.importer, args.path);
613
+ if (!resolved)
614
+ return undefined;
615
+ if (!resolved.startsWith(serverDir + path.sep))
616
+ return undefined;
617
+ if (apiDir &&
618
+ (resolved.startsWith(apiDir + path.sep) || resolved === apiDir)) {
619
+ return undefined;
620
+ }
621
+ return {
622
+ errors: [
623
+ {
624
+ text: `[mates-fullstack] Import blocked: ` +
625
+ `"${path.relative(projectRoot, args.importer)}" cannot import ` +
626
+ `"${path.relative(projectRoot, resolved)}".\n` +
627
+ `Only server/api/** may be imported by client code; it is converted to RPC stubs. ` +
628
+ `All other server/** files are server-only.`,
629
+ },
630
+ ],
631
+ };
632
+ });
633
+ },
634
+ };
635
+ }
636
+ /**
637
+ * Dedupe plugin.
638
+ *
639
+ * Forces all imports of "mates" and "lit-html" to resolve to the same
640
+ * single physical file. Without this, if the project has multiple copies
641
+ * of these packages (e.g. in a monorepo), esbuild may bundle two separate
642
+ * instances with independent module-level state — breaking the renderer.
643
+ */
644
+ function _dedupePlugin(matesPath, projectRoot) {
645
+ const litHtmlDir = path.join(projectRoot, "node_modules", "lit-html");
646
+ const litHtmlMain = path.join(litHtmlDir, "lit-html.js");
647
+ return {
648
+ name: "mates-dedupe",
649
+ setup(build) {
650
+ // Dedupe "mates"
651
+ if (matesPath) {
652
+ build.onResolve({ filter: /^mates$/ }, () => ({
653
+ path: matesPath,
654
+ }));
655
+ }
656
+ // Dedupe "lit-html" bare import
657
+ if (fs.existsSync(litHtmlMain)) {
658
+ build.onResolve({ filter: /^lit-html$/ }, () => ({
659
+ path: litHtmlMain,
660
+ }));
661
+ }
662
+ // Dedupe "lit-html/directives/*" and other subpaths
663
+ if (fs.existsSync(litHtmlDir)) {
664
+ build.onResolve({ filter: /^lit-html\// }, (args) => {
665
+ const subpath = args.path.slice("lit-html/".length);
666
+ const resolved = path.join(litHtmlDir, subpath);
667
+ if (fs.existsSync(resolved))
668
+ return { path: resolved };
669
+ return undefined;
670
+ });
671
+ }
672
+ },
673
+ };
674
+ }
675
+ // ─── RPC stub generation ──────────────────────────────────────────────────────
676
+ /**
677
+ * Generate an RPC stub module for a server/api/ file.
678
+ * Scans the source for exported async function names (via regex),
679
+ * filters reserved names, and returns a browser-safe stub.
680
+ */
681
+ export function generateRpcStub(source, filePath, apiDir) {
682
+ return _generateRpcStub(source, filePath, apiDir, `${_getOwnPackageName()}/client`);
683
+ }
684
+ function _generateRpcStub(source, filePath, apiDir, clientImport) {
685
+ const names = _extractExportNames(source);
686
+ const filtered = names.filter((n) => !RESERVED_NAMES.has(n));
687
+ if (filtered.length === 0) {
688
+ return "// No RPC exports found\n";
689
+ }
690
+ // Derive module path: users.ts → users, posts/index.ts → posts
691
+ const rel = path.relative(apiDir, filePath);
692
+ const modulePath = rel
693
+ .replace(/\.(ts|js|tsx|jsx)$/, "")
694
+ .split(path.sep)
695
+ .join("/")
696
+ .replace(/\/index$/, "");
697
+ const stubs = filtered
698
+ .map((name) => {
699
+ // Avoid double-slash when modulePath is empty (server/api/index.ts)
700
+ const url = modulePath ? `/api/${modulePath}/${name}` : `/api/${name}`;
701
+ return (`export async function ${name}(payload) {\n` +
702
+ ` return __rpcCall(${JSON.stringify(url)}, payload);\n` +
703
+ `}`);
704
+ })
705
+ .join("\n\n");
706
+ return `import { __rpcCall } from ${JSON.stringify(clientImport)};\n\n${stubs}\n`;
707
+ }
708
+ /**
709
+ * Extract exported async function names from TypeScript source.
710
+ * Regex-based — covers all common patterns without a full AST parse.
711
+ */
712
+ export function extractExportNames(source) {
713
+ return _extractExportNames(source);
714
+ }
715
+ function _extractExportNames(source) {
716
+ const names = [];
717
+ const seen = new Set();
718
+ const add = (n) => {
719
+ if (!seen.has(n)) {
720
+ seen.add(n);
721
+ names.push(n);
722
+ }
723
+ };
724
+ let m;
725
+ // export async function NAME(
726
+ const fnPat = /^export\s+async\s+function\s+(\w+)\s*\(/gm;
727
+ while ((m = fnPat.exec(source)) !== null)
728
+ add(m[1]);
729
+ // export const NAME = async (
730
+ // export const NAME = async function(
731
+ const constPat = /^export\s+const\s+(\w+)\s*=\s*async\s*(?:function\s*)?\(/gm;
732
+ while ((m = constPat.exec(source)) !== null)
733
+ add(m[1]);
734
+ return names;
735
+ }
736
+ let _cssDevState = null;
737
+ /** Clear incremental CSS state — call before a full rebuild / cleanDist. */
738
+ export function clearCssDevState() {
739
+ _cssDevState = null;
740
+ }
741
+ /**
742
+ * Patch a single CSS file in the dev bundle without running a full esbuild
743
+ * build pass.
744
+ *
745
+ * Returns the output filename on success, or null when the changed path is
746
+ * not tracked (e.g. a new file was added) — the caller should fall back to
747
+ * a full buildStyles().
748
+ */
749
+ export async function patchCssFile(changedAbsPath) {
750
+ const state = _cssDevState;
751
+ if (!state)
752
+ return null;
753
+ const abs = path.resolve(changedAbsPath);
754
+ if (!state.cache.has(abs))
755
+ return null; // file not in bundle → need full rebuild
756
+ try {
757
+ const raw = fs.readFileSync(abs, "utf-8");
758
+ const result = await esbuild.transform(raw, {
759
+ loader: "css",
760
+ minify: true,
761
+ sourcemap: false,
762
+ });
763
+ state.cache.set(abs, result.code);
764
+ }
765
+ catch {
766
+ // Transform error — leave old cached content so the page keeps working.
767
+ // The browser will show DevTools errors when it tries to apply the styles.
768
+ }
769
+ const bundled = state.fileOrder
770
+ .map((f) => state.cache.get(f) ?? "")
771
+ .join("\n");
772
+ fs.writeFileSync(state.outPath, bundled, "utf-8");
773
+ return state.outFilename;
774
+ }
775
+ /**
776
+ * Bundle all CSS files found in the project into a single stylesheet.
777
+ *
778
+ * Dev: dist/assets/styles.css (stable URL for HMR)
779
+ * Prod: dist/assets/styles-[hash].css (content-hashed)
780
+ *
781
+ * Returns the output CSS basename, or null if no style files found.
782
+ */
783
+ export async function buildStyles(projectRoot, options = {}) {
784
+ const { minify = false, outDir = path.join(projectRoot, "dist", "assets"), hash = minify, } = options;
785
+ fs.mkdirSync(outDir, { recursive: true });
786
+ // Remove stale styles files before writing. This also handles the case where
787
+ // the last CSS file was deleted during dev HMR.
788
+ for (const f of fs.readdirSync(outDir)) {
789
+ if (/^styles.*\.css$/.test(f) || /^styles.*\.css\.map$/.test(f)) {
790
+ try {
791
+ fs.unlinkSync(path.join(outDir, f));
792
+ }
793
+ catch {
794
+ /* ignore */
795
+ }
796
+ }
797
+ }
798
+ // Collect styles from the whole project, not only client/.
799
+ // This supports patterns like importing ../public/style.css from client code
800
+ // while still emitting a single CSS bundle linked in the HTML shell.
801
+ const styleFiles = _collectStyleFiles(projectRoot);
802
+ if (styleFiles.length === 0)
803
+ return null;
804
+ // Synthesize an entry that @imports every style file in source order.
805
+ const entryContents = styleFiles
806
+ .map((f) => `@import ${JSON.stringify(f)};`)
807
+ .join("\n");
808
+ const tempDir = path.join(projectRoot, ".mates-dev");
809
+ fs.mkdirSync(tempDir, { recursive: true });
810
+ const tempEntry = path.join(tempDir, "_styles-entry.css");
811
+ fs.writeFileSync(tempEntry, entryContents, "utf-8");
812
+ let result;
813
+ try {
814
+ result = (await esbuild.build({
815
+ entryPoints: [tempEntry],
816
+ outdir: outDir,
817
+ entryNames: hash ? "styles-[hash]" : "styles",
818
+ bundle: true,
819
+ minify,
820
+ sourcemap: false,
821
+ metafile: true,
822
+ logLevel: "warning",
823
+ publicPath: "/assets",
824
+ assetNames: hash ? "[name]-[hash]" : "[name]",
825
+ loader: {
826
+ ".css": "css",
827
+ ".png": "file",
828
+ ".jpg": "file",
829
+ ".jpeg": "file",
830
+ ".gif": "file",
831
+ ".webp": "file",
832
+ ".avif": "file",
833
+ ".ico": "file",
834
+ ".bmp": "file",
835
+ ".tiff": "file",
836
+ ".svg": "file",
837
+ ".woff": "file",
838
+ ".woff2": "file",
839
+ ".ttf": "file",
840
+ ".otf": "file",
841
+ ".eot": "file",
842
+ },
843
+ }));
844
+ }
845
+ finally {
846
+ try {
847
+ fs.unlinkSync(tempEntry);
848
+ }
849
+ catch {
850
+ /* ignore */
851
+ }
852
+ }
853
+ let outFilename = null;
854
+ for (const outFile of Object.keys(result.metafile.outputs)) {
855
+ if (outFile.endsWith(".css")) {
856
+ outFilename = path.basename(outFile);
857
+ break;
858
+ }
859
+ }
860
+ // Build per-file incremental cache for dev HMR (stable output name only).
861
+ // Each file is individually transformed so patchCssFile() can swap one entry
862
+ // and rebuild the bundle by concatenation — no full esbuild pass needed.
863
+ if (!hash && outFilename) {
864
+ const cache = new Map();
865
+ await Promise.all(styleFiles.map(async (f) => {
866
+ try {
867
+ const raw = fs.readFileSync(f, "utf-8");
868
+ const r = await esbuild.transform(raw, {
869
+ loader: "css",
870
+ minify: true,
871
+ sourcemap: false,
872
+ });
873
+ cache.set(f, r.code);
874
+ }
875
+ catch {
876
+ cache.set(f, fs.readFileSync(f, "utf-8"));
877
+ }
878
+ }));
879
+ _cssDevState = {
880
+ fileOrder: styleFiles,
881
+ cache,
882
+ outPath: path.join(outDir, outFilename),
883
+ outFilename,
884
+ };
885
+ }
886
+ else {
887
+ // Production build — clear any stale dev state.
888
+ _cssDevState = null;
889
+ }
890
+ return outFilename;
891
+ }
892
+ function _collectStyleFiles(dir) {
893
+ const results = [];
894
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
895
+ const full = path.join(dir, entry.name);
896
+ if (entry.isDirectory()) {
897
+ if (["node_modules", "dist", ".mates-dev", ".git", "coverage"].includes(entry.name)) {
898
+ continue;
899
+ }
900
+ results.push(..._collectStyleFiles(full));
901
+ }
902
+ else if (/\.css$/i.test(entry.name)) {
903
+ results.push(full);
904
+ }
905
+ }
906
+ return results.sort();
907
+ }
908
+ // ─── Public dir copying ───────────────────────────────────────────────────────
909
+ /**
910
+ * Recursively copy public/ to outDir.
911
+ * Used by the production build and SSG commands.
912
+ */
913
+ export function copyPublicDir(publicDir, outDir) {
914
+ if (!fs.existsSync(publicDir))
915
+ return;
916
+ fs.mkdirSync(outDir, { recursive: true });
917
+ _copyDirSync(publicDir, outDir);
918
+ }
919
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
920
+ /**
921
+ * Find the extracted CSS output filename from an esbuild metafile.
922
+ * Returns just the basename, e.g. "client-abc12345.css", or null if no CSS.
923
+ *
924
+ * esbuild automatically extracts CSS imports into a separate .css file
925
+ * when `loader: { ".css": "css" }` is set and CSS files are imported.
926
+ */
927
+ function _findCssOutput(result, assetsDir) {
928
+ for (const outFile of Object.keys(result.metafile.outputs)) {
929
+ if (!outFile.endsWith(".css"))
930
+ continue;
931
+ // Only return the entry-level CSS (not chunk CSS)
932
+ return path.basename(outFile);
933
+ }
934
+ return null;
935
+ }
936
+ /**
937
+ * Find the entry output filename from an esbuild metafile.
938
+ * Returns just the basename, e.g. "client-abc12345.js".
939
+ */
940
+ function _findEntryOutput(result, entryPoint, assetsDir) {
941
+ const normalEntry = path.resolve(entryPoint);
942
+ for (const [outFile, meta] of Object.entries(result.metafile.outputs)) {
943
+ if (!meta.entryPoint)
944
+ continue;
945
+ const normalMeta = path.resolve(meta.entryPoint);
946
+ if (normalMeta === normalEntry) {
947
+ return path.basename(outFile);
948
+ }
949
+ }
950
+ // Fallback: find the largest .js output (likely the entry)
951
+ let largest = 0;
952
+ let largestFile = null;
953
+ for (const outFile of Object.keys(result.metafile.outputs)) {
954
+ if (!outFile.endsWith(".js"))
955
+ continue;
956
+ const fullPath = path.isAbsolute(outFile)
957
+ ? outFile
958
+ : path.join(assetsDir, path.basename(outFile));
959
+ try {
960
+ const size = fs.statSync(fullPath).size;
961
+ if (size > largest) {
962
+ largest = size;
963
+ largestFile = path.basename(outFile);
964
+ }
965
+ }
966
+ catch {
967
+ /* ignore */
968
+ }
969
+ }
970
+ return largestFile;
971
+ }
972
+ function _warnBundleSize(filePath) {
973
+ try {
974
+ const stat = fs.statSync(filePath);
975
+ const kb = Math.round(stat.size / 1024);
976
+ if (kb > 500) {
977
+ devWarn(`Client bundle is ${kb}KB. ` +
978
+ "Consider code splitting with dynamic imports.");
979
+ }
980
+ }
981
+ catch {
982
+ /* ignore */
983
+ }
984
+ }
985
+ function _copyDirSync(src, dest) {
986
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
987
+ const srcPath = path.join(src, entry.name);
988
+ const destPath = path.join(dest, entry.name);
989
+ if (entry.isDirectory()) {
990
+ fs.mkdirSync(destPath, { recursive: true });
991
+ _copyDirSync(srcPath, destPath);
992
+ }
993
+ else {
994
+ fs.copyFileSync(srcPath, destPath);
995
+ }
996
+ }
997
+ }
998
+ /**
999
+ * Content hash for cache-busting fallback (SHA-256, first 8 chars).
1000
+ */
1001
+ export function contentHash(filePath) {
1002
+ try {
1003
+ const content = fs.readFileSync(filePath, "utf-8");
1004
+ return crypto
1005
+ .createHash("sha256")
1006
+ .update(content)
1007
+ .digest("hex")
1008
+ .slice(0, 8);
1009
+ }
1010
+ catch {
1011
+ return Date.now().toString(36);
1012
+ }
1013
+ }
1014
+ //# sourceMappingURL=build-prod.js.map