veryfront 0.1.1100 → 0.1.1101

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 (42) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/routing/api/context-builder.d.ts +23 -1
  3. package/esm/src/routing/api/context-builder.d.ts.map +1 -1
  4. package/esm/src/routing/api/context-builder.js +22 -1
  5. package/esm/src/routing/api/handler.d.ts +11 -1
  6. package/esm/src/routing/api/handler.d.ts.map +1 -1
  7. package/esm/src/routing/api/handler.js +46 -12
  8. package/esm/src/routing/api/module-loader/loader.d.ts +5 -0
  9. package/esm/src/routing/api/module-loader/loader.d.ts.map +1 -1
  10. package/esm/src/routing/api/module-loader/loader.js +37 -2
  11. package/esm/src/server/dev-server/middleware.d.ts.map +1 -1
  12. package/esm/src/server/dev-server/middleware.js +35 -7
  13. package/esm/src/server/handlers/request/ssr/error-page-fallback.js +20 -3
  14. package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts +0 -11
  15. package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts.map +1 -1
  16. package/esm/src/server/handlers/request/ssr/ssr.handler.js +23 -1
  17. package/esm/src/transforms/esm/degraded-artifact.d.ts +15 -0
  18. package/esm/src/transforms/esm/degraded-artifact.d.ts.map +1 -0
  19. package/esm/src/transforms/esm/degraded-artifact.js +22 -0
  20. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  21. package/esm/src/transforms/esm/http-cache.js +47 -17
  22. package/esm/src/transforms/esm/import-parser.d.ts.map +1 -1
  23. package/esm/src/transforms/esm/import-parser.js +47 -1
  24. package/esm/src/transforms/esm/specifier-resolver.d.ts +21 -4
  25. package/esm/src/transforms/esm/specifier-resolver.d.ts.map +1 -1
  26. package/esm/src/transforms/esm/specifier-resolver.js +43 -23
  27. package/esm/src/transforms/import-rewriter/strategies/asset-strategy.d.ts +23 -0
  28. package/esm/src/transforms/import-rewriter/strategies/asset-strategy.d.ts.map +1 -0
  29. package/esm/src/transforms/import-rewriter/strategies/asset-strategy.js +49 -0
  30. package/esm/src/transforms/import-rewriter/strategies/index.d.ts +1 -0
  31. package/esm/src/transforms/import-rewriter/strategies/index.d.ts.map +1 -1
  32. package/esm/src/transforms/import-rewriter/strategies/index.js +1 -0
  33. package/esm/src/transforms/import-rewriter/unified-rewriter.d.ts.map +1 -1
  34. package/esm/src/transforms/import-rewriter/unified-rewriter.js +2 -0
  35. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts.map +1 -1
  36. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +2 -1
  37. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.d.ts +19 -0
  38. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.d.ts.map +1 -1
  39. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.js +23 -1
  40. package/esm/src/utils/version-constant.d.ts +1 -1
  41. package/esm/src/utils/version-constant.js +1 -1
  42. package/package.json +5 -5
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.1100",
3
+ "version": "0.1.1101",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "minimumDependencyAge": {
@@ -11,10 +11,32 @@ export interface APIContext {
11
11
  cookies: Record<string, string>;
12
12
  headers: Headers;
13
13
  url: URL;
14
- json: (data: unknown, init?: ResponseInit) => Response;
14
+ /**
15
+ * Read the request body as JSON, or build a JSON response.
16
+ *
17
+ * `ctx.json()` with no arguments parses and returns the request body.
18
+ * `ctx.json(data, init?)` returns a JSON `Response`.
19
+ */
20
+ json: {
21
+ (): Promise<unknown>;
22
+ (data: unknown, init?: ResponseInit): Response;
23
+ };
15
24
  text: (data: string, init?: ResponseInit) => Response;
16
25
  fs: FileSystemAdapter;
17
26
  }
27
+ /**
28
+ * Build the `ctx.json` helper for a request.
29
+ *
30
+ * Overloaded on arity. `ctx.json(data)` builds a response; `ctx.json()` reads
31
+ * the request body, which is what handlers reach for, and what the zero-arg
32
+ * form was previously misread as. It used to stringify `undefined` into a
33
+ * Response, so `await ctx.json()` yielded a Response object that serialised
34
+ * back out as `{}` and silently dropped every posted payload.
35
+ *
36
+ * Exported because the isolation Worker builds its own context and has to
37
+ * behave identically. Handlers must not care which one ran them.
38
+ */
39
+ export declare function createJsonHelper(request: Request): APIContext["json"];
18
40
  export declare function createContext(request: Request, match: RouteMatch, fs: FileSystemAdapter): APIContext;
19
41
  /**
20
42
  * @deprecated Use {@link flattenRouteParams} directly. Kept as a thin alias so
@@ -1 +1 @@
1
- {"version":3,"file":"context-builder.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/api/context-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,QAAQ,CAAC;IACvD,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,QAAQ,CAAC;IACtD,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAgBD,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,iBAAiB,GACpB,UAAU,CAmBZ;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAExB"}
1
+ {"version":3,"file":"context-builder.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/api/context-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC;IACT;;;;;OAKG;IACH,IAAI,EAAE;QACJ,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;KAChD,CAAC;IACF,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,QAAQ,CAAC;IACtD,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAgBD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAUrE;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,UAAU,EACjB,EAAE,EAAE,iBAAiB,GACpB,UAAU,CAmBZ;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAExB"}
@@ -10,9 +10,30 @@ function createResponse(body, contentType, init) {
10
10
  },
11
11
  });
12
12
  }
13
+ /**
14
+ * Build the `ctx.json` helper for a request.
15
+ *
16
+ * Overloaded on arity. `ctx.json(data)` builds a response; `ctx.json()` reads
17
+ * the request body, which is what handlers reach for, and what the zero-arg
18
+ * form was previously misread as. It used to stringify `undefined` into a
19
+ * Response, so `await ctx.json()` yielded a Response object that serialised
20
+ * back out as `{}` and silently dropped every posted payload.
21
+ *
22
+ * Exported because the isolation Worker builds its own context and has to
23
+ * behave identically. Handlers must not care which one ran them.
24
+ */
25
+ export function createJsonHelper(request) {
26
+ function json(...args) {
27
+ if (args.length === 0)
28
+ return request.json();
29
+ const [data, init] = args;
30
+ return createResponse(JSON.stringify(data), "application/json", init);
31
+ }
32
+ return json;
33
+ }
13
34
  export function createContext(request, match, fs) {
14
35
  const url = new URL(request.url);
15
- const json = (data, init) => createResponse(JSON.stringify(data), "application/json", init);
36
+ const json = createJsonHelper(request);
16
37
  const text = (data, init) => createResponse(data, "text/plain", init);
17
38
  return {
18
39
  request,
@@ -6,6 +6,17 @@ import { loadHandlerModule } from "./module-loader/loader.js";
6
6
  import { discoverAppRoutes, discoverPagesRoutes } from "./route-discovery.js";
7
7
  import type { HandlerContext } from "../../types/index.js";
8
8
  export type { APIContext, APIRoute };
9
+ /**
10
+ * Reduce a module load error to something safe to put in a response body.
11
+ *
12
+ * The message is worth returning, since it usually names a syntax or import
13
+ * error the developer can act on. The rest of it is not: a raw load error
14
+ * carries the temp directory the bundle was written to, absolute paths into the
15
+ * framework install, and a full stack trace. A path inside the project survives
16
+ * as a project-relative one, because that is the part that identifies the file
17
+ * to fix.
18
+ */
19
+ export declare function sanitizeLoadErrorForResponse(message: string, projectDir?: string): string;
9
20
  /**
10
21
  * Injection interface for testing APIRouteHandler dependencies
11
22
  */
@@ -31,7 +42,6 @@ export declare class APIRouteHandler {
31
42
  private projectDir;
32
43
  private router;
33
44
  private routeCache;
34
- private lastErrorMessage;
35
45
  private activeRequests;
36
46
  private destroyRequested;
37
47
  private destroyed;
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/api/handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAMlD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAErC;;GAEG;AACH,UAAU,mBAAmB;IAC3B,iBAAiB,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAC7C,mBAAmB,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACjD,iBAAiB,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAC7C,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;CAC9B;AAID;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAE3E;AAWD,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;AAE3E,qBAAa,eAAe;IAmBxB,OAAO,CAAC,UAAU;IAlBpB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,UAAU,CAA6E;IAC/F,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,cAAc,CAAwC;IAE9D,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,iBAAiB,CAA8B;IAEvD,OAAO,CAAC,MAAM,CAAsD;IACpE,OAAO,CAAC,aAAa,CAA8B;gBAGzC,UAAU,EAAE,MAAM,EAC1B,OAAO,CAAC,EAAE,cAAc;IAM1B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgD3B,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IA2FxE,OAAO,CAAC,WAAW;IAuCnB,UAAU,IAAI,IAAI;IAKlB,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,UAAU;YAQJ,aAAa;YAsBb,gBAAgB;YAOhB,cAAc;YAcd,YAAY;YAOZ,cAAc;CAW7B;AAED,OAAO,EACL,UAAU,EACV,SAAS,EACT,mBAAmB,IAAI,WAAW,EAClC,YAAY,IAAI,IAAI,EACpB,QAAQ,EACR,gBAAgB,IAAI,QAAQ,EAC5B,YAAY,GACb,MAAM,yCAAyC,CAAC"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/src/routing/api/handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAMlD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAYrC;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAYzF;AAED;;GAEG;AACH,UAAU,mBAAmB;IAC3B,iBAAiB,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAC7C,mBAAmB,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACjD,iBAAiB,CAAC,EAAE,OAAO,iBAAiB,CAAC;IAC7C,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;CAC9B;AAID;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAE3E;AAWD,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;AAW3E,qBAAa,eAAe;IAkBxB,OAAO,CAAC,UAAU;IAjBpB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,UAAU,CAA6E;IAC/F,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,cAAc,CAAwC;IAE9D,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,iBAAiB,CAA8B;IAEvD,OAAO,CAAC,MAAM,CAAsD;IACpE,OAAO,CAAC,aAAa,CAA8B;gBAGzC,UAAU,EAAE,MAAM,EAC1B,OAAO,CAAC,EAAE,cAAc;IAM1B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgD3B,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAwGxE,OAAO,CAAC,WAAW;IAsCnB,UAAU,IAAI,IAAI;IAKlB,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,UAAU;YAQJ,aAAa;YAsBb,gBAAgB;YAOhB,cAAc;YAcd,YAAY;YAOZ,cAAc;CAW7B;AAED,OAAO,EACL,UAAU,EACV,SAAS,EACT,mBAAmB,IAAI,WAAW,EAClC,YAAY,IAAI,IAAI,EACpB,QAAQ,EACR,gBAAgB,IAAI,QAAQ,EAC5B,YAAY,GACb,MAAM,yCAAyC,CAAC"}
@@ -12,6 +12,33 @@ import { executeAppRoute, executePagesRoute } from "./route-executor.js";
12
12
  import { withSpan } from "../../observability/tracing/otlp-setup.js";
13
13
  /** Max entries in the loaded-handler LRU cache */
14
14
  const HANDLER_CACHE_MAX_ENTRIES = 256;
15
+ /** Longest sanitised load error a response body carries. */
16
+ const MAX_LOAD_ERROR_LENGTH = 300;
17
+ /**
18
+ * Roots that only ever identify the machine running the server: home
19
+ * directories and the temp directories bundling writes to.
20
+ */
21
+ const MACHINE_PATH_PATTERN = /(?:file:\/\/)?\/(?:private\/)?(?:Users|home|root|var\/folders|var\/tmp|tmp)\/\S*/g;
22
+ /**
23
+ * Reduce a module load error to something safe to put in a response body.
24
+ *
25
+ * The message is worth returning, since it usually names a syntax or import
26
+ * error the developer can act on. The rest of it is not: a raw load error
27
+ * carries the temp directory the bundle was written to, absolute paths into the
28
+ * framework install, and a full stack trace. A path inside the project survives
29
+ * as a project-relative one, because that is the part that identifies the file
30
+ * to fix.
31
+ */
32
+ export function sanitizeLoadErrorForResponse(message, projectDir) {
33
+ // A stack trace is never the actionable part.
34
+ let text = (message.split("\n")[0] ?? "").trim();
35
+ if (projectDir) {
36
+ const root = projectDir.replace(/\/+$/, "");
37
+ text = text.replaceAll(`file://${root}/`, "").replaceAll(`${root}/`, "");
38
+ }
39
+ text = text.replace(MACHINE_PATH_PATTERN, "<PATH>");
40
+ return text.length > MAX_LOAD_ERROR_LENGTH ? `${text.slice(0, MAX_LOAD_ERROR_LENGTH)}...` : text;
41
+ }
15
42
  let injectedDeps = null;
16
43
  /**
17
44
  * Inject dependencies for testing. Pass null to reset to defaults.
@@ -31,7 +58,6 @@ export class APIRouteHandler {
31
58
  projectDir;
32
59
  router = new ApiRouteMatcher();
33
60
  routeCache = new LRUCache({ maxEntries: HANDLER_CACHE_MAX_ENTRIES });
34
- lastErrorMessage = null;
35
61
  activeRequests = 0;
36
62
  destroyRequested = false;
37
63
  destroyed = false;
@@ -116,18 +142,27 @@ export class APIRouteHandler {
116
142
  page: match.route.page,
117
143
  params: match.params,
118
144
  });
119
- const handler = await this.loadHandler(match);
145
+ const { handler, errorMessage } = await this.loadHandler(match);
120
146
  if (!handler) {
147
+ const msg = errorMessage ?? "Handler not found";
121
148
  try {
122
- logger.error(`handler module failed to load: ${match.route.page}`);
149
+ // The full detail, paths and all, belongs in the log.
150
+ logger.error(`handler module failed to load: ${match.route.page}`, { reason: msg });
123
151
  }
124
152
  catch (e) {
125
153
  logger.warn("API error log failed", e);
126
154
  }
127
- const msg = this.lastErrorMessage ?? "Handler not found";
128
155
  if (msg.includes("Remote import blocked by allow-list"))
129
156
  return badGateway(msg);
130
- return internalServerError("Handler not found");
157
+ // The reason the module failed to load is the only useful thing here.
158
+ // Reporting a flat "Handler not found" for what is usually a syntax or
159
+ // import error sends people hunting for a routing problem that does
160
+ // not exist. Local development only, and sanitised: a raw load error
161
+ // carries temp directories, absolute paths and a stack trace, none of
162
+ // which belong in a response body.
163
+ return internalServerError(ctx?.isLocalProject
164
+ ? sanitizeLoadErrorForResponse(msg, this.projectDir)
165
+ : "Handler not found");
131
166
  }
132
167
  // App Router routes are always named route.ts/js/tsx/jsx
133
168
  // Pages Router routes have descriptive names like articles.ts
@@ -156,7 +191,7 @@ export class APIRouteHandler {
156
191
  await this.ensureConfig(adapter);
157
192
  const cached = this.routeCache.get(modulePath);
158
193
  if (cached)
159
- return cached;
194
+ return { handler: cached, errorMessage: null };
160
195
  try {
161
196
  const deps = getDeps();
162
197
  const handler = await deps.loadHandlerModule({
@@ -168,16 +203,15 @@ export class APIRouteHandler {
168
203
  // Only cache handlers that export at least one HTTP method.
169
204
  // Empty objects ({}) from failed imports are truthy but useless —
170
205
  // caching them would prevent retry after the user fixes the import.
171
- if (handler && Object.keys(handler).length > 0) {
172
- this.routeCache.set(modulePath, handler);
173
- }
174
- return handler && Object.keys(handler).length > 0 ? handler : null;
206
+ const usable = handler && Object.keys(handler).length > 0 ? handler : null;
207
+ if (usable)
208
+ this.routeCache.set(modulePath, usable);
209
+ return { handler: usable, errorMessage: null };
175
210
  }
176
211
  catch (error) {
177
212
  const msg = error instanceof Error ? error.message : String(error);
178
- this.lastErrorMessage = msg;
179
213
  logger.error(`[API] Failed to load handler for ${modulePath}: ${msg}`);
180
- return null;
214
+ return { handler: null, errorMessage: msg };
181
215
  }
182
216
  }, { "api.modulePath": modulePath });
183
217
  }
@@ -2,4 +2,9 @@ import type { APIRoute, LoadModuleOptions } from "./types.js";
2
2
  export { generateCompiledBinaryRequireShim, getNodeExternalPackagesToResolve, loadVeryfrontExportsMap, resolveEsmUserDependencies, resolveNodePackageToFileUrl, rewriteCompiledBinaryUserDependencyImports, rewriteCompiledBinaryVeryfrontImports, rewriteDenoNodeBuiltinImports, rewriteDenoNpmDependencyImports, rewriteNodeExternalImports, } from "./external-import-rewriter.js";
3
3
  export { toCjsDestructureBindings } from "./loader-helpers.js";
4
4
  export declare function loadHandlerModule(options: LoadModuleOptions): Promise<APIRoute | null>;
5
+ /**
6
+ * True when a module failed to load because Deno could not resolve one of its
7
+ * import specifiers, rather than because the module itself is broken.
8
+ */
9
+ export declare function isSpecifierResolutionError(error: unknown): boolean;
5
10
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/routing/api/module-loader/loader.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAkB9D,OAAO,EACL,iCAAiC,EACjC,gCAAgC,EAChC,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,EAC3B,0CAA0C,EAC1C,qCAAqC,EACrC,6BAA6B,EAC7B,+BAA+B,EAC/B,0BAA0B,GAC3B,MAAM,+BAA+B,CAAC;AAIvC,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAyBtF"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/routing/api/module-loader/loader.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAkB9D,OAAO,EACL,iCAAiC,EACjC,gCAAgC,EAChC,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,EAC3B,0CAA0C,EAC1C,qCAAqC,EACrC,6BAA6B,EAC7B,+BAA+B,EAC/B,0BAA0B,GAC3B,MAAM,+BAA+B,CAAC;AAIvC,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAyBtF;AA0DD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIlE"}
@@ -45,8 +45,25 @@ async function loadModule(args) {
45
45
  return loadAndTranspileModule(modulePath, projectDir, adapter, fs, config);
46
46
  }
47
47
  const fileExistsLocally = await fs.exists(modulePath);
48
- if (fileExistsLocally)
49
- return loadTSModuleDirect(modulePath);
48
+ if (fileExistsLocally) {
49
+ try {
50
+ return await loadTSModuleDirect(modulePath);
51
+ }
52
+ catch (error) {
53
+ // A direct import shares the dev server's runtime context, which is what
54
+ // makes auto-discovery (agentRegistry and friends) work — but it leaves
55
+ // specifier resolution to Deno, which knows nothing about the project's
56
+ // `@/` alias. Bundling can resolve that import map path, so fall back to
57
+ // it rather than reporting a routing-shaped 500.
58
+ if (!isSpecifierResolutionError(error))
59
+ throw error;
60
+ logger.debug("Direct import could not resolve a specifier, bundling instead", {
61
+ modulePath,
62
+ error: error instanceof Error ? error.message : String(error),
63
+ });
64
+ return loadAndTranspileModule(modulePath, projectDir, adapter, fs, config);
65
+ }
66
+ }
50
67
  logger.debug(`File not local, using adapter-based loading: ${modulePath}`);
51
68
  return loadAndTranspileModule(modulePath, projectDir, adapter, fs, config);
52
69
  }
@@ -55,6 +72,24 @@ async function loadModule(args) {
55
72
  * This allows the module to share the same runtime context as the dev server,
56
73
  * enabling auto-discovery features like agentRegistry to work.
57
74
  */
75
+ /**
76
+ * Deno's resolver reports an unresolvable import as a TypeError whose message
77
+ * opens by naming the specifier it could not resolve. Anchoring on that opening
78
+ * is what keeps a module's own error out: a route that throws
79
+ * `Error("Cannot find module x")` at evaluation time is broken, and re-running
80
+ * it under bundling would evaluate broken code a second time.
81
+ */
82
+ const SPECIFIER_RESOLUTION_MESSAGE = /^(?:Import "[^"]+" not a dependency|Module not found "[^"]+"|Relative import path "[^"]+" not prefixed)/;
83
+ /**
84
+ * True when a module failed to load because Deno could not resolve one of its
85
+ * import specifiers, rather than because the module itself is broken.
86
+ */
87
+ export function isSpecifierResolutionError(error) {
88
+ // Every other error shape the direct import can produce belongs to the module.
89
+ if (!(error instanceof TypeError))
90
+ return false;
91
+ return SPECIFIER_RESOLUTION_MESSAGE.test(error.message.trimStart());
92
+ }
58
93
  function loadTSModuleDirect(modulePath) {
59
94
  const cacheBuster = `?v=${Date.now()}`;
60
95
  const url = modulePath.startsWith("file://")
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../../src/src/server/dev-server/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM7D,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAEnD,UAAU,qBAAqB;IAC7B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAkFD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CA6B/B;AAkFD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,eAAe,EACvB,cAAc,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAClD,OAAO,CAAC,IAAI,CAAC,CASf"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../../src/src/server/dev-server/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM7D,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAEnD,UAAU,qBAAqB;IAC7B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAkFD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAkC/B;AAqHD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,eAAe,EACvB,cAAc,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAClD,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -86,11 +86,11 @@ export async function loadMiddlewareFile(projectDir, adapter, options = {}) {
86
86
  try {
87
87
  logger.debug(`Loading ${middlewareFile}`);
88
88
  if (isVirtualFilesystem(adapter.fs)) {
89
- return await loadMiddlewareFromVirtualFS(middlewarePath, adapter, options.throwOnError === true);
89
+ return await loadMiddlewareFromVirtualFS(middlewarePath, adapter, options.throwOnError === true, middlewareFile);
90
90
  }
91
91
  const middlewareUrl = `file://${middlewarePath}?t=${Date.now()}-${dntShim.crypto.randomUUID()}`;
92
92
  const middlewareModule = await import(middlewareUrl);
93
- return normalizeMiddlewareExport(middlewareModule, options.throwOnError === true);
93
+ return normalizeMiddlewareExport(middlewareModule, options.throwOnError === true, middlewareFile);
94
94
  }
95
95
  catch (error) {
96
96
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -101,7 +101,7 @@ export async function loadMiddlewareFile(projectDir, adapter, options = {}) {
101
101
  }
102
102
  return [];
103
103
  }
104
- async function loadMiddlewareFromVirtualFS(middlewarePath, adapter, strictExport) {
104
+ async function loadMiddlewareFromVirtualFS(middlewarePath, adapter, strictExport, sourceFile = "middleware.ts") {
105
105
  const fs = createFileSystem();
106
106
  const content = await adapter.fs.readFile(middlewarePath);
107
107
  const source = typeof content === "string" ? content : new TextDecoder().decode(content);
@@ -130,20 +130,48 @@ async function loadMiddlewareFromVirtualFS(middlewarePath, adapter, strictExport
130
130
  try {
131
131
  await fs.writeTextFile(tempFile, js);
132
132
  const middlewareModule = await import(`file://${tempFile}?v=${Date.now()}`);
133
- return normalizeMiddlewareExport(middlewareModule, strictExport);
133
+ return normalizeMiddlewareExport(middlewareModule, strictExport, sourceFile);
134
134
  }
135
135
  finally {
136
136
  await fs.remove(tempDir, { recursive: true });
137
137
  }
138
138
  }
139
- function normalizeMiddlewareExport(middlewareModule, strict = false) {
139
+ /**
140
+ * Explain what a root middleware file must export. When the module looks like it
141
+ * was written for Next.js, say so, because that is overwhelmingly why this
142
+ * fails. A bad root middleware takes down every route, so the message has
143
+ * to be enough to fix the file without reading the source.
144
+ */
145
+ function invalidMiddlewareExport(middlewareModule, sourceFile) {
146
+ const named = middlewareModule && typeof middlewareModule === "object"
147
+ ? Object.keys(middlewareModule)
148
+ : [];
149
+ const looksLikeNext = named.includes("middleware") &&
150
+ typeof middlewareModule.middleware === "function";
151
+ const detail = looksLikeNext
152
+ ? `Found a named "middleware" export, which is the Next.js convention. ` +
153
+ `Veryfront expects a default export, and its middleware receives ` +
154
+ `(c, next), where c is a context carrying c.req, not the Request itself.`
155
+ : named.length > 0
156
+ ? `Found export(s): ${named.join(", ")}.`
157
+ : `The module has no usable export.`;
158
+ return new TypeError(`Invalid middleware export in ${sourceFile}. ${detail}\n` +
159
+ `Expected a default export that is a middleware function, or a non-empty ` +
160
+ `array of them:\n\n` +
161
+ ` export default async function middleware(c, next) {\n` +
162
+ ` const response = await next();\n` +
163
+ ` return response;\n` +
164
+ ` }\n\n` +
165
+ `See docs/guides/middleware.md.`);
166
+ }
167
+ function normalizeMiddlewareExport(middlewareModule, strict = false, sourceFile = "middleware.ts") {
140
168
  const exported = middlewareModule && typeof middlewareModule === "object" &&
141
169
  "default" in middlewareModule
142
170
  ? middlewareModule.default
143
171
  : middlewareModule;
144
172
  if (Array.isArray(exported)) {
145
173
  if (strict && (exported.length === 0 || exported.some((value) => typeof value !== "function"))) {
146
- throw new TypeError("Invalid middleware export: expected a function or non-empty array of functions");
174
+ throw invalidMiddlewareExport(middlewareModule, sourceFile);
147
175
  }
148
176
  return exported.filter((middleware) => typeof middleware === "function");
149
177
  }
@@ -151,7 +179,7 @@ function normalizeMiddlewareExport(middlewareModule, strict = false) {
151
179
  return [exported];
152
180
  }
153
181
  if (strict) {
154
- throw new TypeError("Invalid middleware export: expected a function or non-empty array of functions");
182
+ throw invalidMiddlewareExport(middlewareModule, sourceFile);
155
183
  }
156
184
  return [];
157
185
  }
@@ -78,6 +78,23 @@ async function setCachedPath(cacheKey, path) {
78
78
  }
79
79
  errorPagePathCache.set(cacheKey, path);
80
80
  }
81
+ /**
82
+ * Whether a "this project has no error page" answer may be cached.
83
+ *
84
+ * In dev it may not. Nothing invalidates this cache when the filesystem
85
+ * changes, so caching the miss means a project that adds `pages/500.tsx` while
86
+ * the server is running keeps getting the dev overlay until restart, with no
87
+ * indication why. A miss costs one file probe on an error render, which is not
88
+ * a path worth optimising in dev.
89
+ */
90
+ function canCacheMiss(ctx) {
91
+ return !ctx.isLocalProject;
92
+ }
93
+ async function setCachedMiss(cacheKey, ctx) {
94
+ if (!canCacheMiss(ctx))
95
+ return;
96
+ await setCachedPath(cacheKey, null);
97
+ }
81
98
  async function deleteCachedPath(cacheKey) {
82
99
  if (injectedCacheRepo) {
83
100
  await injectedCacheRepo.delete(cacheKey);
@@ -104,7 +121,7 @@ async function tryLoadErrorPage(pagesDir, pageType, ctx, reactVersion) {
104
121
  try {
105
122
  const resolvedPath = await ctx.adapter.fs.resolveFile(basePath);
106
123
  if (!resolvedPath) {
107
- await setCachedPath(cacheKey, null);
124
+ await setCachedMiss(cacheKey, ctx);
108
125
  return null;
109
126
  }
110
127
  const fullPath = joinPath(ctx.projectDir, resolvedPath);
@@ -117,7 +134,7 @@ async function tryLoadErrorPage(pagesDir, pageType, ctx, reactVersion) {
117
134
  catch (_) {
118
135
  // expected: resolveFile may fail, fall through to extension probing
119
136
  }
120
- await setCachedPath(cacheKey, null);
137
+ await setCachedMiss(cacheKey, ctx);
121
138
  return null;
122
139
  }
123
140
  for (const ext of ERROR_PAGE_EXTENSIONS) {
@@ -136,7 +153,7 @@ async function tryLoadErrorPage(pagesDir, pageType, ctx, reactVersion) {
136
153
  // expected: file with this extension doesn't exist
137
154
  }
138
155
  }
139
- await setCachedPath(cacheKey, null);
156
+ await setCachedMiss(cacheKey, ctx);
140
157
  return null;
141
158
  }
142
159
  async function loadErrorComponent(filePath, ctx, reactVersion) {
@@ -15,17 +15,6 @@ import { type SSRServiceLike } from "../../../services/rendering/ssr.service.js"
15
15
  * Config override (PRODUCTION_MODE) takes precedence.
16
16
  */
17
17
  export declare function isProductionMode(ctx: HandlerContext, _url?: URL): boolean;
18
- /**
19
- * SSR Handler - Thin orchestration layer
20
- *
21
- * Responsibilities:
22
- * - Route matching and quick rejections
23
- * - Multi-project context setup
24
- * - Response building with headers
25
- * - Custom fallback handling (not-found, error pages)
26
- *
27
- * Business logic is delegated to SSRService.
28
- */
29
18
  export declare class SSRHandler extends BaseHandler {
30
19
  metadata: HandlerMetadata;
31
20
  private ssrService;
@@ -1 +1 @@
1
- {"version":3,"file":"ssr.handler.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/request/ssr/ssr.handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAEf,aAAa,EACd,MAAM,gBAAgB,CAAC;AAWxB,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,4CAA4C,CAAC;AAMpD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAKzE;AAED;;;;;;;;;;GAUG;AACH,qBAAa,UAAW,SAAQ,WAAW;IACzC,QAAQ,EAAE,eAAe,CAIvB;IAEF,OAAO,CAAC,UAAU,CAAiB;gBAEvB,UAAU,CAAC,EAAE,cAAc;IAKvC,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IA8BjE,OAAO,CAAC,qBAAqB;IAmF7B,OAAO,CAAC,iBAAiB;IA8DzB,OAAO,CAAC,cAAc;YAgBR,cAAc;YA6Bd,sBAAsB;YAgBtB,aAAa;CAU5B"}
1
+ {"version":3,"file":"ssr.handler.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/request/ssr/ssr.handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAEf,aAAa,EACd,MAAM,gBAAgB,CAAC;AAWxB,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,4CAA4C,CAAC;AAOpD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAKzE;AA+BD,qBAAa,UAAW,SAAQ,WAAW;IACzC,QAAQ,EAAE,eAAe,CAIvB;IAEF,OAAO,CAAC,UAAU,CAAiB;gBAEvB,UAAU,CAAC,EAAE,cAAc;IAKvC,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IA8BjE,OAAO,CAAC,qBAAqB;IAmF7B,OAAO,CAAC,iBAAiB;IAkEzB,OAAO,CAAC,cAAc;YAgBR,cAAc;YA6Bd,sBAAsB;YAgBtB,aAAa;CAU5B"}
@@ -19,6 +19,7 @@ import { tryNotFoundFallback } from "./not-found-fallback.js";
19
19
  import { tryErrorPageFallback } from "./error-page-fallback.js";
20
20
  import { SSRService, } from "../../../services/rendering/ssr.service.js";
21
21
  import { ErrorPages } from "../../../utils/error-html.js";
22
+ import { VeryfrontError } from "../../../../errors/index.js";
22
23
  import { buildSSRResponse } from "./ssr-response-builder.js";
23
24
  const logger = serverLogger.component("ssr");
24
25
  /**
@@ -43,6 +44,23 @@ export function isProductionMode(ctx, _url) {
43
44
  *
44
45
  * Business logic is delegated to SSRService.
45
46
  */
47
+ /**
48
+ * True for errors raised while compiling or resolving project source, as
49
+ * opposed to errors thrown by the running application.
50
+ *
51
+ * Module-load failures arrive wrapped in a RUNTIME-category `render-error`,
52
+ * which loses the original category, so they are identified by their
53
+ * `criticalFailures` context instead: a page module that could not be *loaded*
54
+ * always failed to compile or resolve.
55
+ */
56
+ function isBuildError(error) {
57
+ if (!(error instanceof VeryfrontError))
58
+ return false;
59
+ if (error.category === "BUILD" || error.category === "MODULE")
60
+ return true;
61
+ const context = error.context;
62
+ return Array.isArray(context?.criticalFailures) && context.criticalFailures.length > 0;
63
+ }
46
64
  export class SSRHandler extends BaseHandler {
47
65
  metadata = {
48
66
  name: "SSRHandler",
@@ -182,7 +200,11 @@ export class SSRHandler extends BaseHandler {
182
200
  if (result.errorType === "not-found") {
183
201
  return this.handleNotFound(req, ctx, slug, nonce);
184
202
  }
185
- if (result.errorType === "server-error" && !result.showDevOverlay) {
203
+ const isServerError = result.errorType === "server-error" ||
204
+ result.errorType === "runtime";
205
+ // Project error pages should beat the dev overlay for runtime errors.
206
+ // Build/import errors stay visible because their overlay is actionable.
207
+ if (isServerError && !(result.showDevOverlay && isBuildError(result.error))) {
186
208
  const customResponse = await this.tryCustomErrorFallback(req, ctx, result, nonce);
187
209
  if (customResponse)
188
210
  return customResponse;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Degradation marking for cached HTTP module artifacts.
3
+ *
4
+ * A module whose lazy dependency could not be prefetched is still usable for
5
+ * the render that produced it, but it must never be mistaken for a complete
6
+ * artifact on a later render. The marker travels with the code, so any reader
7
+ * of the on-disk cache can tell the two apart.
8
+ *
9
+ * @module transforms/esm/degraded-artifact
10
+ */
11
+ /** Mark bundle code as produced from an incomplete dependency prefetch. */
12
+ export declare function markDegradedArtifact(code: string): string;
13
+ /** Return whether bundle code was produced from an incomplete prefetch. */
14
+ export declare function isDegradedArtifact(code: string): boolean;
15
+ //# sourceMappingURL=degraded-artifact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"degraded-artifact.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/degraded-artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Degradation marking for cached HTTP module artifacts.
3
+ *
4
+ * A module whose lazy dependency could not be prefetched is still usable for
5
+ * the render that produced it, but it must never be mistaken for a complete
6
+ * artifact on a later render. The marker travels with the code, so any reader
7
+ * of the on-disk cache can tell the two apart.
8
+ *
9
+ * @module transforms/esm/degraded-artifact
10
+ */
11
+ /** Preserved comment format that survives minification */
12
+ const VF_DEGRADED_MARKER = "\n/*! @vf-degraded */\n";
13
+ /** Mark bundle code as produced from an incomplete dependency prefetch. */
14
+ export function markDegradedArtifact(code) {
15
+ if (isDegradedArtifact(code))
16
+ return code;
17
+ return `${code}${VF_DEGRADED_MARKER}`;
18
+ }
19
+ /** Return whether bundle code was produced from an incomplete prefetch. */
20
+ export function isDegradedArtifact(code) {
21
+ return code.endsWith(VF_DEGRADED_MARKER);
22
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"http-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/http-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAGL,KAAK,YAAY,EAKjB,wBAAwB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAElB,gBAAgB,EAEhB,KAAK,OAAO,EACb,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAA2B,MAAM,4BAA4B,CAAC;AACxF,OAAO,EACL,0BAA0B,EAO3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EAKvB,MAAM,uBAAuB,CAAC;AAe/B,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,GACnB,CAAC;AAGF,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAsRlC,wEAAwE;AACxE,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,sBAAsB,CAAC,CAkCjC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,wBAAwB,GAClC,OAAO,CAAC,OAAO,CAAC,CAElB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,wBAAwB,GAClC,OAAO,CAAC,MAAM,EAAE,CAAC,CAEnB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErF;AAGD,eAAO,MAAM,wBAAwB,0BAAoB,CAAC"}
1
+ {"version":3,"file":"http-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/http-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzE,OAAO,EAGL,KAAK,YAAY,EAKjB,wBAAwB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAElB,gBAAgB,EAEhB,KAAK,OAAO,EACb,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAA2B,MAAM,4BAA4B,CAAC;AACxF,OAAO,EACL,0BAA0B,EAO3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EAKvB,MAAM,uBAAuB,CAAC;AAe/B,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,GACnB,CAAC;AAGF,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAwTlC,wEAAwE;AACxE,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,sBAAsB,CAAC,CAkCjC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,wBAAwB,GAClC,OAAO,CAAC,OAAO,CAAC,CAElB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,wBAAwB,GAClC,OAAO,CAAC,MAAM,EAAE,CAAC,CAEnB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErF;AAGD,eAAO,MAAM,wBAAwB,0BAAoB,CAAC"}
@@ -23,6 +23,7 @@ import { CACHE_DIR_TOKEN, detokenizeAllCachePaths, detokenizeCachePaths, tokeniz
23
23
  import { looksLikeHtmlContent as looksLikeHtmlNotJs } from "./html-content.js";
24
24
  // Extracted modules
25
25
  import { embedSourceUrl, extractSourceUrl } from "./source-url-embed.js";
26
+ import { isDegradedArtifact, markDegradedArtifact } from "./degraded-artifact.js";
26
27
  import { buildHttpCacheIdentity, buildHttpCacheIdentityMetadata, ensureAbsoluteDir, ensurePreparedHttpCacheRequestOptions, getEffectiveHttpCacheRequest, hashHttpCacheIdentity, hasIncompatibleFilePaths, isHttpUrl, normalizeHttpUrl, prepareHttpCacheRequestOptions, } from "./http-cache-helpers.js";
27
28
  import { extractBundleDeps, validateBundleDepsExist } from "./bundle-deps-validator.js";
28
29
  import { __clearInFlightHttpFetches, bundleAccumulatorStorage, inFlightHttpFetches, processingStackStorage, refreshDistributedCacheAsync, trackBundleAccumulator, waitForInFlightFetch, } from "./in-flight-manager.js";
@@ -56,15 +57,32 @@ async function cacheHttpModuleInternal(url, options) {
56
57
  const fs = createFileSystem();
57
58
  if (await exists(cachePath)) {
58
59
  const code = await fs.readTextFile(cachePath);
59
- const deps = extractBundleDeps(code);
60
- if (deps.length > 0) {
61
- const depsValid = await validateBundleDepsExist(deps, cacheDir);
62
- if (!depsValid) {
63
- httpCacheLog.debug("Local cache has missing deps, will re-fetch", {
64
- url: normalizedUrl,
65
- hash,
66
- missingDeps: deps.length,
67
- });
60
+ if (isDegradedArtifact(code)) {
61
+ // The artifact on disk is the fallback a previous render wrote when a
62
+ // dependency could not be prefetched. Retry the prefetch instead of
63
+ // handing the degradation on.
64
+ httpCacheLog.debug("Local cache holds a degraded artifact, will re-fetch", {
65
+ url: normalizedUrl,
66
+ hash,
67
+ });
68
+ }
69
+ else {
70
+ const deps = extractBundleDeps(code);
71
+ if (deps.length > 0) {
72
+ const depsValid = await validateBundleDepsExist(deps, cacheDir);
73
+ if (!depsValid) {
74
+ httpCacheLog.debug("Local cache has missing deps, will re-fetch", {
75
+ url: normalizedUrl,
76
+ hash,
77
+ missingDeps: deps.length,
78
+ });
79
+ }
80
+ else {
81
+ getCachedPaths().set(cacheKey, cachePath);
82
+ refreshDistributedCacheAsync(hash, code, cacheDir, normalizedUrl, identityMetadata, getLastDistributedRefresh);
83
+ trackBundleAccumulator(hash, normalizedUrl, cachePath);
84
+ return cachePath;
85
+ }
68
86
  }
69
87
  else {
70
88
  getCachedPaths().set(cacheKey, cachePath);
@@ -73,12 +91,6 @@ async function cacheHttpModuleInternal(url, options) {
73
91
  return cachePath;
74
92
  }
75
93
  }
76
- else {
77
- getCachedPaths().set(cacheKey, cachePath);
78
- refreshDistributedCacheAsync(hash, code, cacheDir, normalizedUrl, identityMetadata, getLastDistributedRefresh);
79
- trackBundleAccumulator(hash, normalizedUrl, cachePath);
80
- return cachePath;
81
- }
82
94
  }
83
95
  const processingStack = getProcessingStack();
84
96
  if (processingStack.has(cacheIdentity)) {
@@ -198,13 +210,18 @@ async function cacheHttpModuleInternal(url, options) {
198
210
  });
199
211
  }
200
212
  processingStack.add(cacheIdentity);
213
+ let degraded = [];
201
214
  try {
202
- code = await rewriteModuleImports(code, normalizedUrl, options, cacheHttpModule);
215
+ const rewritten = await rewriteModuleImports(code, normalizedUrl, options, cacheHttpModule);
216
+ code = rewritten.code;
217
+ degraded = rewritten.degraded;
203
218
  }
204
219
  finally {
205
220
  processingStack.delete(cacheIdentity);
206
221
  }
207
222
  code = embedSourceUrl(code, normalizedUrl);
223
+ if (degraded.length > 0)
224
+ code = markDegradedArtifact(code);
208
225
  await fs.mkdir(cacheDir, { recursive: true });
209
226
  await fs.writeTextFile(cachePath, code);
210
227
  if (!(await exists(cachePath))) {
@@ -212,6 +229,19 @@ async function cacheHttpModuleInternal(url, options) {
212
229
  detail: `[HTTP-CACHE] INVARIANT VIOLATION: File write succeeded but file does not exist: ${cachePath}`,
213
230
  });
214
231
  }
232
+ if (degraded.length > 0) {
233
+ // The file on disk carries this render through, but the artifact is not
234
+ // the one this URL is supposed to produce. Keeping it out of the
235
+ // distributed cache and the in-memory path map means the next render
236
+ // retries the prefetch instead of inheriting one upstream blip for the
237
+ // lifetime of the distributed entry.
238
+ httpCacheLog.warn("Not caching a module with unresolved dynamic imports", {
239
+ url: normalizedUrl,
240
+ hash,
241
+ degraded,
242
+ });
243
+ return cachePath;
244
+ }
215
245
  try {
216
246
  await httpBundleCache.setCode(String(hash), asLocalModuleCode(code), normalizedUrl, HTTP_MODULE_DISTRIBUTED_TTL_SEC, identityMetadata);
217
247
  }
@@ -256,7 +286,7 @@ async function cacheHttpModule(url, options) {
256
286
  export function cacheHttpImportsToLocal(code, options) {
257
287
  const requestOptions = prepareHttpCacheRequestOptions(options);
258
288
  return bundleAccumulatorStorage.run([], async () => {
259
- const replacements = await buildReplacements(code, undefined, requestOptions, cacheHttpModule);
289
+ const { replacements } = await buildReplacements(code, undefined, requestOptions, cacheHttpModule);
260
290
  if (replacements.size === 0)
261
291
  return { code };
262
292
  httpCacheLog.debug("Cached HTTP imports", { count: replacements.size });
@@ -1 +1 @@
1
- {"version":3,"file":"import-parser.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/import-parser.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAKtE,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;IAC1C,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAKD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,uBAAuB,CAAC,CAiFlC"}
1
+ {"version":3,"file":"import-parser.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/import-parser.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAKtE,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;IAC1C,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAKD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,uBAAuB,CAAC,CA0HlC"}
@@ -1,3 +1,4 @@
1
+ import { compileContent } from "../mdx/compiler/index.js";
1
2
  import { getEsbuild } from "../../platform/compat/esbuild.js";
2
3
  import { join } from "../../platform/compat/path/index.js";
3
4
  import { createFileSystem } from "../../platform/compat/fs.js";
@@ -11,8 +12,18 @@ export async function parseLocalImports(code, filePath, projectDir, adapter) {
11
12
  if (filePath.endsWith(".css") || filePath.endsWith(".json")) {
12
13
  return { imports: [], cssImports: [], crossProjectImports: [], missing: [] };
13
14
  }
15
+ // MDX/Markdown is not JSX, so handing the raw source to esbuild under the
16
+ // `jsx` loader fails with "<stdin>:1:1: ERROR: Syntax error" — which surfaced
17
+ // to users as "Component has missing dependencies" for a file that exists.
18
+ // Compile content to JSX first, exactly as the transform pipeline's parse
19
+ // stage does, then read the imports out of that.
20
+ let parseSource = code;
21
+ if (/\.mdx?$/i.test(filePath)) {
22
+ const compiled = await compileContent("development", projectDir, code, undefined, filePath, "server");
23
+ parseSource = compiled.compiledCode;
24
+ }
14
25
  const esbuild = await getEsbuild();
15
- const result = await esbuild.transform(code, {
26
+ const result = await esbuild.transform(parseSource, {
16
27
  loader: getLoaderFromPath(filePath),
17
28
  format: "esm",
18
29
  target: "esnext",
@@ -32,6 +43,28 @@ export async function parseLocalImports(code, filePath, projectDir, adapter) {
32
43
  const specifier = imp.n;
33
44
  if (!specifier)
34
45
  continue;
46
+ // The content compile above runs with the "server" target, which rewrites a
47
+ // relative specifier to an absolute `file://` URL before the lexer ever
48
+ // sees it. Without this branch those dependencies match none of the shapes
49
+ // below and are dropped without even being reported as missing, so an MDX
50
+ // file's sibling components are never recursively transformed.
51
+ if (specifier.startsWith("file://")) {
52
+ const absolutePath = fileUrlToPath(specifier);
53
+ if (absolutePath && await checkFileExists(absolutePath, adapter)) {
54
+ const entry = { specifier, absolutePath };
55
+ if (absolutePath.endsWith(".css"))
56
+ cssImports.push(entry);
57
+ else
58
+ localImports.push(entry);
59
+ continue;
60
+ }
61
+ missingImports.push({
62
+ specifier,
63
+ fromFile: filePath,
64
+ reason: `File not found: ${absolutePath ?? specifier}`,
65
+ });
66
+ continue;
67
+ }
35
68
  if (specifier.startsWith("./") || specifier.startsWith("../")) {
36
69
  const resolved = await resolveLocalImportPath(filePath, specifier, adapter);
37
70
  if (resolved) {
@@ -83,6 +116,19 @@ export async function parseLocalImports(code, filePath, projectDir, adapter) {
83
116
  }
84
117
  return { imports: localImports, cssImports, crossProjectImports, missing: missingImports };
85
118
  }
119
+ /** Filesystem path behind a `file://` specifier, or null when it is not one. */
120
+ function fileUrlToPath(specifier) {
121
+ try {
122
+ const url = new URL(specifier);
123
+ if (url.protocol !== "file:")
124
+ return null;
125
+ return decodeURIComponent(url.pathname);
126
+ }
127
+ catch (_) {
128
+ /* expected: not a well-formed URL */
129
+ return null;
130
+ }
131
+ }
86
132
  async function checkFileExists(path, adapter) {
87
133
  try {
88
134
  const fs = adapter?.fs.stat ? adapter.fs : createFileSystem();
@@ -9,15 +9,32 @@
9
9
  import { type CacheOptions } from "./http-cache-helpers.js";
10
10
  /** Function signature for caching an HTTP module and returning its local path. */
11
11
  export type CacheHttpModuleFn = (url: string, options: CacheOptions) => Promise<string | null>;
12
+ /** Specifier replacements plus the specifiers that were left unresolved. */
13
+ export interface SpecifierReplacements {
14
+ readonly replacements: ReadonlyMap<string, string>;
15
+ /** Specifiers left in place because prefetching them failed. */
16
+ readonly degraded: readonly string[];
17
+ }
18
+ /** Rewritten module code plus the specifiers that were left unresolved. */
19
+ export interface RewrittenModule {
20
+ readonly code: string;
21
+ /** Specifiers left in place because prefetching them failed. */
22
+ readonly degraded: readonly string[];
23
+ }
12
24
  /**
13
25
  * Build a map of specifier replacements by resolving all imports in the code.
14
26
  *
15
- * Resolution failure is fatal for a static import and best-effort for a
16
- * specifier only ever used in `import(...)`. See {@link isDynamicOnly}.
27
+ * Resolution failure is fatal unless the runtime can resolve the specifier on
28
+ * its own. See {@link runtimeResolvableSpecifiers}. Every specifier left in
29
+ * place is reported as degraded so callers can decide whether the resulting
30
+ * code is fit to cache.
17
31
  */
18
- export declare function buildReplacements(code: string, baseUrl: string | undefined, options: CacheOptions, cacheHttpModule: CacheHttpModuleFn): Promise<Map<string, string>>;
32
+ export declare function buildReplacements(code: string, baseUrl: string | undefined, options: CacheOptions, cacheHttpModule: CacheHttpModuleFn): Promise<SpecifierReplacements>;
19
33
  /**
20
34
  * Rewrite all HTTP/npm/bare import specifiers in module code to local cached paths.
35
+ *
36
+ * Reports any specifier left in place, so the caller can keep the resulting
37
+ * code out of the caches that outlive this render.
21
38
  */
22
- export declare function rewriteModuleImports(code: string, moduleUrl: string, options: CacheOptions, cacheHttpModule: CacheHttpModuleFn): Promise<string>;
39
+ export declare function rewriteModuleImports(code: string, moduleUrl: string, options: CacheOptions, cacheHttpModule: CacheHttpModuleFn): Promise<RewrittenModule>;
23
40
  //# sourceMappingURL=specifier-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"specifier-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/specifier-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,EACL,KAAK,YAAY,EAQlB,MAAM,yBAAyB,CAAC;AAEjC,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AA4G/F;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE,YAAY,EACrB,eAAe,EAAE,iBAAiB,GACjC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoC9B;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,YAAY,EACrB,eAAe,EAAE,iBAAiB,GACjC,OAAO,CAAC,MAAM,CAAC,CAKjB"}
1
+ {"version":3,"file":"specifier-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/specifier-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,EACL,KAAK,YAAY,EAQlB,MAAM,yBAAyB,CAAC;AAEjC,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAoH/F,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnD,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE,YAAY,EACrB,eAAe,EAAE,iBAAiB,GACjC,OAAO,CAAC,qBAAqB,CAAC,CAuChC;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,YAAY,EACrB,eAAe,EAAE,iBAAiB,GACjC,OAAO,CAAC,eAAe,CAAC,CAa1B"}
@@ -83,21 +83,26 @@ async function resolveSpecifier(specifier, baseUrl, options, cacheHttpModule) {
83
83
  return resolveSpecifier(mapped, baseUrl, options, cacheHttpModule);
84
84
  }
85
85
  /**
86
- * Specifiers this module only ever reaches through `import(...)`.
86
+ * Specifiers the runtime can still resolve on its own if prefetching fails.
87
87
  *
88
- * The distinction decides what a resolution failure means. A static import is
89
- * part of the emitted module's own import graph, so the artifact contract holds
90
- * for it: every static dependency resolves to a local path before the module is
91
- * handed to the runtime loader, and a failure to do that is fatal, exactly as
92
- * it was before graceful degradation existed.
88
+ * Two conditions must hold together. The specifier must be reached only
89
+ * through `import(...)`, because a static import is part of the emitted
90
+ * module's own import graph: every static dependency resolves to a local path
91
+ * before the module is handed to the runtime loader, and a failure to do that
92
+ * is fatal, exactly as it was before graceful degradation existed. A dynamic
93
+ * specifier is resolved by the runtime at call time and is routinely guarded by
94
+ * the caller (`platform/adapters/redis/modules.js` only calls
95
+ * `await import("redis")` when the redis adapter is actually used), so failing
96
+ * to prefetch it leaves the specifier in place rather than taking down a render
97
+ * that would never have imported it.
93
98
  *
94
- * A dynamic specifier is resolved by the runtime at call time and is routinely
95
- * guarded by the caller (`platform/adapters/redis/modules.js` only calls
96
- * `await import("redis")` when the redis adapter is actually used). Pre-fetching
97
- * it is an optimisation, so failing to pre-fetch it leaves the specifier in
98
- * place rather than taking down a render that would never have imported it.
99
+ * The specifier must also be an absolute http(s) URL, because that is the only
100
+ * form the runtime can resolve without the transform. A relative specifier left
101
+ * in place resolves against the local bundle cache directory, where the chunk
102
+ * was never written; `npm:` and bare specifiers need the import map the cached
103
+ * module no longer carries. Those failures stay fatal.
99
104
  */
100
- function isDynamicOnly(imports) {
105
+ function runtimeResolvableSpecifiers(imports) {
101
106
  const dynamic = new Set();
102
107
  const staticSpecifiers = new Set();
103
108
  for (const imp of imports) {
@@ -107,23 +112,30 @@ function isDynamicOnly(imports) {
107
112
  }
108
113
  for (const specifier of staticSpecifiers)
109
114
  dynamic.delete(specifier);
115
+ for (const specifier of [...dynamic]) {
116
+ if (!isHttpUrl(specifier))
117
+ dynamic.delete(specifier);
118
+ }
110
119
  return dynamic;
111
120
  }
112
121
  /**
113
122
  * Build a map of specifier replacements by resolving all imports in the code.
114
123
  *
115
- * Resolution failure is fatal for a static import and best-effort for a
116
- * specifier only ever used in `import(...)`. See {@link isDynamicOnly}.
124
+ * Resolution failure is fatal unless the runtime can resolve the specifier on
125
+ * its own. See {@link runtimeResolvableSpecifiers}. Every specifier left in
126
+ * place is reported as degraded so callers can decide whether the resulting
127
+ * code is fit to cache.
117
128
  */
118
129
  export async function buildReplacements(code, baseUrl, options, cacheHttpModule) {
119
130
  const imports = await parseImports(code);
120
131
  const uniqueSpecifiers = [...new Set(imports.map((imp) => imp.n).filter(Boolean))];
121
- const dynamicOnly = isDynamicOnly(imports);
132
+ const runtimeResolvable = runtimeResolvableSpecifiers(imports);
122
133
  const settled = await Promise.allSettled(uniqueSpecifiers.map(async (specifier) => ({
123
134
  specifier,
124
135
  resolved: await resolveSpecifier(specifier, baseUrl, options, cacheHttpModule),
125
136
  })));
126
137
  const replacements = new Map();
138
+ const degraded = [];
127
139
  for (let i = 0; i < settled.length; i++) {
128
140
  const outcome = settled[i];
129
141
  const specifier = uniqueSpecifiers[i];
@@ -135,24 +147,32 @@ export async function buildReplacements(code, baseUrl, options, cacheHttpModule)
135
147
  replacements.set(resolvedFor, resolved);
136
148
  continue;
137
149
  }
138
- // A static import must resolve. Leaving one unresolved would emit a module
139
- // whose own import graph reaches outside the local cache, which is not what
140
- // the runtime loader is handed anywhere else.
141
- if (!dynamicOnly.has(specifier))
150
+ // Anything the runtime cannot resolve on its own must resolve here.
151
+ // Leaving one unresolved would emit a module whose own import graph reaches
152
+ // outside the local cache, which is not what the runtime loader is handed
153
+ // anywhere else.
154
+ if (!runtimeResolvable.has(specifier))
142
155
  throw outcome.reason;
156
+ degraded.push(specifier);
143
157
  logger.warn("Leaving an unresolvable dynamic specifier for runtime resolution", {
144
158
  specifier,
145
159
  error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
146
160
  });
147
161
  }
148
- return replacements;
162
+ return { replacements, degraded };
149
163
  }
150
164
  /**
151
165
  * Rewrite all HTTP/npm/bare import specifiers in module code to local cached paths.
166
+ *
167
+ * Reports any specifier left in place, so the caller can keep the resulting
168
+ * code out of the caches that outlive this render.
152
169
  */
153
170
  export async function rewriteModuleImports(code, moduleUrl, options, cacheHttpModule) {
154
- const replacements = await buildReplacements(code, moduleUrl, options, cacheHttpModule);
171
+ const { replacements, degraded } = await buildReplacements(code, moduleUrl, options, cacheHttpModule);
155
172
  if (replacements.size === 0)
156
- return code;
157
- return replaceSpecifiers(code, (specifier) => replacements.get(specifier) ?? null);
173
+ return { code, degraded };
174
+ return {
175
+ code: await replaceSpecifiers(code, (specifier) => replacements.get(specifier) ?? null),
176
+ degraded,
177
+ };
158
178
  }
@@ -0,0 +1,23 @@
1
+ import type { ImportRewriteStrategy, ImportSpecifierInfo, RewriteContext, RewriteResult } from "../types.js";
2
+ /**
3
+ * Reject `import logo from "@/assets/logo.svg"`.
4
+ *
5
+ * Veryfront serves static assets from `public/` at the root path; it has no
6
+ * asset-import model, so there is nothing to rewrite such a specifier to.
7
+ * Previously the alias and relative strategies fell through to their
8
+ * "no extension, add .js" branch and emitted `assets/logo.svg.js`, so the
9
+ * failure surfaced as:
10
+ *
11
+ * Module not found "file:///_vf_modules/assets/logo.svg.js"
12
+ *
13
+ * naming a file the author never wrote, in a directory that does not exist.
14
+ * Failing here instead names the real file and the supported alternative.
15
+ */
16
+ export declare class AssetStrategy implements ImportRewriteStrategy {
17
+ readonly name = "asset";
18
+ readonly priority = 0.6;
19
+ matches(specifier: string, _ctx: RewriteContext): boolean;
20
+ rewrite(info: ImportSpecifierInfo, ctx: RewriteContext): RewriteResult;
21
+ }
22
+ export declare const assetStrategy: AssetStrategy;
23
+ //# sourceMappingURL=asset-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-strategy.d.ts","sourceRoot":"","sources":["../../../../../src/src/transforms/import-rewriter/strategies/asset-strategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,aAAa,EACd,MAAM,aAAa,CAAC;AAarB;;;;;;;;;;;;;GAaG;AACH,qBAAa,aAAc,YAAW,qBAAqB;IACzD,QAAQ,CAAC,IAAI,WAAW;IAGxB,QAAQ,CAAC,QAAQ,OAAO;IAExB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO;IAIzD,OAAO,CAAC,IAAI,EAAE,mBAAmB,EAAE,GAAG,EAAE,cAAc,GAAG,aAAa;CAiBvE;AAED,eAAO,MAAM,aAAa,eAAsB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { COMPILATION_ERROR } from "../../../errors/index.js";
2
+ /**
3
+ * Static asset extensions that are not JavaScript modules.
4
+ *
5
+ * `.css` is deliberately absent — CSS imports are supported and are stripped
6
+ * by the pipeline's CSS stage before import resolution runs, so they never
7
+ * reach this strategy.
8
+ */
9
+ const ASSET_EXTENSION_PATTERN = /\.(?:svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|otf|eot|mp4|webm|mp3|wav|pdf)(?:\?.*)?$/i;
10
+ /**
11
+ * Reject `import logo from "@/assets/logo.svg"`.
12
+ *
13
+ * Veryfront serves static assets from `public/` at the root path; it has no
14
+ * asset-import model, so there is nothing to rewrite such a specifier to.
15
+ * Previously the alias and relative strategies fell through to their
16
+ * "no extension, add .js" branch and emitted `assets/logo.svg.js`, so the
17
+ * failure surfaced as:
18
+ *
19
+ * Module not found "file:///_vf_modules/assets/logo.svg.js"
20
+ *
21
+ * naming a file the author never wrote, in a directory that does not exist.
22
+ * Failing here instead names the real file and the supported alternative.
23
+ */
24
+ export class AssetStrategy {
25
+ name = "asset";
26
+ // Ahead of the alias (1) and relative strategies so it sees both
27
+ // `@/assets/logo.svg` and `./logo.svg`.
28
+ priority = 0.6;
29
+ matches(specifier, _ctx) {
30
+ return ASSET_EXTENSION_PATTERN.test(specifier);
31
+ }
32
+ rewrite(info, ctx) {
33
+ // Name the importing file relative to the project. A path outside it is a
34
+ // machine path, so only the file name goes in the message.
35
+ const importer = ctx.filePath.startsWith(ctx.projectDir)
36
+ ? ctx.filePath.slice(ctx.projectDir.length).replace(/^\/+/, "")
37
+ : ctx.filePath.split("/").pop() ?? ctx.filePath;
38
+ const fileName = (info.specifier.split("/").pop() ?? info.specifier).split(/[?#]/, 1)[0] ??
39
+ info.specifier;
40
+ throw COMPILATION_ERROR.create({
41
+ detail: `Cannot import the static asset "${info.specifier}" as a module (in ${importer}). ` +
42
+ `Veryfront serves static assets from public/ at the root path. ` +
43
+ `Move the file to public/${fileName} and reference it by URL instead, ` +
44
+ `for example <img src="/${fileName}" />. ` +
45
+ `See docs/guides/project-structure.md.`,
46
+ });
47
+ }
48
+ }
49
+ export const assetStrategy = new AssetStrategy();
@@ -4,6 +4,7 @@
4
4
  * @module transforms/import-rewriter/strategies
5
5
  */
6
6
  export { AliasStrategy, aliasStrategy } from "./alias-strategy.js";
7
+ export { AssetStrategy, assetStrategy } from "./asset-strategy.js";
7
8
  export { BareStrategy, bareStrategy } from "./bare-strategy.js";
8
9
  export { NodeBuiltinStrategy, nodeBuiltinStrategy } from "./node-builtin-strategy.js";
9
10
  export { CrossProjectStrategy, crossProjectStrategy, isCrossProjectImport, parseCrossProjectImport, } from "./cross-project-strategy.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/transforms/import-rewriter/strategies/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtF,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/src/transforms/import-rewriter/strategies/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtF,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
@@ -4,6 +4,7 @@
4
4
  * @module transforms/import-rewriter/strategies
5
5
  */
6
6
  export { AliasStrategy, aliasStrategy } from "./alias-strategy.js";
7
+ export { AssetStrategy, assetStrategy } from "./asset-strategy.js";
7
8
  export { BareStrategy, bareStrategy } from "./bare-strategy.js";
8
9
  export { NodeBuiltinStrategy, nodeBuiltinStrategy } from "./node-builtin-strategy.js";
9
10
  export { CrossProjectStrategy, crossProjectStrategy, isCrossProjectImport, parseCrossProjectImport, } from "./cross-project-strategy.js";
@@ -1 +1 @@
1
- {"version":3,"file":"unified-rewriter.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/import-rewriter/unified-rewriter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAiB,MAAM,YAAY,CAAC;AAkBvF,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACrC,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,UAAU,CAA0B;gBAEhC,OAAO,CAAC,EAAE,cAAc;IAIpC;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B3D;;OAEG;IACH,OAAO,CAAC,aAAa;CAoCtB;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,uBAA8B,CAAC;AAE3D;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjF"}
1
+ {"version":3,"file":"unified-rewriter.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/import-rewriter/unified-rewriter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAiB,MAAM,YAAY,CAAC;AAmBvF,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACrC,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,UAAU,CAA0B;gBAEhC,OAAO,CAAC,EAAE,cAAc;IAIpC;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B3D;;OAEG;IACH,OAAO,CAAC,aAAa;CAoCtB;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,uBAA8B,CAAC;AAE3D;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjF"}
@@ -7,11 +7,13 @@
7
7
  import { withSpan } from "../../observability/tracing/otlp-setup.js";
8
8
  import { applyRewrites, parseAllImports } from "./parse-cache.js";
9
9
  import { aliasStrategy, bareStrategy, crossProjectStrategy, importMapStrategy, nodeBuiltinStrategy, reactStrategy, relativeStrategy, urlStrategy, vendorStrategy, veryfrontStrategy, } from "./strategies/index.js";
10
+ import { assetStrategy } from "./strategies/asset-strategy.js";
10
11
  /**
11
12
  * Default strategy execution order by priority.
12
13
  */
13
14
  const DEFAULT_STRATEGIES = [
14
15
  nodeBuiltinStrategy, // 0.5 - Node.js built-ins (noop for browser)
16
+ assetStrategy, // 0.6 - static assets are not modules; reject with a pointer
15
17
  reactStrategy, // 0 - React packages first
16
18
  aliasStrategy, // 1 - @/ aliases
17
19
  veryfrontStrategy, // 1.5 - #veryfront/*, veryfront/*
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAkC,MAAM,4BAA4B,CAAC;AACzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAG5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAcxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAS9B;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAOtD;AAED;;GAEG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,SAAS,EAAE,MAAM;CAI9B;AAcD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,oBAAoB,EAC7B,gBAAgB,CAAC,EAAE,MAAM,EACzB,OAAO,GAAE,GAAG,CAAC,MAAM,CAAa,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwFxB;AA8ID;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IACR,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GACA,oBAAoB,CAUtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAkC,MAAM,4BAA4B,CAAC;AACzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAG5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAcxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAS9B;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAOtD;AAED;;GAEG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,SAAS,EAAE,MAAM;CAI9B;AAcD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,oBAAoB,EAC7B,gBAAgB,CAAC,EAAE,MAAM,EACzB,OAAO,GAAE,GAAG,CAAC,MAAM,CAAa,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwFxB;AA+ID;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IACR,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GACA,oBAAoB,CAUtB"}
@@ -18,7 +18,7 @@ import { getModulePathCache } from "../cache/index.js";
18
18
  import { hashString } from "../utils/hash.js";
19
19
  import { resolveModuleFile } from "../resolution/file-finder.js";
20
20
  import { getTransformCacheKey, getVersionedPathCacheKey } from "./cache-keys.js";
21
- import { resolveNestedModuleImports } from "./nested-imports.js";
21
+ import { resolveNestedImportBase, resolveNestedModuleImports } from "./nested-imports.js";
22
22
  import { readDistributedCache } from "./distributed-cache.js";
23
23
  import { resolveUnresolvedModuleViaHttpFallback } from "./http-fallback.js";
24
24
  import { normalizePath } from "./module-cache.js";
@@ -215,6 +215,7 @@ async function doFetchAndCacheModule(normalizedPath, context, fetchAndCacheModul
215
215
  moduleCode,
216
216
  esmCacheDir,
217
217
  normalizedPath,
218
+ parentBasePath: resolveNestedImportBase(normalizedPath, actualFilePath),
218
219
  strictMissingModules: context.strictMissingModules ?? true,
219
220
  projectSlug,
220
221
  fetchAndCacheModule: fetchAndCacheModuleFn,
@@ -42,7 +42,26 @@ export interface ResolveNestedModuleImportsInput {
42
42
  strictMissingModules: boolean;
43
43
  fetchAndCacheModule: (path: string, parent?: string) => Promise<string | null>;
44
44
  log?: Logger;
45
+ /**
46
+ * Path this module's relative imports resolve against. Defaults to
47
+ * `normalizedPath`; see {@link resolveNestedImportBase}.
48
+ */
49
+ parentBasePath?: string;
45
50
  }
51
+ /**
52
+ * The path a module's own relative imports should resolve against.
53
+ *
54
+ * A directory barrel lives at `lib/index.ts` but is addressed as
55
+ * `_vf_modules/lib`. Resolving its children against `_vf_modules/lib.js` drops
56
+ * the trailing segment as if it were a filename, so `./constants.js` becomes
57
+ * `_vf_modules/constants.js` — one directory too high. The file is then not
58
+ * found and gets replaced by a stub, and the barrel silently stops re-exporting
59
+ * anything: `does not provide an export named 'COLORS'`.
60
+ *
61
+ * When the module actually resolved to an index file, keep the directory
62
+ * segment by addressing it as `<dir>/index.js`.
63
+ */
64
+ export declare function resolveNestedImportBase(normalizedPath: string, actualFilePath?: string): string;
46
65
  /**
47
66
  * Resolve nested /_vf_modules and relative imports into local file:// cache paths.
48
67
  */
@@ -1 +1 @@
1
- {"version":3,"file":"nested-imports.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAQtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAMzD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,GACjB;IACD,SAAS,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjF,QAAQ,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjF,CAkCA;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAM3F;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,MAAM,EACnB,oBAAoB,EAAE,OAAO,EAC7B,gBAAgB,CAAC,EAAE,MAAM,EACzB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CAqCjB;AAED,MAAM,WAAW,+BAA+B;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,+BAA+B,GACrC,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
1
+ {"version":3,"file":"nested-imports.d.ts","sourceRoot":"","sources":["../../../../../../src/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAQtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAMzD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,GACjB;IACD,SAAS,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjF,QAAQ,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjF,CAkCA;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAM3F;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,MAAM,EACnB,oBAAoB,EAAE,OAAO,EAC7B,gBAAgB,CAAC,EAAE,MAAM,EACzB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CAqCjB;AAED,MAAM,WAAW,+BAA+B;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EACtB,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CASR;AAED;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,+BAA+B,GACrC,OAAO,CAAC,MAAM,CAAC,CA0DjB"}
@@ -83,6 +83,28 @@ export async function processNestedImports(moduleCode, results, esmCacheDir, str
83
83
  }
84
84
  return replaceSourceSpans(moduleCode, replacements);
85
85
  }
86
+ /**
87
+ * The path a module's own relative imports should resolve against.
88
+ *
89
+ * A directory barrel lives at `lib/index.ts` but is addressed as
90
+ * `_vf_modules/lib`. Resolving its children against `_vf_modules/lib.js` drops
91
+ * the trailing segment as if it were a filename, so `./constants.js` becomes
92
+ * `_vf_modules/constants.js` — one directory too high. The file is then not
93
+ * found and gets replaced by a stub, and the barrel silently stops re-exporting
94
+ * anything: `does not provide an export named 'COLORS'`.
95
+ *
96
+ * When the module actually resolved to an index file, keep the directory
97
+ * segment by addressing it as `<dir>/index.js`.
98
+ */
99
+ export function resolveNestedImportBase(normalizedPath, actualFilePath) {
100
+ if (!actualFilePath || !/(?:^|\/)index\.(?:tsx?|jsx?|mdx|md)$/.test(actualFilePath)) {
101
+ return normalizedPath;
102
+ }
103
+ const withoutExt = normalizedPath.replace(/\.(?:js|mjs)$/, "");
104
+ if (withoutExt.endsWith("/index"))
105
+ return normalizedPath;
106
+ return `${withoutExt}/index.js`;
107
+ }
86
108
  /**
87
109
  * Resolve nested /_vf_modules and relative imports into local file:// cache paths.
88
110
  */
@@ -111,7 +133,7 @@ export async function resolveNestedModuleImports(input) {
111
133
  original,
112
134
  start,
113
135
  end,
114
- nestedFilePath: await input.fetchAndCacheModule(path, input.normalizedPath),
136
+ nestedFilePath: await input.fetchAndCacheModule(path, input.parentBasePath ?? input.normalizedPath),
115
137
  [key]: path,
116
138
  })));
117
139
  input.log?.debug(`${LOG_PREFIX_MDX_LOADER} [fetchAndCacheModule] processing vfModules DONE`, {
@@ -1,3 +1,3 @@
1
1
  /** Shared version value. */
2
- export declare const VERSION = "0.1.1100";
2
+ export declare const VERSION = "0.1.1101";
3
3
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
3
  /** Shared version value. */
4
- export const VERSION = "0.1.1100";
4
+ export const VERSION = "0.1.1101";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.1100",
3
+ "version": "0.1.1101",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
@@ -328,10 +328,10 @@
328
328
  "@types/react": "19.2.14",
329
329
  "@types/react-dom": "19.2.3",
330
330
  "ws": "8.21.0",
331
- "@veryfront/ext-bundler-esbuild": "0.1.1100",
332
- "@veryfront/ext-content-mdx": "0.1.1100",
333
- "@veryfront/ext-css-tailwind": "0.1.1100",
334
- "@veryfront/ext-parser-babel": "0.1.1100"
331
+ "@veryfront/ext-bundler-esbuild": "0.1.1101",
332
+ "@veryfront/ext-content-mdx": "0.1.1101",
333
+ "@veryfront/ext-css-tailwind": "0.1.1101",
334
+ "@veryfront/ext-parser-babel": "0.1.1101"
335
335
  },
336
336
  "devDependencies": {
337
337
  "@types/node": "20.9.0"