@pylonsync/functions 0.3.278 → 0.3.280

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.280",
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,48 @@ 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
+ if (!fs.existsSync(manifestPath)) return null;
760
+
761
+ // Reuse a prebuilt bundle when it's explicitly marked (`.prebuilt`) OR when
762
+ // its manifest already targets an ABSOLUTE (CDN) `public_prefix`. The builder
763
+ // bakes an absolute prefix only when it pre-built for the CDN, and it
764
+ // published the hashed assets under THOSE exact hashes — so the runtime must
765
+ // serve this exact manifest verbatim (a local rebuild emits different hashes
766
+ // that would 404 on the CDN). The manifest is a regular file that always
767
+ // ships with the build; keying on it (not just the `.prebuilt` dotfile, which
768
+ // can be dropped in transit) makes reuse robust. A same-origin
769
+ // `/_pylon/build/` manifest is a normal dev/local build → don't short-circuit
770
+ // (let dev hot-rebuild).
771
+ const marker = path.join(outdir, ".prebuilt");
772
+ if (fs.existsSync(marker)) return { manifestPath, outdir };
773
+ try {
774
+ const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
775
+ if (
776
+ typeof m.public_prefix === "string" &&
777
+ /^https?:\/\//i.test(m.public_prefix)
778
+ ) {
779
+ return { manifestPath, outdir };
780
+ }
781
+ } catch {
782
+ /* unreadable/partial manifest → fall through and rebuild */
783
+ }
784
+ return null;
785
+ }
786
+
745
787
  /**
746
788
  * Run the bundler in-process and return the manifest path + outdir.
747
789
  * Used from `handleBundleClient` (protocol RPC path from Rust) AND
@@ -750,6 +792,18 @@ let _inflightBuild: Promise<BuildOutput> | null = null;
750
792
  export async function buildClientBundle(
751
793
  appDirRel: string = "app",
752
794
  ): Promise<BuildOutput> {
795
+ // Prebuilt bundle (Pylon Cloud): the builder already produced a complete,
796
+ // content-hashed client-build with `public_prefix` baked to the CDN, shipped
797
+ // it in the artifact, and published the hashed assets to the CDN. Reuse it
798
+ // verbatim instead of rebuilding on the app machine — rebuilding would (a)
799
+ // burn cold-start CPU on every machine on every boot and (b) risk emitting a
800
+ // manifest whose hashes don't match the assets already on the CDN. The
801
+ // `.prebuilt` marker is written LAST by the builder, so a torn/partial copy
802
+ // is never mistaken for complete. Dev never writes the marker, so local
803
+ // hot-rebuild is unaffected.
804
+ const prebuilt = await _prebuiltBundle();
805
+ if (prebuilt) return prebuilt;
806
+
753
807
  if (_inflightBuild) return _inflightBuild;
754
808
  _inflightBuild = (async () => {
755
809
  try {
@@ -1212,3 +1266,28 @@ function makeBuildId(): string {
1212
1266
  _buildCounter += 1;
1213
1267
  return `${process.pid.toString(36)}-${Date.now().toString(36)}-${_buildCounter}`;
1214
1268
  }
1269
+
1270
+ /**
1271
+ * Base URL/path prepended to every SSR-emitted asset reference (the entry
1272
+ * `<script type="module">`, `<link rel="stylesheet">`, and `modulepreload`s)
1273
+ * and to client-side chunk loads — baked into the bundle manifest as
1274
+ * `public_prefix`.
1275
+ *
1276
+ * Defaults to `/_pylon/build/`, which the Rust host serves from the build's
1277
+ * local `outdir`. That keeps dev and any deploy that doesn't opt in unchanged.
1278
+ *
1279
+ * Set `PYLON_PUBLIC_PREFIX` to an ABSOLUTE base (a CDN / object-storage URL for
1280
+ * this build's content-hashed assets, e.g. `https://assets.pyln.dev/<build>/`)
1281
+ * to serve assets off the app machines entirely. This is what makes hashed
1282
+ * assets survive an artifact prune and resolve IDENTICALLY across every Fly
1283
+ * machine: the entry filename SSR emits becomes an absolute URL that always
1284
+ * exists, instead of a machine-local path a prune/rollover can 404 (the bug
1285
+ * where a stale machine emitted `client-entry-…-<oldhash>.js` after that file
1286
+ * was pruned, breaking hydration). A trailing slash is enforced so
1287
+ * `${prefix}${file}` always joins correctly.
1288
+ */
1289
+ function resolvePublicPrefix(): string {
1290
+ const raw = (process.env.PYLON_PUBLIC_PREFIX ?? "").trim();
1291
+ if (!raw) return "/_pylon/build/";
1292
+ return raw.endsWith("/") ? raw : `${raw}/`;
1293
+ }
@@ -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