@pylonsync/functions 0.3.278 → 0.3.279

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.3.278",
3
+ "version": "0.3.279",
4
4
  "description": "TypeScript function runtime for pylon — defines server-side queries, mutations, and actions.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,8 @@
10
10
  "types": "./src/index.ts",
11
11
  "default": "./src/index.ts"
12
12
  },
13
- "./runtime": "./src/runtime.ts"
13
+ "./runtime": "./src/runtime.ts",
14
+ "./client-bundler": "./src/ssr-client-bundler.ts"
14
15
  },
15
16
  "bin": {
16
17
  "pylon-functions-runtime": "src/runtime.ts"
@@ -742,6 +742,27 @@ export interface BuildOutput {
742
742
  */
743
743
  let _inflightBuild: Promise<BuildOutput> | null = null;
744
744
 
745
+ /**
746
+ * If a complete prebuilt client-build is present on disk (Pylon Cloud: shipped
747
+ * in the artifact by the builder, marked with `.prebuilt` written last), return
748
+ * its manifest + outdir so callers skip the rebuild entirely. Returns null when
749
+ * there's no prebuilt bundle (dev, or a deploy that didn't pre-build) — callers
750
+ * then build normally.
751
+ */
752
+ async function _prebuiltBundle(): Promise<BuildOutput | null> {
753
+ const fsMod: any = await import("node:fs");
754
+ const pathMod: any = await import("node:path");
755
+ const fs = fsMod.default ?? fsMod;
756
+ const path = pathMod.default ?? pathMod;
757
+ const outdir = path.join(process.cwd(), ".pylon", "client-build");
758
+ const manifestPath = path.join(outdir, "manifest.json");
759
+ const marker = path.join(outdir, ".prebuilt");
760
+ if (fs.existsSync(marker) && fs.existsSync(manifestPath)) {
761
+ return { manifestPath, outdir };
762
+ }
763
+ return null;
764
+ }
765
+
745
766
  /**
746
767
  * Run the bundler in-process and return the manifest path + outdir.
747
768
  * Used from `handleBundleClient` (protocol RPC path from Rust) AND
@@ -750,6 +771,18 @@ let _inflightBuild: Promise<BuildOutput> | null = null;
750
771
  export async function buildClientBundle(
751
772
  appDirRel: string = "app",
752
773
  ): Promise<BuildOutput> {
774
+ // Prebuilt bundle (Pylon Cloud): the builder already produced a complete,
775
+ // content-hashed client-build with `public_prefix` baked to the CDN, shipped
776
+ // it in the artifact, and published the hashed assets to the CDN. Reuse it
777
+ // verbatim instead of rebuilding on the app machine — rebuilding would (a)
778
+ // burn cold-start CPU on every machine on every boot and (b) risk emitting a
779
+ // manifest whose hashes don't match the assets already on the CDN. The
780
+ // `.prebuilt` marker is written LAST by the builder, so a torn/partial copy
781
+ // is never mistaken for complete. Dev never writes the marker, so local
782
+ // hot-rebuild is unaffected.
783
+ const prebuilt = await _prebuiltBundle();
784
+ if (prebuilt) return prebuilt;
785
+
753
786
  if (_inflightBuild) return _inflightBuild;
754
787
  _inflightBuild = (async () => {
755
788
  try {
@@ -1212,3 +1245,28 @@ function makeBuildId(): string {
1212
1245
  _buildCounter += 1;
1213
1246
  return `${process.pid.toString(36)}-${Date.now().toString(36)}-${_buildCounter}`;
1214
1247
  }
1248
+
1249
+ /**
1250
+ * Base URL/path prepended to every SSR-emitted asset reference (the entry
1251
+ * `<script type="module">`, `<link rel="stylesheet">`, and `modulepreload`s)
1252
+ * and to client-side chunk loads — baked into the bundle manifest as
1253
+ * `public_prefix`.
1254
+ *
1255
+ * Defaults to `/_pylon/build/`, which the Rust host serves from the build's
1256
+ * local `outdir`. That keeps dev and any deploy that doesn't opt in unchanged.
1257
+ *
1258
+ * Set `PYLON_PUBLIC_PREFIX` to an ABSOLUTE base (a CDN / object-storage URL for
1259
+ * this build's content-hashed assets, e.g. `https://assets.pyln.dev/<build>/`)
1260
+ * to serve assets off the app machines entirely. This is what makes hashed
1261
+ * assets survive an artifact prune and resolve IDENTICALLY across every Fly
1262
+ * machine: the entry filename SSR emits becomes an absolute URL that always
1263
+ * exists, instead of a machine-local path a prune/rollover can 404 (the bug
1264
+ * where a stale machine emitted `client-entry-…-<oldhash>.js` after that file
1265
+ * was pruned, breaking hydration). A trailing slash is enforced so
1266
+ * `${prefix}${file}` always joins correctly.
1267
+ */
1268
+ function resolvePublicPrefix(): string {
1269
+ const raw = (process.env.PYLON_PUBLIC_PREFIX ?? "").trim();
1270
+ if (!raw) return "/_pylon/build/";
1271
+ return raw.endsWith("/") ? raw : `${raw}/`;
1272
+ }
@@ -1018,7 +1018,12 @@ export function buildHydrationTail(args: {
1018
1018
  .replace(/\u2029/g, "\\u2029");
1019
1019
  let tail = `<script id="__PYLON_DATA__" type="application/json">${json}</script>`;
1020
1020
  if (args.manifestRoute) {
1021
- tail += `<script type="module" src="${args.publicPrefix}${args.manifestRoute.file}"></script>`;
1021
+ // A cross-origin CDN module (`public_prefix` is an absolute http(s) URL) is
1022
+ // fetched in CORS mode; `crossorigin` makes the matching modulepreload
1023
+ // reusable (no double fetch) and surfaces load errors. No-op for the
1024
+ // same-origin `/_pylon/build/` default.
1025
+ const co = /^https?:\/\//i.test(args.publicPrefix) ? " crossorigin" : "";
1026
+ tail += `<script type="module"${co} src="${args.publicPrefix}${args.manifestRoute.file}"></script>`;
1022
1027
  } else {
1023
1028
  tail += `<script>console.warn(${JSON.stringify(`[pylon ssr] hydration disabled: ${args.manifestErr}`)})</script>`;
1024
1029
  }
@@ -1117,11 +1122,12 @@ async function renderBoundaryToClient(
1117
1122
  }
1118
1123
  }
1119
1124
  if (manifestRoute) {
1125
+ const co = /^https?:\/\//i.test(publicPrefix) ? " crossorigin" : "";
1120
1126
  for (const css of manifestRoute.css) {
1121
1127
  headBlob += `<link rel="stylesheet" href="${publicPrefix}${css}">`;
1122
1128
  }
1123
1129
  for (const chunk of manifestRoute.imports) {
1124
- headBlob += `<link rel="modulepreload" href="${publicPrefix}${chunk}">`;
1130
+ headBlob += `<link rel="modulepreload"${co} href="${publicPrefix}${chunk}">`;
1125
1131
  }
1126
1132
  } else {
1127
1133
  // No per-boundary entry → fall back to the global stylesheet union so the
@@ -1884,11 +1890,12 @@ export async function handleRenderRoute(
1884
1890
  // (it needs the inline __PYLON_DATA__ to have been parsed first).
1885
1891
  let headBlob = "";
1886
1892
  if (preloadManifestRoute) {
1893
+ const co = /^https?:\/\//i.test(preloadPublicPrefix) ? " crossorigin" : "";
1887
1894
  for (const css of preloadManifestRoute.css) {
1888
1895
  headBlob += `<link rel="stylesheet" href="${preloadPublicPrefix}${css}">`;
1889
1896
  }
1890
1897
  for (const chunk of preloadManifestRoute.imports) {
1891
- headBlob += `<link rel="modulepreload" href="${preloadPublicPrefix}${chunk}">`;
1898
+ headBlob += `<link rel="modulepreload"${co} href="${preloadPublicPrefix}${chunk}">`;
1892
1899
  }
1893
1900
  } else {
1894
1901
  // No per-route client entry. This is the unmatched-URL not-found