veryfront 0.1.764 → 0.1.766

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 (59) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/cache/keys/builders/render.d.ts +8 -1
  3. package/esm/src/cache/keys/builders/render.d.ts.map +1 -1
  4. package/esm/src/cache/keys/builders/render.js +10 -2
  5. package/esm/src/html/html-shell-generator.d.ts.map +1 -1
  6. package/esm/src/html/html-shell-generator.js +24 -4
  7. package/esm/src/html/schemas/html.schema.d.ts +2 -0
  8. package/esm/src/html/schemas/html.schema.d.ts.map +1 -1
  9. package/esm/src/html/schemas/html.schema.js +1 -0
  10. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
  11. package/esm/src/platform/adapters/fs/veryfront/adapter.js +34 -0
  12. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts +31 -0
  13. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts.map +1 -1
  14. package/esm/src/platform/adapters/veryfront-api-client/client.js +21 -0
  15. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts +6 -1
  16. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
  17. package/esm/src/platform/adapters/veryfront-api-client/operations.js +59 -1
  18. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +22 -0
  19. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts.map +1 -1
  20. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +39 -0
  21. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts +1 -1
  22. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts.map +1 -1
  23. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.js +1 -1
  24. package/esm/src/proxy/asset-handler.d.ts +30 -0
  25. package/esm/src/proxy/asset-handler.d.ts.map +1 -0
  26. package/esm/src/proxy/asset-handler.js +108 -0
  27. package/esm/src/proxy/main.js +4 -0
  28. package/esm/src/release-assets/build-executor.d.ts +89 -0
  29. package/esm/src/release-assets/build-executor.d.ts.map +1 -0
  30. package/esm/src/release-assets/build-executor.js +405 -0
  31. package/esm/src/release-assets/constants.d.ts +35 -0
  32. package/esm/src/release-assets/constants.d.ts.map +1 -0
  33. package/esm/src/release-assets/constants.js +50 -0
  34. package/esm/src/release-assets/hash.d.ts +14 -0
  35. package/esm/src/release-assets/hash.d.ts.map +1 -0
  36. package/esm/src/release-assets/hash.js +22 -0
  37. package/esm/src/release-assets/html-consumption.d.ts +31 -0
  38. package/esm/src/release-assets/html-consumption.d.ts.map +1 -0
  39. package/esm/src/release-assets/html-consumption.js +64 -0
  40. package/esm/src/release-assets/manifest-cache.d.ts +70 -0
  41. package/esm/src/release-assets/manifest-cache.d.ts.map +1 -0
  42. package/esm/src/release-assets/manifest-cache.js +181 -0
  43. package/esm/src/release-assets/manifest-schema.d.ts +70 -0
  44. package/esm/src/release-assets/manifest-schema.d.ts.map +1 -0
  45. package/esm/src/release-assets/manifest-schema.js +126 -0
  46. package/esm/src/rendering/context/render-context.d.ts.map +1 -1
  47. package/esm/src/rendering/context/render-context.js +3 -1
  48. package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
  49. package/esm/src/rendering/orchestrator/html.js +16 -0
  50. package/esm/src/rendering/orchestrator/types.d.ts +2 -0
  51. package/esm/src/rendering/orchestrator/types.d.ts.map +1 -1
  52. package/esm/src/server/context/enriched-context.d.ts.map +1 -1
  53. package/esm/src/server/context/enriched-context.js +2 -1
  54. package/esm/src/server/handlers/request/project-run-execute.handler.d.ts +5 -0
  55. package/esm/src/server/handlers/request/project-run-execute.handler.d.ts.map +1 -1
  56. package/esm/src/server/handlers/request/project-run-execute.handler.js +86 -3
  57. package/esm/src/utils/version-constant.d.ts +1 -1
  58. package/esm/src/utils/version-constant.js +1 -1
  59. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Proxy handler for content-addressed release assets.
3
+ *
4
+ * Owns the `/_vf/assets/{hash}.{js|css}` prefix on the project's own domain.
5
+ * Validates the hash + extension, fetches bytes from the API's public
6
+ * `/release-assets/{hash}` endpoint, caches hot bytes in a small in-memory LRU,
7
+ * and serves them immutable + nosniff with an allowlisted content type.
8
+ *
9
+ * The renderer is never involved.
10
+ *
11
+ * @module proxy/asset-handler
12
+ */
13
+ /** True when the path is owned by the release asset prefix. */
14
+ export declare function isReleaseAssetPath(pathname: string): boolean;
15
+ export interface ReleaseAssetHandlerOptions {
16
+ apiBaseUrl: string;
17
+ /** Injectable fetch for tests. Defaults to global fetch. */
18
+ fetchImpl?: typeof fetch;
19
+ }
20
+ /**
21
+ * Serve a release asset for the given request URL.
22
+ *
23
+ * @returns a Response on the asset path, or null if the path is not an asset
24
+ * path (caller should continue normal forwarding). For invalid hashes/exts a
25
+ * 400 is returned; for upstream 404s a no-cache 404 is returned.
26
+ */
27
+ export declare function handleReleaseAssetRequest(url: URL, options: ReleaseAssetHandlerOptions): Promise<Response | null>;
28
+ /** Clear the in-memory asset cache (tests / memory pressure). */
29
+ export declare function clearReleaseAssetProxyCache(): void;
30
+ //# sourceMappingURL=asset-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/asset-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAuBH,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE5D;AAqBD,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAgE1B;AAED,iEAAiE;AACjE,wBAAgB,2BAA2B,IAAI,IAAI,CAElD"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Proxy handler for content-addressed release assets.
3
+ *
4
+ * Owns the `/_vf/assets/{hash}.{js|css}` prefix on the project's own domain.
5
+ * Validates the hash + extension, fetches bytes from the API's public
6
+ * `/release-assets/{hash}` endpoint, caches hot bytes in a small in-memory LRU,
7
+ * and serves them immutable + nosniff with an allowlisted content type.
8
+ *
9
+ * The renderer is never involved.
10
+ *
11
+ * @module proxy/asset-handler
12
+ */
13
+ import { LRUCache } from "../utils/lru-wrapper.js";
14
+ import { contentTypeForExtension, isAllowedReleaseAssetContentType, isValidContentHash, RELEASE_ASSET_IMMUTABLE_MAX_AGE_SECONDS, } from "../release-assets/constants.js";
15
+ const ASSET_PATH_PREFIX = "/_vf/assets/";
16
+ const ASSET_PATH_RE = /^\/_vf\/assets\/([0-9a-f]{64})\.(js|css)$/;
17
+ /** Bound on cached asset bodies (~100 hot entries). */
18
+ const MAX_CACHED_ASSETS = 100;
19
+ const assetCache = new LRUCache({ maxEntries: MAX_CACHED_ASSETS });
20
+ /** True when the path is owned by the release asset prefix. */
21
+ export function isReleaseAssetPath(pathname) {
22
+ return pathname.startsWith(ASSET_PATH_PREFIX);
23
+ }
24
+ const IMMUTABLE_HEADERS = {
25
+ "Cache-Control": `public, max-age=${RELEASE_ASSET_IMMUTABLE_MAX_AGE_SECONDS}, immutable`,
26
+ "X-Content-Type-Options": "nosniff",
27
+ };
28
+ function notFound() {
29
+ return new Response("Not found", {
30
+ status: 404,
31
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache" },
32
+ });
33
+ }
34
+ function badRequest(message) {
35
+ return new Response(message, {
36
+ status: 400,
37
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache" },
38
+ });
39
+ }
40
+ /**
41
+ * Serve a release asset for the given request URL.
42
+ *
43
+ * @returns a Response on the asset path, or null if the path is not an asset
44
+ * path (caller should continue normal forwarding). For invalid hashes/exts a
45
+ * 400 is returned; for upstream 404s a no-cache 404 is returned.
46
+ */
47
+ export async function handleReleaseAssetRequest(url, options) {
48
+ if (!isReleaseAssetPath(url.pathname))
49
+ return null;
50
+ const match = url.pathname.match(ASSET_PATH_RE);
51
+ if (!match) {
52
+ // Path is under the asset prefix but malformed (bad hash/ext) → 400.
53
+ return badRequest("Invalid asset path");
54
+ }
55
+ const hash = match[1];
56
+ const ext = match[2];
57
+ // Defense in depth: the regex already constrains these.
58
+ if (!isValidContentHash(hash) || (ext !== "js" && ext !== "css")) {
59
+ return badRequest("Invalid asset path");
60
+ }
61
+ const cacheKey = `${hash}.${ext}`;
62
+ const cached = assetCache.get(cacheKey);
63
+ if (cached) {
64
+ return new Response(cached.bytes, {
65
+ status: 200,
66
+ headers: { ...IMMUTABLE_HEADERS, "Content-Type": cached.contentType },
67
+ });
68
+ }
69
+ const doFetch = options.fetchImpl ?? fetch;
70
+ const upstreamUrl = `${options.apiBaseUrl}/release-assets/${hash}`;
71
+ let response;
72
+ try {
73
+ response = await doFetch(upstreamUrl);
74
+ }
75
+ catch {
76
+ return new Response("Bad gateway", {
77
+ status: 502,
78
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache" },
79
+ });
80
+ }
81
+ if (response.status === 404)
82
+ return notFound();
83
+ if (!response.ok) {
84
+ return new Response("Bad gateway", {
85
+ status: 502,
86
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache" },
87
+ });
88
+ }
89
+ const upstreamContentType = response.headers.get("content-type");
90
+ if (!isAllowedReleaseAssetContentType(upstreamContentType)) {
91
+ return new Response("Bad gateway", {
92
+ status: 502,
93
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache" },
94
+ });
95
+ }
96
+ // Serve the expected content type for the extension (allowlisted).
97
+ const contentType = contentTypeForExtension(ext);
98
+ const bytes = new Uint8Array(await response.arrayBuffer());
99
+ assetCache.set(cacheKey, { bytes, contentType });
100
+ return new Response(bytes, {
101
+ status: 200,
102
+ headers: { ...IMMUTABLE_HEADERS, "Content-Type": contentType },
103
+ });
104
+ }
105
+ /** Clear the in-memory asset cache (tests / memory pressure). */
106
+ export function clearReleaseAssetProxyCache() {
107
+ assetCache.clear();
108
+ }
@@ -34,6 +34,7 @@ import { parseProjectDomain } from "../server/utils/domain-parser.js";
34
34
  import { exit, getEnv, onSignal } from "../platform/compat/process.js";
35
35
  import { createHttpServer, upgradeWebSocket } from "../platform/compat/http/index.js";
36
36
  import { createProxyErrorResponse, jsonErrorResponse } from "./error-response.js";
37
+ import { handleReleaseAssetRequest, isReleaseAssetPath } from "./asset-handler.js";
37
38
  function getLocalProjects() {
38
39
  const raw = getEnv("LOCAL_PROJECTS");
39
40
  return raw ? JSON.parse(raw) : {};
@@ -470,6 +471,9 @@ function router(req) {
470
471
  }
471
472
  if (url.pathname.startsWith("/_vf/api/"))
472
473
  return handleApiProxy(req, url);
474
+ if (isReleaseAssetPath(url.pathname)) {
475
+ return handleReleaseAssetRequest(url, { apiBaseUrl: config.apiBaseUrl }).then((res) => res ?? forwardToServer(req, url));
476
+ }
473
477
  return forwardToServer(req, url);
474
478
  }
475
479
  // Create server before signal registration so early SIGTERM/SIGINT can close it safely.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Release Asset Manifest — builder executor.
3
+ *
4
+ * Runs inside the project runtime as the `task:release-asset-build` handler.
5
+ * Materializes a release's file set, transforms every browser module through
6
+ * the SAME pipeline `serveModule` uses (byte parity with the JIT fallback is a
7
+ * hard requirement), compiles route CSS where reachable, content-addresses and
8
+ * uploads each asset, then assembles and PUTs the manifest (→ ready).
9
+ *
10
+ * Defensive by construction:
11
+ * - Any module transform failure reports `failed` and stops without PUTting.
12
+ * - Any other build failure (list/hash/upload/PUT) also reports `failed`.
13
+ * - The temp dir is always cleaned up by the caller.
14
+ *
15
+ * @module release-assets/build-executor
16
+ */
17
+ export interface ReleaseAssetBuildInput {
18
+ /** Project reference (slug or id) used for API calls. */
19
+ projectReference: string;
20
+ /** Project UUID. */
21
+ projectId: string;
22
+ /** Release UUID. */
23
+ releaseId: string;
24
+ /** Release version (integer). */
25
+ releaseVersion: number;
26
+ /** Release version string used for API path segments. */
27
+ releaseVersionRef: string;
28
+ /** React version for transforms. */
29
+ reactVersion?: string;
30
+ /** Authenticated, project-scoped API client. */
31
+ client: ReleaseAssetBuildClient;
32
+ /** Runtime adapter used by the transform pipeline. */
33
+ adapter: any;
34
+ /**
35
+ * Transform function. Defaults to the same `transformToESM` pipeline
36
+ * `serveModule` uses (browser, non-SSR) — byte parity is a hard requirement.
37
+ * Injectable for tests.
38
+ */
39
+ transform?: ReleaseAssetTransform;
40
+ }
41
+ export type ReleaseAssetTransform = (source: string, sourceFile: string, projectDir: string, adapter: any, options: {
42
+ projectId: string;
43
+ dev: boolean;
44
+ ssr: boolean;
45
+ reactVersion?: string;
46
+ }) => Promise<string>;
47
+ /** Subset of the API client used by the builder (eases testing). */
48
+ export interface ReleaseAssetBuildClient {
49
+ beginReleaseAssetManifestBuild(version: string): Promise<{
50
+ id: string;
51
+ manifest_version: number;
52
+ state: string;
53
+ }>;
54
+ listAllReleaseFiles(version: string): Promise<Array<{
55
+ path: string;
56
+ content?: string;
57
+ }>>;
58
+ uploadReleaseAsset(version: string, contentHash: string, contentType: string, bytes: Uint8Array): Promise<{
59
+ stored: boolean;
60
+ existed: boolean;
61
+ }>;
62
+ putReleaseAssetManifest(version: string, manifest: unknown): Promise<{
63
+ state: string;
64
+ manifest_version?: number;
65
+ }>;
66
+ reportReleaseAssetManifestState(version: string, state: "partial" | "failed", error?: string): Promise<unknown>;
67
+ /** Optional project CSS compiler; when absent, css:[] is recorded. */
68
+ compileProjectCss?(candidates: Set<string>): Promise<{
69
+ css: string;
70
+ styleProfileHash: string | null;
71
+ } | null>;
72
+ }
73
+ export interface ReleaseAssetBuildResult {
74
+ success: boolean;
75
+ state: "ready" | "failed";
76
+ moduleCount: number;
77
+ cssCount: number;
78
+ routeCount: number;
79
+ gaps: string[];
80
+ error?: string;
81
+ }
82
+ /** Derive a route path from a page module logical path. */
83
+ export declare function routeForPage(logicalPath: string): string | null;
84
+ /**
85
+ * Execute a release asset build. Pure orchestration over the injected client
86
+ * and a runtime-provided temp dir + react version.
87
+ */
88
+ export declare function runReleaseAssetBuild(input: ReleaseAssetBuildInput, tempDir: string): Promise<ReleaseAssetBuildResult>;
89
+ //# sourceMappingURL=build-executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-executor.d.ts","sourceRoot":"","sources":["../../../src/src/release-assets/build-executor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Release Asset Manifest — builder executor.
3
+ *
4
+ * Runs inside the project runtime as the `task:release-asset-build` handler.
5
+ * Materializes a release's file set, transforms every browser module through
6
+ * the SAME pipeline `serveModule` uses (byte parity with the JIT fallback is a
7
+ * hard requirement), compiles route CSS where reachable, content-addresses and
8
+ * uploads each asset, then assembles and PUTs the manifest (→ ready).
9
+ *
10
+ * Defensive by construction:
11
+ * - Any module transform failure reports `failed` and stops without PUTting.
12
+ * - Any other build failure (list/hash/upload/PUT) also reports `failed`.
13
+ * - The temp dir is always cleaned up by the caller.
14
+ *
15
+ * @module release-assets/build-executor
16
+ */
17
+ import { serverLogger } from "../utils/index.js";
18
+ import { VERSION } from "../utils/version.js";
19
+ import { createFileSystem } from "../platform/compat/fs.js";
20
+ import { dirname, join } from "../platform/compat/path/index.js";
21
+ import { transformToESM } from "../transforms/esm-transform.js";
22
+ import { sha256HexBytes } from "./hash.js";
23
+ import { RELEASE_ASSET_BASE_PATH, RELEASE_ASSET_CONTENT_TYPES, RELEASE_ASSET_MANIFEST_SCHEMA_VERSION, RELEASE_ASSET_MAX_SIZE_BYTES, RELEASE_ASSET_UPLOAD_CONCURRENCY, } from "./constants.js";
24
+ const logger = serverLogger.component("release-asset-build");
25
+ /** Browser module source extensions eligible for transform. */
26
+ const BROWSER_MODULE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mdx"];
27
+ /** Directories whose modules are part of the browser closure. */
28
+ const BROWSER_MODULE_DIRS = ["pages/", "components/", "layouts/", "lib/", "src/"];
29
+ /** Sanitize an error for state reporting (no internal paths / stack traces). */
30
+ function sanitizeError(error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ return message.replace(/\/[^\s]+/g, "<path>").slice(0, 300);
33
+ }
34
+ /** True when a logical path is an eligible browser module. */
35
+ function isBrowserModule(path) {
36
+ if (!BROWSER_MODULE_EXTENSIONS.some((ext) => path.endsWith(ext)))
37
+ return false;
38
+ if (path.endsWith(".d.ts"))
39
+ return false;
40
+ return BROWSER_MODULE_DIRS.some((dir) => path.startsWith(dir));
41
+ }
42
+ /** Derive a route path from a page module logical path. */
43
+ export function routeForPage(logicalPath) {
44
+ if (!logicalPath.startsWith("pages/"))
45
+ return null;
46
+ const withoutPrefix = logicalPath.slice("pages/".length);
47
+ const withoutExt = withoutPrefix.replace(/\.(tsx|ts|jsx|mdx|js)$/, "");
48
+ const route = withoutExt.replace(/\/index$/, "").replace(/^index$/, "");
49
+ return `/${route}`.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
50
+ }
51
+ /**
52
+ * Statically resolve relative imports in a source file to logical paths.
53
+ *
54
+ * Parses `import/export ... from "..."` and bare `import "..."` statements.
55
+ * Only relative specifiers (`./` or `../`) are resolved; package imports and
56
+ * absolute URLs are skipped. Extension-less specifiers try each browser module
57
+ * extension in order.
58
+ */
59
+ function resolveStaticImports(source, moduleLogicalPath, knownPaths) {
60
+ const importRe = /(?:^|;|\n)\s*(?:import|export)\s+(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/gm;
61
+ const results = [];
62
+ let m;
63
+ while ((m = importRe.exec(source)) !== null) {
64
+ const specifier = m[1];
65
+ if (!specifier.startsWith("./") && !specifier.startsWith("../"))
66
+ continue;
67
+ const dir = moduleLogicalPath.includes("/")
68
+ ? moduleLogicalPath.slice(0, moduleLogicalPath.lastIndexOf("/"))
69
+ : ".";
70
+ // Resolve the path segments manually (no path library needed for simple cases).
71
+ const segments = `${dir}/${specifier}`.split("/").filter((s) => s !== "");
72
+ const resolved = [];
73
+ for (const seg of segments) {
74
+ if (seg === "..") {
75
+ resolved.pop();
76
+ }
77
+ else if (seg !== ".") {
78
+ resolved.push(seg);
79
+ }
80
+ }
81
+ const candidate = resolved.join("/");
82
+ // If the specifier already has a known extension and exists, use it.
83
+ if (knownPaths.has(candidate)) {
84
+ results.push(candidate);
85
+ continue;
86
+ }
87
+ // Try appending each browser module extension.
88
+ let found = false;
89
+ for (const ext of BROWSER_MODULE_EXTENSIONS) {
90
+ const withExt = `${candidate}${ext}`;
91
+ if (knownPaths.has(withExt)) {
92
+ results.push(withExt);
93
+ found = true;
94
+ break;
95
+ }
96
+ }
97
+ // Also try /index variants for directory imports.
98
+ if (!found) {
99
+ for (const ext of BROWSER_MODULE_EXTENSIONS) {
100
+ const indexPath = `${candidate}/index${ext}`;
101
+ if (knownPaths.has(indexPath)) {
102
+ results.push(indexPath);
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return results;
109
+ }
110
+ /**
111
+ * Walk the static import graph from a set of entry points using BFS.
112
+ * Returns all reachable logical paths (entries included).
113
+ * Modules not in `sourceByPath` are recorded as closure gaps.
114
+ */
115
+ function collectClosure(entrypoints, sourceByPath, knownPaths) {
116
+ const visited = new Set();
117
+ const queue = [...entrypoints];
118
+ const gaps = [];
119
+ while (queue.length > 0) {
120
+ const current = queue.shift();
121
+ if (visited.has(current))
122
+ continue;
123
+ visited.add(current);
124
+ const source = sourceByPath.get(current);
125
+ if (!source) {
126
+ // Module referenced but not in the materialized file set.
127
+ gaps.push(`closure-missing:${current}`);
128
+ continue;
129
+ }
130
+ const imports = resolveStaticImports(source, current, knownPaths);
131
+ for (const imp of imports) {
132
+ if (!visited.has(imp))
133
+ queue.push(imp);
134
+ }
135
+ }
136
+ return { modules: [...visited], gaps };
137
+ }
138
+ /**
139
+ * Execute a release asset build. Pure orchestration over the injected client
140
+ * and a runtime-provided temp dir + react version.
141
+ */
142
+ export async function runReleaseAssetBuild(input, tempDir) {
143
+ const { client } = input;
144
+ const transform = input.transform ??
145
+ ((source, sourceFile, projectDir, adapter, options) => transformToESM(source, sourceFile, projectDir, adapter, {
146
+ projectId: options.projectId,
147
+ dev: options.dev,
148
+ ssr: options.ssr,
149
+ studioEmbed: false,
150
+ reactVersion: options.reactVersion,
151
+ }));
152
+ // H1: wrap the whole build so any non-transform failure also reports failed.
153
+ try {
154
+ return await runBuildInner(input, tempDir, client, transform);
155
+ }
156
+ catch (error) {
157
+ const sanitized = sanitizeError(error);
158
+ logger.warn("Release asset build failed (non-transform error)", {
159
+ releaseId: input.releaseId,
160
+ error: sanitized,
161
+ });
162
+ try {
163
+ await client.reportReleaseAssetManifestState(input.releaseVersionRef, "failed", sanitized);
164
+ }
165
+ catch (reportErr) {
166
+ logger.warn("Failed to report build failure state", {
167
+ releaseId: input.releaseId,
168
+ error: sanitizeError(reportErr),
169
+ });
170
+ }
171
+ return {
172
+ success: false,
173
+ state: "failed",
174
+ moduleCount: 0,
175
+ cssCount: 0,
176
+ routeCount: 0,
177
+ gaps: [],
178
+ error: sanitized,
179
+ };
180
+ }
181
+ }
182
+ async function runBuildInner(input, tempDir, client, transform) {
183
+ // 1. Begin (idempotent). H2: capture manifest_version from the API response.
184
+ const beginResult = await client.beginReleaseAssetManifestBuild(input.releaseVersionRef);
185
+ const manifestVersion = beginResult.manifest_version;
186
+ // 2. Materialize the release file set.
187
+ const files = await client.listAllReleaseFiles(input.releaseVersionRef);
188
+ const fs = createFileSystem();
189
+ const sourceByPath = new Map();
190
+ for (const file of files) {
191
+ if (typeof file.content !== "string")
192
+ continue;
193
+ sourceByPath.set(file.path, file.content);
194
+ const abs = join(tempDir, file.path);
195
+ await fs.mkdir(dirname(abs), { recursive: true });
196
+ await fs.writeTextFile(abs, file.content);
197
+ }
198
+ // 3 + 4. Collect the browser module closure and transform each module
199
+ // through the SAME pipeline serveModule uses (browser, non-SSR).
200
+ const modules = {};
201
+ const gaps = [];
202
+ const uploadQueue = [];
203
+ // Bytes are held per-hash only until uploaded, then dropped (M3).
204
+ const pendingBytes = new Map();
205
+ const knownPaths = new Set(sourceByPath.keys());
206
+ for (const [logicalPath, source] of sourceByPath) {
207
+ if (!isBrowserModule(logicalPath))
208
+ continue;
209
+ // M2: enforce client-side size limit before transform (source is a proxy;
210
+ // transformed output is checked after encoding below).
211
+ const sourceFile = join(tempDir, logicalPath);
212
+ let code;
213
+ try {
214
+ code = await transform(source, sourceFile, tempDir, input.adapter, {
215
+ projectId: input.projectId,
216
+ dev: false,
217
+ ssr: false,
218
+ reactVersion: input.reactVersion,
219
+ });
220
+ }
221
+ catch (error) {
222
+ // Hard requirement: any module transform failure → report failed, stop.
223
+ const sanitized = sanitizeError(error);
224
+ logger.warn("Module transform failed during release asset build", {
225
+ path: logicalPath,
226
+ error: sanitized,
227
+ });
228
+ await client.reportReleaseAssetManifestState(input.releaseVersionRef, "failed", sanitized);
229
+ return {
230
+ success: false,
231
+ state: "failed",
232
+ moduleCount: 0,
233
+ cssCount: 0,
234
+ routeCount: 0,
235
+ gaps,
236
+ error: sanitized,
237
+ };
238
+ }
239
+ // L2: hash the bytes, not the string.
240
+ const bytes = new TextEncoder().encode(code);
241
+ // M2: enforce 10 MB client-side limit — skip oversized modules with a gap.
242
+ if (bytes.byteLength > RELEASE_ASSET_MAX_SIZE_BYTES) {
243
+ gaps.push(`oversized:${logicalPath}`);
244
+ logger.warn("Module exceeds max size, skipping", {
245
+ path: logicalPath,
246
+ size: bytes.byteLength,
247
+ limit: RELEASE_ASSET_MAX_SIZE_BYTES,
248
+ });
249
+ continue;
250
+ }
251
+ const contentHash = await sha256HexBytes(bytes);
252
+ const entry = {
253
+ logicalPath,
254
+ contentHash,
255
+ size: bytes.byteLength,
256
+ contentType: RELEASE_ASSET_CONTENT_TYPES.js,
257
+ };
258
+ modules[logicalPath] = entry;
259
+ if (!pendingBytes.has(contentHash)) {
260
+ pendingBytes.set(contentHash, { bytes, contentType: RELEASE_ASSET_CONTENT_TYPES.js });
261
+ uploadQueue.push(entry);
262
+ }
263
+ }
264
+ // 5b. CSS: compile project CSS where reachable, else record css:[] and note.
265
+ const css = [];
266
+ const cssHashes = [];
267
+ if (client.compileProjectCss) {
268
+ try {
269
+ const candidates = collectClassCandidates(sourceByPath);
270
+ const compiled = await client.compileProjectCss(candidates);
271
+ if (compiled && compiled.css) {
272
+ const bytes = new TextEncoder().encode(compiled.css);
273
+ const contentHash = await sha256HexBytes(bytes);
274
+ css.push({
275
+ contentHash,
276
+ size: bytes.byteLength,
277
+ contentType: RELEASE_ASSET_CONTENT_TYPES.css,
278
+ styleProfileHash: compiled.styleProfileHash,
279
+ });
280
+ cssHashes.push(contentHash);
281
+ if (!pendingBytes.has(contentHash)) {
282
+ pendingBytes.set(contentHash, { bytes, contentType: RELEASE_ASSET_CONTENT_TYPES.css });
283
+ uploadQueue.push({
284
+ logicalPath: `__css__/${contentHash}`,
285
+ contentHash,
286
+ size: bytes.byteLength,
287
+ contentType: RELEASE_ASSET_CONTENT_TYPES.css,
288
+ });
289
+ }
290
+ }
291
+ }
292
+ catch (error) {
293
+ // CSS is best-effort: record a gap, keep css:[].
294
+ gaps.push("css:compile-failed");
295
+ logger.warn("Release asset CSS compile failed (recording gap)", {
296
+ error: sanitizeError(error),
297
+ });
298
+ }
299
+ }
300
+ else {
301
+ gaps.push("css:no-pipeline");
302
+ }
303
+ // 5a. Upload assets with bounded concurrency, dropping bytes after each
304
+ // successful upload (M3) to bound peak memory.
305
+ await uploadWithConcurrency(uploadQueue, RELEASE_ASSET_UPLOAD_CONCURRENCY, async (asset) => {
306
+ const stored = pendingBytes.get(asset.contentHash);
307
+ if (!stored)
308
+ return;
309
+ await client.uploadReleaseAsset(input.releaseVersionRef, asset.contentHash, stored.contentType, stored.bytes);
310
+ // M3: drop bytes immediately after upload.
311
+ pendingBytes.delete(asset.contentHash);
312
+ });
313
+ // B2. Routes: walk the full static import closure from each page entrypoint.
314
+ // Modules whose source is not in sourceByPath are recorded as closure gaps.
315
+ const routes = {};
316
+ const pageModules = Object.keys(modules).filter((p) => p.startsWith("pages/"));
317
+ for (const logicalPath of pageModules) {
318
+ const route = routeForPage(logicalPath);
319
+ if (!route)
320
+ continue;
321
+ const { modules: closureModules, gaps: closureGaps } = collectClosure([logicalPath], sourceByPath, knownPaths);
322
+ // Include only modules we actually have in the manifest (transformed +
323
+ // within size limit). Framework lib/* modules are excluded per contract
324
+ // (they are embedded by the runtime, not shipped as release assets).
325
+ const manifestedModules = closureModules.filter((m) => modules[m] !== undefined);
326
+ // Closure members not in the manifest (missing transforms, oversized, or
327
+ // framework-provided) are recorded as gaps for this route.
328
+ for (const missing of closureModules) {
329
+ if (modules[missing] === undefined && !missing.startsWith("lib/")) {
330
+ closureGaps.push(`route-gap:${route}:${missing}`);
331
+ }
332
+ }
333
+ if (closureGaps.length > 0) {
334
+ gaps.push(...closureGaps.filter((g) => !gaps.includes(g)));
335
+ }
336
+ routes[route] = { modules: manifestedModules, css: cssHashes };
337
+ }
338
+ // 6. Assemble and PUT the manifest.
339
+ const sourceContentHash = await sha256HexBytes(new TextEncoder().encode([...sourceByPath.keys()].sort().join("\n")));
340
+ const manifest = {
341
+ schemaVersion: RELEASE_ASSET_MANIFEST_SCHEMA_VERSION,
342
+ projectId: input.projectId,
343
+ releaseId: input.releaseId,
344
+ releaseVersion: input.releaseVersion,
345
+ // H2: use the manifest_version returned by begin, not a hardcoded 1.
346
+ manifestVersion,
347
+ builderVersion: VERSION,
348
+ sourceContentHash,
349
+ createdAt: new Date().toISOString(),
350
+ assetBasePath: RELEASE_ASSET_BASE_PATH,
351
+ modules: Object.fromEntries(Object.entries(modules).map(([path, entry]) => [path, {
352
+ contentHash: entry.contentHash,
353
+ size: entry.size,
354
+ contentType: entry.contentType,
355
+ }])),
356
+ css,
357
+ routes,
358
+ dependencies: {},
359
+ fallback: { mode: "jit", gaps },
360
+ };
361
+ const result = await client.putReleaseAssetManifest(input.releaseVersionRef, manifest);
362
+ logger.info("Release asset manifest built", {
363
+ releaseId: input.releaseId,
364
+ manifestVersion,
365
+ moduleCount: Object.keys(modules).length,
366
+ cssCount: css.length,
367
+ routeCount: Object.keys(routes).length,
368
+ state: result.state,
369
+ });
370
+ return {
371
+ success: true,
372
+ state: "ready",
373
+ moduleCount: Object.keys(modules).length,
374
+ cssCount: css.length,
375
+ routeCount: Object.keys(routes).length,
376
+ gaps,
377
+ };
378
+ }
379
+ /** Extract Tailwind class candidates from materialized source (best-effort). */
380
+ function collectClassCandidates(sourceByPath) {
381
+ const candidates = new Set();
382
+ const re = /class(?:Name)?\s*=\s*["'`]([^"'`]+)["'`]/g;
383
+ for (const source of sourceByPath.values()) {
384
+ let m;
385
+ while ((m = re.exec(source)) !== null) {
386
+ for (const cls of m[1].split(/\s+/)) {
387
+ if (cls)
388
+ candidates.add(cls);
389
+ }
390
+ }
391
+ }
392
+ return candidates;
393
+ }
394
+ /** Run an async task over items with a fixed concurrency limit. */
395
+ async function uploadWithConcurrency(items, concurrency, task) {
396
+ let index = 0;
397
+ async function worker() {
398
+ while (index < items.length) {
399
+ const current = index++;
400
+ await task(items[current]);
401
+ }
402
+ }
403
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
404
+ await Promise.all(workers);
405
+ }