wawesome 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -231,10 +231,32 @@ Add `"assets"` to deploy static files beside your code:
231
231
  }
232
232
  ```
233
233
 
234
- Everything under that directory is deployed with the version, addressed by its path from the
235
- directory root. The CLI hashes each file and asks the platform which of them it does not already
236
- hold, so a redeploy that changed one chunk uploads one chunk and a deploy that changed nothing at
237
- all is refused before a byte moves. Assets are not served yet.
234
+ Everything under that directory is deployed with the version and served at its path beneath your
235
+ Function's URL `dist/client/assets/index-a1.js` answers at `https://<app-host>/<function>/assets/index-a1.js`.
236
+ The CLI hashes each file and asks the platform which of them it does not already hold, so a redeploy
237
+ that changed one chunk uploads one chunk — and a deploy that changed nothing at all is refused
238
+ before a byte moves.
239
+
240
+ Files are served straight from object storage; your Function is never invoked for one, and no
241
+ invocation is recorded. They answer on your App's own hostname and nowhere else — on the
242
+ development path form (`/x/<tenant>/<app>/<function>/...`) the same address reaches your handler
243
+ as it always has, because a file on an origin every workspace shares would be same-origin with
244
+ all of them. Each carries `Cache-Control: public, max-age=31536000, immutable` and an
245
+ `ETag`, so name your build output by content hash — a file's bytes must never change under a name a
246
+ browser has already cached for a year. The content type comes from the extension against a fixed
247
+ allowlist and is never sniffed; anything off it is served as a download.
248
+
249
+ Two rules to know about:
250
+
251
+ - **Everything beneath `assets/` is static**, whatever the deploy carries. A request there never
252
+ reaches your handler — an unknown path under it is a 404, not a route for you to answer.
253
+ - **At most 100 files may sit outside `assets/`.** Those paths travel on the version record so a
254
+ request can be routed without a lookup per file. Put bulk output under `assets/`, where a file
255
+ costs nothing; `favicon.ico`, `robots.txt` and a `.well-known/` directory are what the rest is
256
+ for.
257
+
258
+ **HTML is refused at deploy time.** Your Function renders its own markup, and a document served from
259
+ your App's own origin is the sharpest same-origin vector a static file has.
238
260
 
239
261
  ### Reserved headers
240
262
 
package/dist/index.mjs CHANGED
@@ -5,12 +5,12 @@ import path from "node:path";
5
5
  import { build } from "esbuild";
6
6
  import os from "node:os";
7
7
  import { parse } from "acorn";
8
+ import { spawnSync } from "node:child_process";
8
9
  import http from "node:http";
9
10
  import readline from "node:readline";
10
11
  import { Readable } from "node:stream";
11
12
  import crypto from "node:crypto";
12
13
  import { confirm, select } from "@inquirer/prompts";
13
- import { spawnSync } from "node:child_process";
14
14
  import zlib from "node:zlib";
15
15
  //#region src/config.ts
16
16
  /**
@@ -503,8 +503,6 @@ function collectBindings(node, bound, boundNodes, scope, functionValues, pending
503
503
  case "AssignmentExpression":
504
504
  bindAssignmentTarget(node.left, bound);
505
505
  bindBody(node.left, node.right, scope, functionValues, pending);
506
- break;
507
- default: break;
508
506
  }
509
507
  }
510
508
  function bindBody(target, value, scope, functionValues, pending) {
@@ -608,10 +606,7 @@ function bindPattern(node, bound, boundNodes) {
608
606
  case "RestElement":
609
607
  bindPattern(node.argument, bound, boundNodes);
610
608
  break;
611
- case "AssignmentPattern":
612
- bindPattern(node.left, bound, boundNodes);
613
- break;
614
- default: break;
609
+ case "AssignmentPattern": bindPattern(node.left, bound, boundNodes);
615
610
  }
616
611
  }
617
612
  function bindAssignmentTarget(target, bound) {
@@ -639,6 +634,49 @@ function isNode(value) {
639
634
  return typeof value === "object" && value !== null && typeof value.type === "string";
640
635
  }
641
636
  //#endregion
637
+ //#region src/project-build.ts
638
+ /**
639
+ * Run the command a project declares as the thing that produces its entry point.
640
+ *
641
+ * See **Produced entry point** in `CONTEXT.md` for why the PATH and `NODE_ENV`
642
+ * are what they are.
643
+ */
644
+ function runProjectBuild(command, projectDir, verbose = false) {
645
+ console.log(`[wawesome] Running the project's build (${command})...`);
646
+ const result = spawnSync(command, {
647
+ cwd: projectDir,
648
+ shell: true,
649
+ stdio: verbose ? "inherit" : [
650
+ "ignore",
651
+ "pipe",
652
+ "pipe"
653
+ ],
654
+ env: {
655
+ ...process.env,
656
+ NODE_ENV: "production",
657
+ PATH: withBinDirectories(projectDir, process.env.PATH ?? "")
658
+ }
659
+ });
660
+ if (result.status === 0) return;
661
+ console.error(`[wawesome] Error: the project's build failed (${command}).`);
662
+ for (const stream of [result.stdout, result.stderr]) {
663
+ const text = stream?.toString().trim();
664
+ if (text) console.error(text);
665
+ }
666
+ process.exit(1);
667
+ }
668
+ function withBinDirectories(projectDir, existing) {
669
+ const dirs = [];
670
+ let current = path.resolve(projectDir);
671
+ for (;;) {
672
+ dirs.push(path.join(current, "node_modules", ".bin"));
673
+ const parent = path.dirname(current);
674
+ if (parent === current) break;
675
+ current = parent;
676
+ }
677
+ return [...dirs, existing].join(path.delimiter);
678
+ }
679
+ //#endregion
642
680
  //#region src/build.ts
643
681
  /**
644
682
  * Bundles user TS/JS entry point into a single optimized ESM JavaScript file using esbuild.
@@ -649,6 +687,7 @@ async function buildJs(entryInput, options) {
649
687
  const entry = entryInput || config?.entry || "src/index.ts";
650
688
  const outPath = path.resolve(options.out);
651
689
  const isVerbose = Boolean(options.verbose);
690
+ if (config?.build) runProjectBuild(config.build, process.cwd(), isVerbose);
652
691
  if (!fs.existsSync(entry)) {
653
692
  console.error(`[wawesome] Error: Entry file '${entry}' not found.`);
654
693
  process.exit(1);
@@ -720,7 +759,7 @@ async function buildJs(entryInput, options) {
720
759
  * that has to name this version — `--version`, the dependency a scaffolded
721
760
  * project pins — reads it here, so a release bumps one file.
722
761
  */
723
- const CLI_VERSION = "0.3.0";
762
+ const CLI_VERSION = "0.4.0";
724
763
  //#endregion
725
764
  //#region src/prompt.ts
726
765
  /**
@@ -1192,11 +1231,10 @@ const MONTHS = [
1192
1231
  ];
1193
1232
  /** Indented to sit inside the receipt the deploy already prints. */
1194
1233
  function headroomLines(usage) {
1195
- const lines = [
1196
- ` Plan: ${usage.plan.name} this deploy does not change your bill.`,
1197
- ` Apps: ${usage.occupied_app_slots} / ${usage.plan.limits.app_slots} slots`,
1198
- ` Usage: ${periodLabel(usage.period.start, usage.period.end)}`
1199
- ];
1234
+ const lines = [` Plan: ${usage.plan.name} — this deploy does not change your bill.`, ` Apps: ${usage.occupied_app_slots} / ${usage.plan.limits.app_slots} slots`];
1235
+ const stored = storedLine(usage);
1236
+ if (stored !== null) lines.push(stored);
1237
+ lines.push(` Usage: ${periodLabel(usage.period.start, usage.period.end)}`);
1200
1238
  const entries = Object.entries(usage.allowances);
1201
1239
  const labelWidth = widest(entries.map(([key]) => allowanceLabel(key)));
1202
1240
  const amountWidth = widest(entries.map(([key, allowance]) => amount(key, allowance.used, allowance.limit)));
@@ -1209,6 +1247,17 @@ function headroomLines(usage) {
1209
1247
  if (refused !== null) lines.push(` Refused: ${formatCount(refused)} request${refused === 1 ? "" : "s"} at your share`);
1210
1248
  return lines;
1211
1249
  }
1250
+ /**
1251
+ * What the workspace holds against what its plan grants. Beside the App slots
1252
+ * rather than among the allowances below, because neither is spent over the
1253
+ * period those are read against.
1254
+ */
1255
+ function storedLine(usage) {
1256
+ const occupied = usage.occupied_stored_bytes;
1257
+ const granted = usage.plan.limits.stored_bytes;
1258
+ if (typeof occupied !== "number" || typeof granted !== "number") return null;
1259
+ return ` Storage: ${formatBytes(occupied)} / ${formatBytes(granted)}`;
1260
+ }
1212
1261
  function refusedAtShare(usage) {
1213
1262
  const refused = usage.refusals?.at_granted_share;
1214
1263
  if (typeof refused !== "number" || !Number.isFinite(refused) || refused <= 0) return null;
@@ -1265,6 +1314,21 @@ function widest(values) {
1265
1314
  return values.reduce((longest, value) => Math.max(longest, value.length), 0);
1266
1315
  }
1267
1316
  //#endregion
1317
+ //#region src/billing.ts
1318
+ function billingPageUrl() {
1319
+ const base = getDashboardUrl().replace(/\/+$/, "");
1320
+ try {
1321
+ return new URL("billing", `${base}/`).toString();
1322
+ } catch {
1323
+ return `${base}/billing`;
1324
+ }
1325
+ }
1326
+ /** The rule itself is the gateway's prose, and is deliberately not restated here. */
1327
+ function planLimitAdvice(reason) {
1328
+ if (reason !== "app-slots-exhausted" && reason !== "storage-exhausted") return "";
1329
+ return `Where to resolve it: ${billingPageUrl()}`;
1330
+ }
1331
+ //#endregion
1268
1332
  //#region src/assets.ts
1269
1333
  /**
1270
1334
  * The files under `dir`, hashed.
@@ -1616,6 +1680,8 @@ async function uploadAssets(creds, app, funcName, bundle, assets, isVerbose) {
1616
1680
  const errorBody = await res.text();
1617
1681
  const refusal = rejectionOf(errorBody, res.status, `Upload of '${asset.path}' failed (HTTP ${res.status}).`);
1618
1682
  console.error(`[wawesome] Error: ${refusal.message}`);
1683
+ const advice = planLimitAdvice(refusal.reason);
1684
+ if (advice) console.error(`[wawesome] ${advice}`);
1619
1685
  if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
1620
1686
  process.exit(1);
1621
1687
  }
@@ -1623,20 +1689,6 @@ async function uploadAssets(creds, app, funcName, bundle, assets, isVerbose) {
1623
1689
  console.log(`[wawesome] ✅ ${toUpload.length} asset(s) uploaded.`);
1624
1690
  }
1625
1691
  //#endregion
1626
- //#region src/billing.ts
1627
- function billingPageUrl() {
1628
- const base = getDashboardUrl().replace(/\/+$/, "");
1629
- try {
1630
- return new URL("billing", `${base}/`).toString();
1631
- } catch {
1632
- return `${base}/billing`;
1633
- }
1634
- }
1635
- function appSlotAdvice(reason) {
1636
- if (reason !== "app-slots-exhausted") return "";
1637
- return `Where to resolve it: ${billingPageUrl()}`;
1638
- }
1639
- //#endregion
1640
1692
  //#region src/env.ts
1641
1693
  const STANDARD_SECRET_MESSAGES = [
1642
1694
  "Encrypted at rest using AES-256",
@@ -2559,7 +2611,7 @@ async function wireUp(creds, appSlug, manifest, answers) {
2559
2611
  try {
2560
2612
  await ensureApp(creds, appSlug);
2561
2613
  } catch (err) {
2562
- fail(errorText(err), ...[appSlotAdvice(reasonOf(err)), scaffolded].filter(Boolean));
2614
+ fail(errorText(err), ...[planLimitAdvice(reasonOf(err)), scaffolded].filter(Boolean));
2563
2615
  }
2564
2616
  for (const { declared, value } of answers) {
2565
2617
  if (!value) {
@@ -0,0 +1,23 @@
1
+ import { Plugin } from "vite";
2
+ //#region src/vite.d.ts
3
+ interface WawesomeReactOptions {
4
+ /** The module the browser hydrates from. */
5
+ client?: string;
6
+ /** The module exporting the `fetch` handler the platform invokes. */
7
+ server?: string;
8
+ /** The HTML document the shell is cut from. */
9
+ html?: string;
10
+ }
11
+ /**
12
+ * Builds a React application into the two halves the platform deploys: a client
13
+ * bundle uploaded as **Static assets**, and a server bundle that renders the
14
+ * document and streams it.
15
+ *
16
+ * The document's own HTML is inlined into the server bundle rather than uploaded
17
+ * with the rest: served statically at the mount root it would shadow the SSR
18
+ * route and answer with an empty shell that fails to hydrate. Every URL it
19
+ * carries is a **Request-time base** away from the mount.
20
+ */
21
+ declare function wawesomeReact(options?: WawesomeReactOptions): Plugin;
22
+ //#endregion
23
+ export { WawesomeReactOptions, wawesomeReact as default, wawesomeReact };
package/dist/vite.mjs ADDED
@@ -0,0 +1,247 @@
1
+ import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-CXON0L5V.mjs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ //#region src/vite.ts
5
+ const DOCUMENT_MODULE_ID = "virtual:wawesome/document";
6
+ const BASE_MODULE_ID = "virtual:wawesome/base";
7
+ const RESOLVED_DOCUMENT = `\0${DOCUMENT_MODULE_ID}`;
8
+ const RESOLVED_BASE = `\0${BASE_MODULE_ID}`;
9
+ /** Where the rendered application is spliced into the document. */
10
+ const APP_MARKER = "<!--app-html-->";
11
+ const BASE_GLOBAL = "__WAWESOME_BASE__";
12
+ /**
13
+ * Stands where the **Request-time base** goes. The document is escaped whole
14
+ * and split on this afterwards, so no part of the markup can be read as the
15
+ * interpolation.
16
+ */
17
+ const BASE_SLOT = "\0wawesome-base\0";
18
+ /**
19
+ * Builds a React application into the two halves the platform deploys: a client
20
+ * bundle uploaded as **Static assets**, and a server bundle that renders the
21
+ * document and streams it.
22
+ *
23
+ * The document's own HTML is inlined into the server bundle rather than uploaded
24
+ * with the rest: served statically at the mount root it would shadow the SSR
25
+ * route and answer with an empty shell that fails to hydrate. Every URL it
26
+ * carries is a **Request-time base** away from the mount.
27
+ */
28
+ function wawesomeReact(options = {}) {
29
+ const clientEntry = normalize(options.client ?? "src/entry.client.tsx");
30
+ const serverEntry = normalize(options.server ?? "src/entry.server.tsx");
31
+ const htmlFile = normalize(options.html ?? "index.html");
32
+ let root = process.cwd();
33
+ let dev = null;
34
+ let clientBuild = null;
35
+ return {
36
+ name: "wawesome:react-ssr",
37
+ config(_userConfig, env) {
38
+ const building = env.command === "build";
39
+ return {
40
+ appType: "custom",
41
+ base: "./",
42
+ environments: {
43
+ client: { build: {
44
+ outDir: "dist/client",
45
+ rollupOptions: { input: { index: clientEntry } }
46
+ } },
47
+ ssr: {
48
+ define: building ? { "process.env.NODE_ENV": "\"production\"" } : absentGlobalDefines(root),
49
+ resolve: building ? {
50
+ noExternal: true,
51
+ external: [],
52
+ conditions: [
53
+ "workerd",
54
+ "worker",
55
+ "browser"
56
+ ]
57
+ } : {},
58
+ build: {
59
+ outDir: "dist/server",
60
+ ssr: true,
61
+ sourcemap: true,
62
+ copyPublicDir: false,
63
+ rollupOptions: {
64
+ input: { index: serverEntry },
65
+ output: { codeSplitting: false }
66
+ }
67
+ }
68
+ }
69
+ },
70
+ builder: {
71
+ sharedConfigBuild: true,
72
+ async buildApp(builder) {
73
+ await builder.build(builder.environments.client);
74
+ await builder.build(builder.environments.ssr);
75
+ }
76
+ }
77
+ };
78
+ },
79
+ configResolved(config) {
80
+ root = config.root;
81
+ },
82
+ configureServer(server) {
83
+ dev = server;
84
+ reportUnenforceableGaps(server, root);
85
+ return () => {
86
+ server.middlewares.use((req, res, next) => {
87
+ render(server, serverEntry, req, res).catch(next);
88
+ });
89
+ };
90
+ },
91
+ hotUpdate({ file }) {
92
+ if (file !== path.join(root, htmlFile)) return;
93
+ const module = this.environment.moduleGraph.getModuleById(RESOLVED_DOCUMENT);
94
+ if (module) this.environment.moduleGraph.invalidateModule(module);
95
+ dev?.environments.client.hot.send({ type: "full-reload" });
96
+ },
97
+ generateBundle(_output, bundle) {
98
+ if (this.environment.name !== "client") return;
99
+ const entry = Object.values(bundle).find((chunk) => chunk.type === "chunk" && chunk.isEntry);
100
+ if (!entry || entry.type !== "chunk") {
101
+ this.error(`The client build produced no entry chunk for '${clientEntry}'.`);
102
+ return;
103
+ }
104
+ clientBuild = {
105
+ entry: entry.fileName,
106
+ stylesheets: [...entry.viteMetadata?.importedCss ?? []]
107
+ };
108
+ },
109
+ resolveId(id) {
110
+ if (id === DOCUMENT_MODULE_ID) return RESOLVED_DOCUMENT;
111
+ if (id === BASE_MODULE_ID) return RESOLVED_BASE;
112
+ },
113
+ async load(id) {
114
+ if (id === RESOLVED_BASE) return baseModule();
115
+ if (id !== RESOLVED_DOCUMENT) return;
116
+ const html = fs.readFileSync(path.join(root, htmlFile), "utf-8");
117
+ if (dev) return documentModule(await dev.transformIndexHtml("/", html), clientEntry, {
118
+ entry: clientEntry,
119
+ stylesheets: []
120
+ });
121
+ if (!clientBuild) {
122
+ this.error("The client bundle has not been built, so the document has no script to name. Build both environments together with `vite build`.");
123
+ return;
124
+ }
125
+ return documentModule(html, clientEntry, clientBuild);
126
+ }
127
+ };
128
+ }
129
+ /**
130
+ * The globals the guest does not have, taken away from the application's own
131
+ * modules while it is served locally.
132
+ *
133
+ * Rewritten in the application's source rather than deleted off the global
134
+ * scope, because the dev server is Node and Vite's own logger formats its
135
+ * timestamps with `Intl`. A reference becomes a name nothing defines, which is
136
+ * the ReferenceError the guest gives, and `typeof` still answers "undefined".
137
+ */
138
+ function absentGlobalDefines(projectDir) {
139
+ return Object.fromEntries(absentGlobals(projectDir).map((name) => [name, `__wawesome_absent_${name}`]));
140
+ }
141
+ /**
142
+ * Read off the declaration rather than listed here, and skipped for a global the
143
+ * project's manifest says it polyfills — the two rules the build's surface scan
144
+ * applies, so local development cannot disagree with the deploy about what is
145
+ * there.
146
+ */
147
+ function absentGlobals(projectDir) {
148
+ const polyfilled = polyfilledGlobals(projectDir);
149
+ return unsupportedGlobals().map((entry) => entry.name).filter((name) => !polyfilled.has(name));
150
+ }
151
+ /**
152
+ * The declared gaps this dev server cannot reproduce, said once at startup.
153
+ *
154
+ * The locale-sensitive methods are the quiet half of the **Declared guest
155
+ * surface**: the engine has them and answers wrongly, so an application
156
+ * formatting a price renders different markup here and in production. They
157
+ * cannot be replaced on the prototypes, which are Node's own and shared with
158
+ * Vite — its shortcut handler lowercases with one. So they are named instead,
159
+ * and the two places that do hold an application to them are named with them:
160
+ * `wawesome build` reports every use with the file and line, and a suite run
161
+ * under `wawesome/vitest-setup` refuses them outright.
162
+ */
163
+ function reportUnenforceableGaps(server, projectDir) {
164
+ const gone = absentGlobals(projectDir).join(", ");
165
+ const methods = declaredMethods().map((method) => `${method.target}.${method.name}`).join(", ");
166
+ if (gone) server.config.logger.info(` wawesome ${gone} is not defined here, as on the guest`);
167
+ server.config.logger.warn(` wawesome ${methods} still answer here and will not on the guest`);
168
+ server.config.logger.warn(` ${methodRemedy()}`);
169
+ server.config.logger.warn(" `wawesome build` reports each use; `npm test` refuses them.");
170
+ }
171
+ /**
172
+ * Drive the same handler the platform invokes, over the dev server's own module
173
+ * graph, and write what it answers out as it arrives.
174
+ *
175
+ * No forwarded prefix is set, because locally there is no **Mount** to strip.
176
+ */
177
+ async function render(server, serverEntry, req, res) {
178
+ const { runner } = server.environments.ssr;
179
+ const handler = await runner.import(`/${serverEntry}`);
180
+ const url = new URL(req.url ?? "/", "http://localhost");
181
+ const response = await handler.default.fetch(new Request(url, { method: req.method ?? "GET" }));
182
+ res.statusCode = response.status;
183
+ response.headers.forEach((value, name) => res.setHeader(name, value));
184
+ if (response.body) for await (const chunk of response.body) res.write(chunk);
185
+ res.end();
186
+ }
187
+ function baseModule() {
188
+ return [
189
+ `const held = typeof window === "undefined" ? undefined : window[${JSON.stringify(BASE_GLOBAL)}];`,
190
+ "export const base = typeof held === \"string\" ? held : \"\";",
191
+ ""
192
+ ].join("\n");
193
+ }
194
+ function documentModule(html, clientEntry, built) {
195
+ if (html.includes("\0")) throw new Error("The HTML document carries a NUL byte, which is where the request-time base goes.");
196
+ if (!html.includes(APP_MARKER)) throw new Error(`The HTML document has no ${APP_MARKER} for the application to render into.`);
197
+ const document = withStylesheets(withoutEntryScript(html, clientEntry), built.stylesheets);
198
+ const at = document.indexOf(APP_MARKER);
199
+ return [
200
+ `export const clientEntry = ${JSON.stringify(built.entry)};`,
201
+ "",
202
+ "export function documentShell(base) {",
203
+ ` return {`,
204
+ ` before: ${interpolated(document.slice(0, at))},`,
205
+ ` after: ${interpolated(document.slice(at + 15))},`,
206
+ ` };`,
207
+ "}",
208
+ "",
209
+ "export function baseScript(base) {",
210
+ " // A `<` inside a script body ends the element carrying it, whatever the",
211
+ " // JSON around it says.",
212
+ ` return "window[" + ${JSON.stringify(JSON.stringify(BASE_GLOBAL))} + "]=" +`,
213
+ " JSON.stringify(base).replace(/</g, \"\\\\u003c\");",
214
+ "}",
215
+ ""
216
+ ].join("\n");
217
+ }
218
+ /**
219
+ * The entry script is dropped from the document because React emits it itself,
220
+ * as a bootstrap module whose URL carries the base — the same string the rest
221
+ * of the document is built from.
222
+ */
223
+ function withoutEntryScript(html, clientEntry) {
224
+ return html.replace(/[ \t]*<script\b[^>]*><\/script>\n?/g, (tag) => {
225
+ const src = /\bsrc=["']([^"']+)["']/.exec(tag);
226
+ return src && normalize(src[1]) === clientEntry ? "" : tag;
227
+ });
228
+ }
229
+ /**
230
+ * Stylesheets go in as links rather than through React, so they are in the
231
+ * bytes the shell is committed with — a stylesheet React discovers while
232
+ * rendering arrives after the markup it styles.
233
+ */
234
+ function withStylesheets(html, stylesheets) {
235
+ if (stylesheets.length === 0) return html;
236
+ const links = stylesheets.map((href) => `<link rel="stylesheet" href="${BASE_SLOT}/${href}">`).join("");
237
+ return html.includes("</head>") ? html.replace("</head>", `${links}</head>`) : links + html;
238
+ }
239
+ /** A JS expression for `html`, with the base slot as the one live piece. */
240
+ function interpolated(html) {
241
+ return html.split(BASE_SLOT).map((part) => JSON.stringify(part)).join(" + base + ");
242
+ }
243
+ function normalize(file) {
244
+ return file.replace(/\\/g, "/").replace(/^\.?\//, "");
245
+ }
246
+ //#endregion
247
+ export { wawesomeReact as default, wawesomeReact };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,13 @@
17
17
  "types": "./dist/guest-parity.d.mts",
18
18
  "default": "./dist/guest-parity.mjs"
19
19
  },
20
+ "./vite": {
21
+ "types": "./dist/vite.d.mts",
22
+ "default": "./dist/vite.mjs"
23
+ },
24
+ "./vite-env": {
25
+ "types": "./vite-env.d.ts"
26
+ },
20
27
  "./vitest-setup": {
21
28
  "types": "./dist/vitest-setup.d.mts",
22
29
  "default": "./dist/vitest-setup.mjs"
@@ -25,7 +32,8 @@
25
32
  },
26
33
  "files": [
27
34
  "bin",
28
- "dist"
35
+ "dist",
36
+ "vite-env.d.ts"
29
37
  ],
30
38
  "scripts": {
31
39
  "build": "tsdown",
@@ -45,9 +53,23 @@
45
53
  "devDependencies": {
46
54
  "@changesets/cli": "^2.31.1",
47
55
  "@types/node": "^26.1.1",
56
+ "@types/react": "^19.2.7",
57
+ "@types/react-dom": "^19.2.4",
58
+ "@vitejs/plugin-react": "^6.1.0",
48
59
  "publint": "^0.3.22",
60
+ "react": "^19.2.8",
61
+ "react-dom": "^19.2.8",
49
62
  "tsdown": "^0.22.5",
50
63
  "typescript": "^7.0.2",
64
+ "vite": "^8.2.2",
51
65
  "vitest": "^4.1.10"
66
+ },
67
+ "peerDependencies": {
68
+ "vite": "^8.0.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "vite": {
72
+ "optional": true
73
+ }
52
74
  }
53
75
  }
package/vite-env.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The modules `wawesome/vite` generates, as TypeScript sees them.
3
+ *
4
+ * Hand-written because they exist only during a build, so nothing can emit
5
+ * declarations for them. What they actually contain is `documentModule` and
6
+ * `baseModule` in `src/vite.ts`; the CLI's deploy-seam suite builds a real
7
+ * project and runs its bundle, which is what holds the two together.
8
+ *
9
+ * Reached through `"types": ["wawesome/vite-env"]` in a project's tsconfig.
10
+ */
11
+
12
+ declare module "virtual:wawesome/document" {
13
+ /**
14
+ * The document with the rendered application cut out of it, resolved against
15
+ * `base` — the **Mount** the platform stripped off this request.
16
+ */
17
+ export function documentShell(base: string): { before: string; after: string };
18
+
19
+ /**
20
+ * The client bundle's path, relative to the mount. Joined to a base to become
21
+ * the module the browser hydrates from.
22
+ */
23
+ export const clientEntry: string;
24
+
25
+ /**
26
+ * A script body that hands `base` to the browser, for
27
+ * `renderToReadableStream`'s `bootstrapScriptContent`. It is what
28
+ * `virtual:wawesome/base` reads, so the hydrating client resolves its URLs
29
+ * against exactly what the server rendered against.
30
+ */
31
+ export function baseScript(base: string): string;
32
+ }
33
+
34
+ declare module "virtual:wawesome/base" {
35
+ /** The base the server rendered this document against. Empty at the root. */
36
+ export const base: string;
37
+ }