@warlock.js/web 5.2.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/esm/build/contribution.d.mts +1 -1
- package/esm/build/contribution.mjs +1 -1
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +1 -1
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +1 -1
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +1 -1
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +1 -1
- package/esm/build/page-default-export.mjs.map +1 -1
- package/esm/build/page-routes-manifest.mjs +1 -1
- package/esm/build/page-routes-manifest.mjs.map +1 -1
- package/esm/build/public-files.mjs +1 -1
- package/esm/build/public-files.mjs.map +1 -1
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +1 -1
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +1 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +1 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +1 -1
- package/esm/client/runtime/matcher.mjs +1 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +1 -1
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +1 -1
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +1 -1
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +1 -1
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.mjs +1 -1
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +1 -1
- package/esm/loaders.d.mts +1 -1
- package/esm/metadata.d.mts +1 -1
- package/esm/metadata.mjs +1 -1
- package/esm/metadata.mjs.map +1 -1
- package/esm/props.d.mts +1 -1
- package/esm/route.d.mts +1 -1
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +1 -1
- package/esm/routing/filesystem-route.mjs.map +1 -1
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +1 -1
- package/esm/routing/url-fragment.mjs.map +1 -1
- package/esm/runtime/register-modules.d.mts +1 -1
- package/esm/runtime/register-modules.mjs +1 -1
- package/esm/runtime/register-modules.mjs.map +1 -1
- package/esm/server/build-hydration-payload.mjs +1 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +1 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +1 -1
- package/esm/server/create-page-route-handler.mjs +1 -1
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +1 -1
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +1 -1
- package/esm/server/error-page.mjs +1 -1
- package/esm/server/error-page.mjs.map +1 -1
- package/esm/server/execute-page-request.d.mts +1 -1
- package/esm/server/execute-page-request.mjs +1 -1
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +1 -1
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +1 -1
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
- package/esm/server/install-page-routes-from-manifest.mjs +1 -1
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +1 -1
- package/esm/server/install-page-routes.mjs +1 -1
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +1 -1
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +1 -1
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -1
- package/esm/server/not-found-page.mjs +1 -1
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +1 -1
- package/esm/server/page-context.mjs +1 -1
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +1 -1
- package/esm/server/page-file-change.mjs.map +1 -1
- package/esm/server/page-manifest.d.mts +1 -1
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +1 -1
- package/esm/server/page-render-bundle.mjs.map +1 -1
- package/esm/server/page-route-reload.mjs +1 -1
- package/esm/server/page-route-reload.mjs.map +1 -1
- package/esm/server/register-production-public-files.mjs +1 -1
- package/esm/server/register-production-public-files.mjs.map +1 -1
- package/esm/server/render-page.d.mts +1 -1
- package/esm/server/render-page.mjs +1 -1
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +1 -1
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +1 -1
- package/esm/server/settle-page-response.mjs +1 -1
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +1 -1
- package/esm/server/stylesheet-urls.mjs +1 -1
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +1 -1
- package/esm/server/unregistered-pages.mjs.map +1 -1
- package/esm/server/web-connector-factory.d.mts +1 -1
- package/esm/server/web-connector-factory.mjs +1 -1
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +1 -1
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +1 -1
- package/esm/shared.mjs +1 -1
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +1 -1
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +1 -1
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +1 -1
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +1 -1
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +1 -1
- package/esm/vite/index.mjs +1 -1
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +1 -1
- package/esm/vite/page-registry-plugin.mjs +1 -1
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +1 -1
- package/esm/vite/projection.mjs +1 -1
- package/esm/vite/projection.mjs.map +1 -1
- package/package.json +3 -3
package/esm/shared.mjs
CHANGED
package/esm/shared.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/shared.ts"],"sourcesContent":["import type { SharedContext } from \"./index\";\r\n\r\n/**\r\n * Vite's own `ImportMetaEnv` augmentation ships with the `vite/client` triple-slash\r\n * types, which this package does not pull in (`shared.ts` compiles standalone, not\r\n * inside a Vite app). This narrows only the one key `sealShared` reads — the literal\r\n * `import.meta.env?.DEV` expression below is load-bearing (see the comment at its\r\n * call site) and must stay untouched, so the fix lives in the type, not the syntax.\r\n */\r\ndeclare global {\r\n interface ImportMetaEnv {\r\n readonly DEV?: boolean;\r\n }\r\n\r\n interface ImportMeta {\r\n readonly env?: ImportMetaEnv;\r\n }\r\n}\r\n\r\n/**\r\n * The runtime behind `shared` — the per-request, ALS-backed audit surface.\r\n *\r\n * `shared` is syntactically global for DX but semantically request-scoped: the\r\n * exported value is a Proxy, and EVERY trap re-resolves the current request's\r\n * target through core's AsyncLocalStorage store. This module holds a resolver,\r\n * never data — nothing here may cache a target, because a cached target\r\n * reintroduces the cross-request leak in the one form that still passes a\r\n * single-request test. And it throws rather than falling\r\n * back: outside a live request there is no process-wide object to land on —\r\n * that is `useRequestStore()`'s `|| {}` shape (request-context.ts:74-76),\r\n * deliberately not copied here; the pattern followed instead is\r\n * `requestMemo()`'s raw-store-or-throw (core/src/http/context/request-memo.ts:29-40).\r\n *\r\n * WHY A RESOLVER AND NOT `import { requestContext } from \"@warlock.js/core\"`:\r\n * web does not (and must not yet) depend on core — no core dep in\r\n * web/package.json, no tsconfig path, and no built esm/ in this checkout\r\n * (context.ts:96-98 records the same fact for types). So the PIPELINE, which\r\n * lives where `requestContext` is in scope, owns the wiring end to end:\r\n *\r\n * connectSharedStore(() => requestContext.getStore()); // boot, once\r\n * enterSharedScope(store); // stage 2, per request\r\n * await sealShared(); // settle stage: gate → parse → freeze\r\n *\r\n * shared.ts never opens an ALS scope itself.\r\n */\r\n\r\n/**\r\n * The per-request target's runtime shape. `SharedContext` (index.ts:17) is the\r\n * app-augmentable public surface — apps declaration-merge their own keys onto\r\n * it — so `SharedTarget` intersects it in rather than replacing it: every\r\n * `SharedContext` member is structurally a `SharedTarget` member too, which is\r\n * what lets `enterSharedScope` and `shared` below hand back a `SharedTarget`\r\n * value where a `SharedContext` is expected with no cast at the boundary.\r\n */\r\ntype SharedTarget = SharedContext & Record<string | symbol, unknown>;\r\n\r\n/**\r\n * The pipeline's per-request store, structurally. At runtime this IS core's\r\n * `RequestContextStore` (`{ request, response }`, request-context.ts:10-13);\r\n * only `response.parse` is named here because it is the one member seal needs —\r\n * core's `Response.parse` is public (core/src/http/response.ts:297) and the\r\n * store already carries the instance, which is how seal reaches it without web\r\n * importing core.\r\n */\r\nexport type SharedStore = {\r\n response?: { parse(value: unknown): Promise<unknown> };\r\n};\r\n\r\n/**\r\n * Returns the CURRENT request's store, or undefined outside a request. The\r\n * pipeline connects `() => requestContext.getStore()`.\r\n */\r\nexport type SharedStoreResolver = () => SharedStore | undefined;\r\n\r\ntype SharedScope = {\r\n target: SharedTarget;\r\n sealed: boolean;\r\n};\r\n\r\n/**\r\n * Per-request scope records, keyed by the request's own store object — the\r\n * exact idiom of request-memo.ts:10: the store object is fresh per request\r\n * (RequestContext.buildStore), so two concurrent requests cannot share or even\r\n * see each other's entries, and everything is GC-eligible when the request's\r\n * ALS frame ends.\r\n */\r\nconst sharedScopes = new WeakMap<SharedStore, SharedScope>();\r\n\r\nlet resolveSharedStore: SharedStoreResolver | undefined;\r\n\r\n/**\r\n * Boot-time wiring, called once by the pipeline before any request runs.\r\n * Returns the previously connected resolver so a caller (tests, mainly) can\r\n * restore it.\r\n */\r\nexport function connectSharedStore(\r\n resolve: SharedStoreResolver | undefined,\r\n): SharedStoreResolver | undefined {\r\n const previous = resolveSharedStore;\r\n resolveSharedStore = resolve;\r\n return previous;\r\n}\r\n\r\nfunction currentStore(access: string): SharedStore {\r\n if (!resolveSharedStore) {\r\n throw new Error(\r\n `Cannot ${access}: \\`shared\\` is not connected to a request store ` +\r\n \"(web/src/shared.ts). `shared` is request-scoped — its data lives in \" +\r\n \"core's per-request AsyncLocalStorage store, and this module holds only \" +\r\n \"a resolver, never data. Fix: the server bootstrap must call \" +\r\n \"connectSharedStore(() => requestContext.getStore()) before any request \" +\r\n \"runs; in a unit test, connect a resolver for the context the test runs in.\",\r\n );\r\n }\r\n\r\n const store = resolveSharedStore();\r\n\r\n if (!store) {\r\n throw new Error(\r\n `Cannot ${access}: \\`shared\\` was accessed outside a request context ` +\r\n \"(web/src/shared.ts). `shared` is per-request — this happens at module \" +\r\n \"load, in a background job, or from a late timer/dangling promise that \" +\r\n \"outlived its request. There is deliberately NO fallback to a \" +\r\n \"process-wide object: that would leak one user's data into another's \" +\r\n \"response. Fix: move this access into code the request pipeline runs \" +\r\n \"(middleware, a loader, a component render), or pass the value you need \" +\r\n \"explicitly.\",\r\n );\r\n }\r\n\r\n return store;\r\n}\r\n\r\nfunction scopeOf(store: SharedStore, access: string): SharedScope {\r\n const scope = sharedScopes.get(store);\r\n\r\n if (!scope) {\r\n throw new Error(\r\n `Cannot ${access}: this request has no \\`shared\\` scope yet ` +\r\n \"(web/src/shared.ts). The per-request target is created by the pipeline \" +\r\n \"at stage 2 via enterSharedScope(store); this access ran inside the \" +\r\n \"request context but before that point. Fix: move the access after \" +\r\n \"pipeline stage 2 (any middleware/loader/render code qualifies), or — if \" +\r\n \"you are the pipeline — call enterSharedScope(store) first.\",\r\n );\r\n }\r\n\r\n return scope;\r\n}\r\n\r\nfunction requireScope(access: string): SharedScope {\r\n return scopeOf(currentStore(access), access);\r\n}\r\n\r\nfunction sealedWriteError(action: string, key: string | symbol): Error {\r\n return new Error(\r\n `Cannot ${action} \\`shared.${String(key)}\\`: shared is SEALED for this ` +\r\n \"request (web/src/shared.ts). Writes are middleware work and happen \" +\r\n \"before the seal; after the seal the payload is committed to the page and \" +\r\n \"any further write would silently diverge server from client. Reads keep \" +\r\n \"working. Fix: move this write into middleware (before the pipeline's \" +\r\n \"seal stage), or if the value is render-time state, it does not belong in \" +\r\n \"`shared`.\",\r\n );\r\n}\r\n\r\n/**\r\n * Pipeline stage 2: create THE per-request target. Once per request — the\r\n * store object is the request's identity (fresh per request), so a second call\r\n * for the same store is a pipeline bug, not a merge.\r\n *\r\n * Returns the raw target; the pipeline may hold it, but app code goes through\r\n * `shared`.\r\n */\r\nexport function enterSharedScope(store: SharedStore): SharedContext {\r\n if (sharedScopes.has(store)) {\r\n throw new Error(\r\n \"enterSharedScope() was called twice for the same request store \" +\r\n \"(web/src/shared.ts). Each request gets exactly one `shared` target, \" +\r\n \"created once at pipeline stage 2. Fix: enter the scope once per \" +\r\n \"request; to reach the existing target, use `shared` inside the \" +\r\n \"request context instead.\",\r\n );\r\n }\r\n\r\n const target: SharedTarget = {};\r\n\r\n sharedScopes.set(store, { target, sealed: false });\r\n\r\n return target;\r\n}\r\n\r\nfunction rejectAtGate(path: string, offender: string): never {\r\n throw new Error(\r\n `sealShared(): \\`${path}\\` is ${offender} (web/src/shared.ts prototype ` +\r\n \"gate — runs in production AND dev). `shared` is the audit surface \" +\r\n \"serialized into the page: every value must be plain data (scalars, \" +\r\n \"arrays, plain objects) or carry a `toJSON()` serialization contract \" +\r\n \"(a Resource). A Date/Map/Set/class instance smuggles prototype state \" +\r\n \"past that audit and does not survive serialization intact. Fix: store \" +\r\n \"plain data — an ISO string instead of a Date, an object or array \" +\r\n \"instead of a Map/Set, a Resource (or its `.toJSON()` output) instead \" +\r\n \"of a model or class instance.\",\r\n );\r\n}\r\n\r\n/**\r\n * The prototype gate. Precedence mirrors `Response.parse` exactly so the gate\r\n * and the normalization that follows it agree on every value: toJSON wins over\r\n * the plain-object branch (response.ts:301-305 vs :319) and is NOT descended —\r\n * a Resource's field list is its own contract. Date is\r\n * checked BEFORE toJSON because `Date.prototype.toJSON` exists and Dates are\r\n * rejected regardless.\r\n */\r\nfunction assertClientSafe(value: unknown, path: string): void {\r\n if (value === null || value === undefined) return;\r\n\r\n const valueType = typeof value;\r\n\r\n if (valueType === \"function\") rejectAtGate(path, \"a function\");\r\n if (valueType !== \"object\") return;\r\n\r\n if (value instanceof Date) rejectAtGate(path, \"a Date\");\r\n if (value instanceof Map) rejectAtGate(path, \"a Map\");\r\n if (value instanceof Set) rejectAtGate(path, \"a Set\");\r\n\r\n if (typeof (value as { toJSON?: unknown }).toJSON === \"function\") return;\r\n\r\n if (Array.isArray(value)) {\r\n value.forEach((item, index) => assertClientSafe(item, `${path}[${index}]`));\r\n return;\r\n }\r\n\r\n const proto = Object.getPrototypeOf(value);\r\n\r\n if (proto === Object.prototype || proto === null) {\r\n for (const key of Object.keys(value)) {\r\n assertClientSafe((value as SharedTarget)[key], `${path}.${key}`);\r\n }\r\n return;\r\n }\r\n\r\n const name = (proto?.constructor?.name as string | undefined) ?? \"unknown class\";\r\n rejectAtGate(path, `a class instance (${name})`);\r\n}\r\n\r\nfunction deepFreeze(value: unknown): void {\r\n if (!value || typeof value !== \"object\" || Object.isFrozen(value)) return;\r\n\r\n Object.freeze(value);\r\n\r\n for (const key of Object.keys(value)) {\r\n deepFreeze((value as SharedTarget)[key]);\r\n }\r\n}\r\n\r\nlet browserSharedSnapshot: Readonly<SharedContext> | undefined;\r\nlet browserSharedInstalled = false;\r\n\r\nfunction freezeBrowserSnapshot(value: object, seen: Set<object>): void {\r\n if (seen.has(value)) return;\r\n\r\n seen.add(value);\r\n\r\n for (const key of Object.keys(value)) {\r\n const child = (value as SharedTarget)[key];\r\n\r\n if (child !== null && typeof child === \"object\") {\r\n freezeBrowserSnapshot(child, seen);\r\n }\r\n }\r\n\r\n Object.freeze(value);\r\n}\r\n\r\n/**\r\n * Install the browser's one readonly `shared` snapshot. An object value is\r\n * recursively frozen in place and retained wholesale so every consumer\r\n * observes the same identity; this never reads or writes the server ALS scope.\r\n */\r\nexport function hydrateShared(value: unknown): void {\r\n if (value !== null && typeof value === \"object\") {\r\n freezeBrowserSnapshot(value, new Set<object>());\r\n }\r\n\r\n browserSharedSnapshot = value as Readonly<SharedContext>;\r\n browserSharedInstalled = true;\r\n}\r\n\r\nfunction readBrowserSharedSnapshot(): Readonly<SharedContext> {\r\n if (!browserSharedInstalled) {\r\n throw new Error(\r\n \"Cannot read `shared` via useShared(): the browser snapshot has not been \" +\r\n \"installed (web/src/shared.ts). Hydration must validate the complete \" +\r\n \"#__WARLOCK_DATA__ payload and call hydrateShared() \" +\r\n \"before constructing the React tree.\",\r\n );\r\n }\r\n\r\n return browserSharedSnapshot as Readonly<SharedContext>;\r\n}\r\n\r\n/**\r\n * The settle-stage seal, exported for the pipeline. Order is load-bearing:\r\n *\r\n * 1. PROTOTYPE GATE (pre-parse) — production and dev, offending key named\r\n * with its full path. Fail-fast courtesy: catches most violations before\r\n * spending a parse pass on them.\r\n * 2. `Response.parse` NORMALIZATION — via the store's own response instance\r\n * (core/src/http/response.ts:297, the public surface). parse mutates the\r\n * target IN PLACE (response.ts:327), which is exactly why it MUST precede\r\n * the freeze: freeze-then-parse throws on the first key parse writes\r\n * in place.\r\n * 3. PROTOTYPE GATE AGAIN (post-parse) — closes the re-entry window the first\r\n * gate cannot see: `Response.parse` never re-parses a `toJSON()` result\r\n * (it only normalizes the value handed to it), so a Resource whose `toJSON()` returns a\r\n * Date/Map/Set/class instance re-enters the payload AFTER the first gate\r\n * already ran — the first gate inspects the pre-parse value (a plain\r\n * object with a callable `toJSON`, correctly not descended) and parse then\r\n * puts back exactly what that gate would have rejected. Only a gate that\r\n * runs on the POST-parse target — the object that actually ships — can\r\n * catch it. This is the only gate that matters for correctness; the first\r\n * is a fail-fast courtesy that never gets to be wrong on its own.\r\n * 4. DEEP FREEZE — dev only (env read at seal time). Prod skips the freeze but\r\n * NOT the sealed-write throw below; freezing is defense-in-depth for direct\r\n * target references, the throw is the contract.\r\n * 5. Sealed flag — writes/deletes/defines through `shared` now throw naming\r\n * the key; reads keep working.\r\n *\r\n * `store` defaults to the current request's store; the pipeline holds the\r\n * store either way, since it entered the scope with it. Returns the sealed\r\n * target — serialize THAT, not a re-read of the proxy.\r\n */\r\nexport async function sealShared(store?: SharedStore): Promise<Readonly<SharedContext>> {\r\n const scopeStore = store ?? currentStore(\"seal `shared`\");\r\n const scope = scopeOf(scopeStore, \"seal `shared`\");\r\n\r\n if (scope.sealed) {\r\n throw new Error(\r\n \"sealShared() was called twice for the same request (web/src/shared.ts). \" +\r\n \"The seal is the pipeline's settle stage and runs once, after \" +\r\n \"middleware writes and before serialization. Fix: seal once per \" +\r\n \"request.\",\r\n );\r\n }\r\n\r\n assertClientSafe(scope.target, \"shared\");\r\n\r\n const response = scopeStore.response;\r\n\r\n if (!response || typeof response.parse !== \"function\") {\r\n throw new Error(\r\n \"sealShared() found no `response.parse` on the request store \" +\r\n \"(web/src/shared.ts). Sealing normalizes `shared` through core's \" +\r\n \"public `Response.parse` (core/src/http/response.ts:297), reached via \" +\r\n \"the store's own `response` — the store the pipeline entered the scope \" +\r\n \"with must be core's request store (`{ request, response }`, \" +\r\n \"request-context.ts:10-13). Fix: pass that store (or run sealShared() \" +\r\n \"inside the request context that carries it).\",\r\n );\r\n }\r\n\r\n await response.parse(scope.target);\r\n\r\n assertClientSafe(scope.target, \"shared\");\r\n\r\n /*\r\n OPTIONAL CHAINING IS LOAD-BEARING, and the fallback that used to sit here\r\n is deliberately gone.\r\n\r\n `import.meta.env` is injected by a BUNDLER. When this package is compiled\r\n into an app's server bundle — what happens in this checkout — esbuild's\r\n `define` (`WEB_ESBUILD_PATCH.define` in\r\n `src/build/generate-pages-barrel.ts`) does a literal text-level\r\n match on `import.meta.env.DEV` and replaces it verbatim wherever it\r\n appears, `?.` or not — proven against esbuild's real `transform()`, not\r\n assumed (vite/gate-b-secrets.spec.ts, \"WEB_ESBUILD_PATCH.define\" describe\r\n block): `import.meta.env?.DEV` compiles to `false`, same as the\r\n non-optional form, and no `import.meta` survives in either case. When\r\n `@warlock.js/web` is INSTALLED, it is external to that bundle, nothing\r\n replaces anything, and plain `import.meta.env.DEV` threw `Cannot read\r\n properties of undefined` on EVERY page render. `?.` is what makes the\r\n absent case falsy instead of fatal.\r\n\r\n A `globalThis.process?.env?.NODE_ENV` fallback was tried and REJECTED:\r\n Gate B matches `process.env` only when the object is the bare identifier\r\n `process` (vite/gate-b-secrets.ts:100), so routing through `globalThis`\r\n walks straight past the secret-leak gate. A dev-only freeze is not worth\r\n teaching the codebase the shape that evades that check.\r\n\r\n The cost is honest and small: an installed package running under\r\n `NODE_ENV=development` outside a bundler does not freeze. `DEV` is in Gate\r\n B's own allowlist (gate-b-secrets.ts:44), so this form stays permitted in\r\n client-bound code.\r\n */\r\n if (import.meta.env?.DEV) {\r\n deepFreeze(scope.target);\r\n }\r\n\r\n scope.sealed = true;\r\n\r\n return scope.target as Readonly<SharedContext>;\r\n}\r\n\r\n/**\r\n * The WRITABLE per-request payload — middleware's half of the contract.\r\n *\r\n * Every `shared.x = …` at a call site reads like a global write; it is not —\r\n * each trap below resolves the CURRENT request's target through the connected\r\n * store resolver, on every single access — two concurrent requests writing\r\n * `shared.locale` write to two different objects.\r\n */\r\nexport const shared: SharedContext = new Proxy({} as SharedTarget, {\r\n get(_stub, key) {\r\n return requireScope(`read \\`shared.${String(key)}\\``).target[key];\r\n },\r\n\r\n set(_stub, key, value) {\r\n const scope = requireScope(`write \\`shared.${String(key)}\\``);\r\n\r\n if (scope.sealed) throw sealedWriteError(\"write\", key);\r\n\r\n scope.target[key] = value;\r\n\r\n return true;\r\n },\r\n\r\n has(_stub, key) {\r\n return key in requireScope(`check \\`shared.${String(key)}\\``).target;\r\n },\r\n\r\n deleteProperty(_stub, key) {\r\n const scope = requireScope(`delete \\`shared.${String(key)}\\``);\r\n\r\n if (scope.sealed) throw sealedWriteError(\"delete\", key);\r\n\r\n delete scope.target[key];\r\n\r\n return true;\r\n },\r\n\r\n ownKeys() {\r\n return Reflect.ownKeys(requireScope(\"enumerate `shared`\").target);\r\n },\r\n\r\n getOwnPropertyDescriptor(_stub, key) {\r\n const descriptor = Object.getOwnPropertyDescriptor(\r\n requireScope(`describe \\`shared.${String(key)}\\``).target,\r\n key,\r\n );\r\n\r\n if (!descriptor) return undefined;\r\n\r\n // The proxy's book-keeping target is the empty stub, so a frozen real\r\n // target's non-configurable descriptors would violate proxy invariants if\r\n // reported as-is; configurable:true is the honest report for the proxy\r\n // surface (the sealed-write throw is what enforces immutability).\r\n return { ...descriptor, configurable: true };\r\n },\r\n\r\n defineProperty(_stub, key, descriptor) {\r\n const scope = requireScope(`define \\`shared.${String(key)}\\``);\r\n\r\n if (scope.sealed) throw sealedWriteError(\"define\", key);\r\n\r\n Reflect.defineProperty(scope.target, key, descriptor);\r\n\r\n return true;\r\n },\r\n});\r\n\r\n/**\r\n * The READ half for components at depth. On the server it preserves the live\r\n * ALS proxy behavior. In the browser it returns the exact recursively frozen\r\n * object installed from the validated hydration payload, never an empty or\r\n * process-wide fallback.\r\n */\r\nexport function useShared(): Readonly<SharedContext> {\r\n if (typeof window !== \"undefined\") return readBrowserSharedSnapshot();\r\n\r\n requireScope(\"read `shared` via useShared()\");\r\n\r\n return shared as Readonly<SharedContext>;\r\n}\r\n"],"mappings":";;;;;;;;AAsFA,MAAM,+BAAe,IAAI,QAAkC;AAE3D,IAAI;;;;;;AAOJ,SAAgB,mBACd,SACiC;CACjC,MAAM,WAAW;CACjB,qBAAqB;CACrB,OAAO;AACT;AAEA,SAAS,aAAa,QAA6B;CACjD,IAAI,CAAC,oBACH,MAAM,IAAI,MACR,UAAU,OAAO,4YAMnB;CAGF,MAAM,QAAQ,mBAAmB;CAEjC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,UAAU,OAAO,0dAQnB;CAGF,OAAO;AACT;AAEA,SAAS,QAAQ,OAAoB,QAA6B;CAChE,MAAM,QAAQ,aAAa,IAAI,KAAK;CAEpC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,UAAU,OAAO,0XAMnB;CAGF,OAAO;AACT;AAEA,SAAS,aAAa,QAA6B;CACjD,OAAO,QAAQ,aAAa,MAAM,GAAG,MAAM;AAC7C;AAEA,SAAS,iBAAiB,QAAgB,KAA6B;CACrE,uBAAO,IAAI,MACT,UAAU,OAAO,YAAY,OAAO,GAAG,EAAE,4YAO3C;AACF;;;;;;;;;AAUA,SAAgB,iBAAiB,OAAmC;CAClE,IAAI,aAAa,IAAI,KAAK,GACxB,MAAM,IAAI,MACR,4RAKF;CAGF,MAAM,SAAuB,CAAC;CAE9B,aAAa,IAAI,OAAO;EAAE;EAAQ,QAAQ;CAAM,CAAC;CAEjD,OAAO;AACT;AAEA,SAAS,aAAa,MAAc,UAAyB;CAC3D,MAAM,IAAI,MACR,mBAAmB,KAAK,QAAQ,SAAS,4hBAS3C;AACF;;;;;;;;;AAUA,SAAS,iBAAiB,OAAgB,MAAoB;CAC5D,IAAI,UAAU,QAAQ,UAAU,QAAW;CAE3C,MAAM,YAAY,OAAO;CAEzB,IAAI,cAAc,YAAY,aAAa,MAAM,YAAY;CAC7D,IAAI,cAAc,UAAU;CAE5B,IAAI,iBAAiB,MAAM,aAAa,MAAM,QAAQ;CACtD,IAAI,iBAAiB,KAAK,aAAa,MAAM,OAAO;CACpD,IAAI,iBAAiB,KAAK,aAAa,MAAM,OAAO;CAEpD,IAAI,OAAQ,MAA+B,WAAW,YAAY;CAElE,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,MAAM,SAAS,MAAM,UAAU,iBAAiB,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,CAAC;EAC1E;CACF;CAEA,MAAM,QAAQ,OAAO,eAAe,KAAK;CAEzC,IAAI,UAAU,OAAO,aAAa,UAAU,MAAM;EAChD,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,iBAAkB,MAAuB,MAAM,GAAG,KAAK,GAAG,KAAK;EAEjE;CACF;CAGA,aAAa,MAAM,qBADL,OAAO,aAAa,QAA+B,gBACpB,EAAE;AACjD;AAEA,SAAS,WAAW,OAAsB;CACxC,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG;CAEnE,OAAO,OAAO,KAAK;CAEnB,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,WAAY,MAAuB,IAAI;AAE3C;AAEA,IAAI;AACJ,IAAI,yBAAyB;AAE7B,SAAS,sBAAsB,OAAe,MAAyB;CACrE,IAAI,KAAK,IAAI,KAAK,GAAG;CAErB,KAAK,IAAI,KAAK;CAEd,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAG;EACpC,MAAM,QAAS,MAAuB;EAEtC,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,sBAAsB,OAAO,IAAI;CAErC;CAEA,OAAO,OAAO,KAAK;AACrB;;;;;;AAOA,SAAgB,cAAc,OAAsB;CAClD,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,sBAAsB,uBAAO,IAAI,IAAY,CAAC;CAGhD,wBAAwB;CACxB,yBAAyB;AAC3B;AAEA,SAAS,4BAAqD;CAC5D,IAAI,CAAC,wBACH,MAAM,IAAI,MACR,oOAIF;CAGF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,eAAsB,WAAW,OAAuD;CACtF,MAAM,aAAa,SAAS,aAAa,eAAe;CACxD,MAAM,QAAQ,QAAQ,YAAY,eAAe;CAEjD,IAAI,MAAM,QACR,MAAM,IAAI,MACR,8MAIF;CAGF,iBAAiB,MAAM,QAAQ,QAAQ;CAEvC,MAAM,WAAW,WAAW;CAE5B,IAAI,CAAC,YAAY,OAAO,SAAS,UAAU,YACzC,MAAM,IAAI,MACR,sbAOF;CAGF,MAAM,SAAS,MAAM,MAAM,MAAM;CAEjC,iBAAiB,MAAM,QAAQ,QAAQ;CA+BvC,IAAI,OAAO,KAAK,KAAK,KACnB,WAAW,MAAM,MAAM;CAGzB,MAAM,SAAS;CAEf,OAAO,MAAM;AACf;;;;;;;;;AAUA,MAAa,SAAwB,IAAI,MAAM,CAAC,GAAmB;CACjE,IAAI,OAAO,KAAK;EACd,OAAO,aAAa,iBAAiB,OAAO,GAAG,EAAE,GAAG,EAAE,OAAO;CAC/D;CAEA,IAAI,OAAO,KAAK,OAAO;EACrB,MAAM,QAAQ,aAAa,kBAAkB,OAAO,GAAG,EAAE,GAAG;EAE5D,IAAI,MAAM,QAAQ,MAAM,iBAAiB,SAAS,GAAG;EAErD,MAAM,OAAO,OAAO;EAEpB,OAAO;CACT;CAEA,IAAI,OAAO,KAAK;EACd,OAAO,OAAO,aAAa,kBAAkB,OAAO,GAAG,EAAE,GAAG,EAAE;CAChE;CAEA,eAAe,OAAO,KAAK;EACzB,MAAM,QAAQ,aAAa,mBAAmB,OAAO,GAAG,EAAE,GAAG;EAE7D,IAAI,MAAM,QAAQ,MAAM,iBAAiB,UAAU,GAAG;EAEtD,OAAO,MAAM,OAAO;EAEpB,OAAO;CACT;CAEA,UAAU;EACR,OAAO,QAAQ,QAAQ,aAAa,oBAAoB,EAAE,MAAM;CAClE;CAEA,yBAAyB,OAAO,KAAK;EACnC,MAAM,aAAa,OAAO,yBACxB,aAAa,qBAAqB,OAAO,GAAG,EAAE,GAAG,EAAE,QACnD,GACF;EAEA,IAAI,CAAC,YAAY,OAAO;EAMxB,OAAO;GAAE,GAAG;GAAY,cAAc;EAAK;CAC7C;CAEA,eAAe,OAAO,KAAK,YAAY;EACrC,MAAM,QAAQ,aAAa,mBAAmB,OAAO,GAAG,EAAE,GAAG;EAE7D,IAAI,MAAM,QAAQ,MAAM,iBAAiB,UAAU,GAAG;EAEtD,QAAQ,eAAe,MAAM,QAAQ,KAAK,UAAU;EAEpD,OAAO;CACT;AACF,CAAC;;;;;;;AAQD,SAAgB,YAAqC;CACnD,IAAI,OAAO,WAAW,aAAa,OAAO,0BAA0B;CAEpE,aAAa,+BAA+B;CAE5C,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"shared.mjs","names":[],"sources":["../../../../../../web/src/shared.ts"],"sourcesContent":["import type { SharedContext } from \"./index\";\r\n\r\n/**\r\n * Vite's own `ImportMetaEnv` augmentation ships with the `vite/client` triple-slash\r\n * types, which this package does not pull in (`shared.ts` compiles standalone, not\r\n * inside a Vite app). This narrows only the one key `sealShared` reads — the literal\r\n * `import.meta.env?.DEV` expression below is load-bearing (see the comment at its\r\n * call site) and must stay untouched, so the fix lives in the type, not the syntax.\r\n */\r\ndeclare global {\r\n interface ImportMetaEnv {\r\n readonly DEV?: boolean;\r\n }\r\n\r\n interface ImportMeta {\r\n readonly env?: ImportMetaEnv;\r\n }\r\n}\r\n\r\n/**\r\n * The runtime behind `shared` — the per-request, ALS-backed audit surface.\r\n *\r\n * `shared` is syntactically global for DX but semantically request-scoped: the\r\n * exported value is a Proxy, and EVERY trap re-resolves the current request's\r\n * target through core's AsyncLocalStorage store. This module holds a resolver,\r\n * never data — nothing here may cache a target, because a cached target\r\n * reintroduces the cross-request leak in the one form that still passes a\r\n * single-request test. And it throws rather than falling\r\n * back: outside a live request there is no process-wide object to land on —\r\n * that is `useRequestStore()`'s `|| {}` shape (request-context.ts:74-76),\r\n * deliberately not copied here; the pattern followed instead is\r\n * `requestMemo()`'s raw-store-or-throw (core/src/http/context/request-memo.ts:29-40).\r\n *\r\n * WHY A RESOLVER AND NOT `import { requestContext } from \"@warlock.js/core\"`:\r\n * web does not (and must not yet) depend on core — no core dep in\r\n * web/package.json, no tsconfig path, and no built esm/ in this checkout\r\n * (context.ts:96-98 records the same fact for types). So the PIPELINE, which\r\n * lives where `requestContext` is in scope, owns the wiring end to end:\r\n *\r\n * connectSharedStore(() => requestContext.getStore()); // boot, once\r\n * enterSharedScope(store); // stage 2, per request\r\n * await sealShared(); // settle stage: gate → parse → freeze\r\n *\r\n * shared.ts never opens an ALS scope itself.\r\n */\r\n\r\n/**\r\n * The per-request target's runtime shape. `SharedContext` (index.ts:17) is the\r\n * app-augmentable public surface — apps declaration-merge their own keys onto\r\n * it — so `SharedTarget` intersects it in rather than replacing it: every\r\n * `SharedContext` member is structurally a `SharedTarget` member too, which is\r\n * what lets `enterSharedScope` and `shared` below hand back a `SharedTarget`\r\n * value where a `SharedContext` is expected with no cast at the boundary.\r\n */\r\ntype SharedTarget = SharedContext & Record<string | symbol, unknown>;\r\n\r\n/**\r\n * The pipeline's per-request store, structurally. At runtime this IS core's\r\n * `RequestContextStore` (`{ request, response }`, request-context.ts:10-13);\r\n * only `response.parse` is named here because it is the one member seal needs —\r\n * core's `Response.parse` is public (core/src/http/response.ts:297) and the\r\n * store already carries the instance, which is how seal reaches it without web\r\n * importing core.\r\n */\r\nexport type SharedStore = {\r\n response?: { parse(value: unknown): Promise<unknown> };\r\n};\r\n\r\n/**\r\n * Returns the CURRENT request's store, or undefined outside a request. The\r\n * pipeline connects `() => requestContext.getStore()`.\r\n */\r\nexport type SharedStoreResolver = () => SharedStore | undefined;\r\n\r\ntype SharedScope = {\r\n target: SharedTarget;\r\n sealed: boolean;\r\n};\r\n\r\n/**\r\n * Per-request scope records, keyed by the request's own store object — the\r\n * exact idiom of request-memo.ts:10: the store object is fresh per request\r\n * (RequestContext.buildStore), so two concurrent requests cannot share or even\r\n * see each other's entries, and everything is GC-eligible when the request's\r\n * ALS frame ends.\r\n */\r\nconst sharedScopes = new WeakMap<SharedStore, SharedScope>();\r\n\r\nlet resolveSharedStore: SharedStoreResolver | undefined;\r\n\r\n/**\r\n * Boot-time wiring, called once by the pipeline before any request runs.\r\n * Returns the previously connected resolver so a caller (tests, mainly) can\r\n * restore it.\r\n */\r\nexport function connectSharedStore(\r\n resolve: SharedStoreResolver | undefined,\r\n): SharedStoreResolver | undefined {\r\n const previous = resolveSharedStore;\r\n resolveSharedStore = resolve;\r\n return previous;\r\n}\r\n\r\nfunction currentStore(access: string): SharedStore {\r\n if (!resolveSharedStore) {\r\n throw new Error(\r\n `Cannot ${access}: \\`shared\\` is not connected to a request store ` +\r\n \"(web/src/shared.ts). `shared` is request-scoped — its data lives in \" +\r\n \"core's per-request AsyncLocalStorage store, and this module holds only \" +\r\n \"a resolver, never data. Fix: the server bootstrap must call \" +\r\n \"connectSharedStore(() => requestContext.getStore()) before any request \" +\r\n \"runs; in a unit test, connect a resolver for the context the test runs in.\",\r\n );\r\n }\r\n\r\n const store = resolveSharedStore();\r\n\r\n if (!store) {\r\n throw new Error(\r\n `Cannot ${access}: \\`shared\\` was accessed outside a request context ` +\r\n \"(web/src/shared.ts). `shared` is per-request — this happens at module \" +\r\n \"load, in a background job, or from a late timer/dangling promise that \" +\r\n \"outlived its request. There is deliberately NO fallback to a \" +\r\n \"process-wide object: that would leak one user's data into another's \" +\r\n \"response. Fix: move this access into code the request pipeline runs \" +\r\n \"(middleware, a loader, a component render), or pass the value you need \" +\r\n \"explicitly.\",\r\n );\r\n }\r\n\r\n return store;\r\n}\r\n\r\nfunction scopeOf(store: SharedStore, access: string): SharedScope {\r\n const scope = sharedScopes.get(store);\r\n\r\n if (!scope) {\r\n throw new Error(\r\n `Cannot ${access}: this request has no \\`shared\\` scope yet ` +\r\n \"(web/src/shared.ts). The per-request target is created by the pipeline \" +\r\n \"at stage 2 via enterSharedScope(store); this access ran inside the \" +\r\n \"request context but before that point. Fix: move the access after \" +\r\n \"pipeline stage 2 (any middleware/loader/render code qualifies), or — if \" +\r\n \"you are the pipeline — call enterSharedScope(store) first.\",\r\n );\r\n }\r\n\r\n return scope;\r\n}\r\n\r\nfunction requireScope(access: string): SharedScope {\r\n return scopeOf(currentStore(access), access);\r\n}\r\n\r\nfunction sealedWriteError(action: string, key: string | symbol): Error {\r\n return new Error(\r\n `Cannot ${action} \\`shared.${String(key)}\\`: shared is SEALED for this ` +\r\n \"request (web/src/shared.ts). Writes are middleware work and happen \" +\r\n \"before the seal; after the seal the payload is committed to the page and \" +\r\n \"any further write would silently diverge server from client. Reads keep \" +\r\n \"working. Fix: move this write into middleware (before the pipeline's \" +\r\n \"seal stage), or if the value is render-time state, it does not belong in \" +\r\n \"`shared`.\",\r\n );\r\n}\r\n\r\n/**\r\n * Pipeline stage 2: create THE per-request target. Once per request — the\r\n * store object is the request's identity (fresh per request), so a second call\r\n * for the same store is a pipeline bug, not a merge.\r\n *\r\n * Returns the raw target; the pipeline may hold it, but app code goes through\r\n * `shared`.\r\n */\r\nexport function enterSharedScope(store: SharedStore): SharedContext {\r\n if (sharedScopes.has(store)) {\r\n throw new Error(\r\n \"enterSharedScope() was called twice for the same request store \" +\r\n \"(web/src/shared.ts). Each request gets exactly one `shared` target, \" +\r\n \"created once at pipeline stage 2. Fix: enter the scope once per \" +\r\n \"request; to reach the existing target, use `shared` inside the \" +\r\n \"request context instead.\",\r\n );\r\n }\r\n\r\n const target: SharedTarget = {};\r\n\r\n sharedScopes.set(store, { target, sealed: false });\r\n\r\n return target;\r\n}\r\n\r\nfunction rejectAtGate(path: string, offender: string): never {\r\n throw new Error(\r\n `sealShared(): \\`${path}\\` is ${offender} (web/src/shared.ts prototype ` +\r\n \"gate — runs in production AND dev). `shared` is the audit surface \" +\r\n \"serialized into the page: every value must be plain data (scalars, \" +\r\n \"arrays, plain objects) or carry a `toJSON()` serialization contract \" +\r\n \"(a Resource). A Date/Map/Set/class instance smuggles prototype state \" +\r\n \"past that audit and does not survive serialization intact. Fix: store \" +\r\n \"plain data — an ISO string instead of a Date, an object or array \" +\r\n \"instead of a Map/Set, a Resource (or its `.toJSON()` output) instead \" +\r\n \"of a model or class instance.\",\r\n );\r\n}\r\n\r\n/**\r\n * The prototype gate. Precedence mirrors `Response.parse` exactly so the gate\r\n * and the normalization that follows it agree on every value: toJSON wins over\r\n * the plain-object branch (response.ts:301-305 vs :319) and is NOT descended —\r\n * a Resource's field list is its own contract. Date is\r\n * checked BEFORE toJSON because `Date.prototype.toJSON` exists and Dates are\r\n * rejected regardless.\r\n */\r\nfunction assertClientSafe(value: unknown, path: string): void {\r\n if (value === null || value === undefined) return;\r\n\r\n const valueType = typeof value;\r\n\r\n if (valueType === \"function\") rejectAtGate(path, \"a function\");\r\n if (valueType !== \"object\") return;\r\n\r\n if (value instanceof Date) rejectAtGate(path, \"a Date\");\r\n if (value instanceof Map) rejectAtGate(path, \"a Map\");\r\n if (value instanceof Set) rejectAtGate(path, \"a Set\");\r\n\r\n if (typeof (value as { toJSON?: unknown }).toJSON === \"function\") return;\r\n\r\n if (Array.isArray(value)) {\r\n value.forEach((item, index) => assertClientSafe(item, `${path}[${index}]`));\r\n return;\r\n }\r\n\r\n const proto = Object.getPrototypeOf(value);\r\n\r\n if (proto === Object.prototype || proto === null) {\r\n for (const key of Object.keys(value)) {\r\n assertClientSafe((value as SharedTarget)[key], `${path}.${key}`);\r\n }\r\n return;\r\n }\r\n\r\n const name = (proto?.constructor?.name as string | undefined) ?? \"unknown class\";\r\n rejectAtGate(path, `a class instance (${name})`);\r\n}\r\n\r\nfunction deepFreeze(value: unknown): void {\r\n if (!value || typeof value !== \"object\" || Object.isFrozen(value)) return;\r\n\r\n Object.freeze(value);\r\n\r\n for (const key of Object.keys(value)) {\r\n deepFreeze((value as SharedTarget)[key]);\r\n }\r\n}\r\n\r\nlet browserSharedSnapshot: Readonly<SharedContext> | undefined;\r\nlet browserSharedInstalled = false;\r\n\r\nfunction freezeBrowserSnapshot(value: object, seen: Set<object>): void {\r\n if (seen.has(value)) return;\r\n\r\n seen.add(value);\r\n\r\n for (const key of Object.keys(value)) {\r\n const child = (value as SharedTarget)[key];\r\n\r\n if (child !== null && typeof child === \"object\") {\r\n freezeBrowserSnapshot(child, seen);\r\n }\r\n }\r\n\r\n Object.freeze(value);\r\n}\r\n\r\n/**\r\n * Install the browser's one readonly `shared` snapshot. An object value is\r\n * recursively frozen in place and retained wholesale so every consumer\r\n * observes the same identity; this never reads or writes the server ALS scope.\r\n */\r\nexport function hydrateShared(value: unknown): void {\r\n if (value !== null && typeof value === \"object\") {\r\n freezeBrowserSnapshot(value, new Set<object>());\r\n }\r\n\r\n browserSharedSnapshot = value as Readonly<SharedContext>;\r\n browserSharedInstalled = true;\r\n}\r\n\r\nfunction readBrowserSharedSnapshot(): Readonly<SharedContext> {\r\n if (!browserSharedInstalled) {\r\n throw new Error(\r\n \"Cannot read `shared` via useShared(): the browser snapshot has not been \" +\r\n \"installed (web/src/shared.ts). Hydration must validate the complete \" +\r\n \"#__WARLOCK_DATA__ payload and call hydrateShared() \" +\r\n \"before constructing the React tree.\",\r\n );\r\n }\r\n\r\n return browserSharedSnapshot as Readonly<SharedContext>;\r\n}\r\n\r\n/**\r\n * The settle-stage seal, exported for the pipeline. Order is load-bearing:\r\n *\r\n * 1. PROTOTYPE GATE (pre-parse) — production and dev, offending key named\r\n * with its full path. Fail-fast courtesy: catches most violations before\r\n * spending a parse pass on them.\r\n * 2. `Response.parse` NORMALIZATION — via the store's own response instance\r\n * (core/src/http/response.ts:297, the public surface). parse mutates the\r\n * target IN PLACE (response.ts:327), which is exactly why it MUST precede\r\n * the freeze: freeze-then-parse throws on the first key parse writes\r\n * in place.\r\n * 3. PROTOTYPE GATE AGAIN (post-parse) — closes the re-entry window the first\r\n * gate cannot see: `Response.parse` never re-parses a `toJSON()` result\r\n * (it only normalizes the value handed to it), so a Resource whose `toJSON()` returns a\r\n * Date/Map/Set/class instance re-enters the payload AFTER the first gate\r\n * already ran — the first gate inspects the pre-parse value (a plain\r\n * object with a callable `toJSON`, correctly not descended) and parse then\r\n * puts back exactly what that gate would have rejected. Only a gate that\r\n * runs on the POST-parse target — the object that actually ships — can\r\n * catch it. This is the only gate that matters for correctness; the first\r\n * is a fail-fast courtesy that never gets to be wrong on its own.\r\n * 4. DEEP FREEZE — dev only (env read at seal time). Prod skips the freeze but\r\n * NOT the sealed-write throw below; freezing is defense-in-depth for direct\r\n * target references, the throw is the contract.\r\n * 5. Sealed flag — writes/deletes/defines through `shared` now throw naming\r\n * the key; reads keep working.\r\n *\r\n * `store` defaults to the current request's store; the pipeline holds the\r\n * store either way, since it entered the scope with it. Returns the sealed\r\n * target — serialize THAT, not a re-read of the proxy.\r\n */\r\nexport async function sealShared(store?: SharedStore): Promise<Readonly<SharedContext>> {\r\n const scopeStore = store ?? currentStore(\"seal `shared`\");\r\n const scope = scopeOf(scopeStore, \"seal `shared`\");\r\n\r\n if (scope.sealed) {\r\n throw new Error(\r\n \"sealShared() was called twice for the same request (web/src/shared.ts). \" +\r\n \"The seal is the pipeline's settle stage and runs once, after \" +\r\n \"middleware writes and before serialization. Fix: seal once per \" +\r\n \"request.\",\r\n );\r\n }\r\n\r\n assertClientSafe(scope.target, \"shared\");\r\n\r\n const response = scopeStore.response;\r\n\r\n if (!response || typeof response.parse !== \"function\") {\r\n throw new Error(\r\n \"sealShared() found no `response.parse` on the request store \" +\r\n \"(web/src/shared.ts). Sealing normalizes `shared` through core's \" +\r\n \"public `Response.parse` (core/src/http/response.ts:297), reached via \" +\r\n \"the store's own `response` — the store the pipeline entered the scope \" +\r\n \"with must be core's request store (`{ request, response }`, \" +\r\n \"request-context.ts:10-13). Fix: pass that store (or run sealShared() \" +\r\n \"inside the request context that carries it).\",\r\n );\r\n }\r\n\r\n await response.parse(scope.target);\r\n\r\n assertClientSafe(scope.target, \"shared\");\r\n\r\n /*\r\n OPTIONAL CHAINING IS LOAD-BEARING, and the fallback that used to sit here\r\n is deliberately gone.\r\n\r\n `import.meta.env` is injected by a BUNDLER. When this package is compiled\r\n into an app's server bundle — what happens in this checkout — esbuild's\r\n `define` (`WEB_ESBUILD_PATCH.define` in\r\n `src/build/generate-pages-barrel.ts`) does a literal text-level\r\n match on `import.meta.env.DEV` and replaces it verbatim wherever it\r\n appears, `?.` or not — proven against esbuild's real `transform()`, not\r\n assumed (vite/gate-b-secrets.spec.ts, \"WEB_ESBUILD_PATCH.define\" describe\r\n block): `import.meta.env?.DEV` compiles to `false`, same as the\r\n non-optional form, and no `import.meta` survives in either case. When\r\n `@warlock.js/web` is INSTALLED, it is external to that bundle, nothing\r\n replaces anything, and plain `import.meta.env.DEV` threw `Cannot read\r\n properties of undefined` on EVERY page render. `?.` is what makes the\r\n absent case falsy instead of fatal.\r\n\r\n A `globalThis.process?.env?.NODE_ENV` fallback was tried and REJECTED:\r\n Gate B matches `process.env` only when the object is the bare identifier\r\n `process` (vite/gate-b-secrets.ts:100), so routing through `globalThis`\r\n walks straight past the secret-leak gate. A dev-only freeze is not worth\r\n teaching the codebase the shape that evades that check.\r\n\r\n The cost is honest and small: an installed package running under\r\n `NODE_ENV=development` outside a bundler does not freeze. `DEV` is in Gate\r\n B's own allowlist (gate-b-secrets.ts:44), so this form stays permitted in\r\n client-bound code.\r\n */\r\n if (import.meta.env?.DEV) {\r\n deepFreeze(scope.target);\r\n }\r\n\r\n scope.sealed = true;\r\n\r\n return scope.target as Readonly<SharedContext>;\r\n}\r\n\r\n/**\r\n * The WRITABLE per-request payload — middleware's half of the contract.\r\n *\r\n * Every `shared.x = …` at a call site reads like a global write; it is not —\r\n * each trap below resolves the CURRENT request's target through the connected\r\n * store resolver, on every single access — two concurrent requests writing\r\n * `shared.locale` write to two different objects.\r\n */\r\nexport const shared: SharedContext = new Proxy({} as SharedTarget, {\r\n get(_stub, key) {\r\n return requireScope(`read \\`shared.${String(key)}\\``).target[key];\r\n },\r\n\r\n set(_stub, key, value) {\r\n const scope = requireScope(`write \\`shared.${String(key)}\\``);\r\n\r\n if (scope.sealed) throw sealedWriteError(\"write\", key);\r\n\r\n scope.target[key] = value;\r\n\r\n return true;\r\n },\r\n\r\n has(_stub, key) {\r\n return key in requireScope(`check \\`shared.${String(key)}\\``).target;\r\n },\r\n\r\n deleteProperty(_stub, key) {\r\n const scope = requireScope(`delete \\`shared.${String(key)}\\``);\r\n\r\n if (scope.sealed) throw sealedWriteError(\"delete\", key);\r\n\r\n delete scope.target[key];\r\n\r\n return true;\r\n },\r\n\r\n ownKeys() {\r\n return Reflect.ownKeys(requireScope(\"enumerate `shared`\").target);\r\n },\r\n\r\n getOwnPropertyDescriptor(_stub, key) {\r\n const descriptor = Object.getOwnPropertyDescriptor(\r\n requireScope(`describe \\`shared.${String(key)}\\``).target,\r\n key,\r\n );\r\n\r\n if (!descriptor) return undefined;\r\n\r\n // The proxy's book-keeping target is the empty stub, so a frozen real\r\n // target's non-configurable descriptors would violate proxy invariants if\r\n // reported as-is; configurable:true is the honest report for the proxy\r\n // surface (the sealed-write throw is what enforces immutability).\r\n return { ...descriptor, configurable: true };\r\n },\r\n\r\n defineProperty(_stub, key, descriptor) {\r\n const scope = requireScope(`define \\`shared.${String(key)}\\``);\r\n\r\n if (scope.sealed) throw sealedWriteError(\"define\", key);\r\n\r\n Reflect.defineProperty(scope.target, key, descriptor);\r\n\r\n return true;\r\n },\r\n});\r\n\r\n/**\r\n * The READ half for components at depth. On the server it preserves the live\r\n * ALS proxy behavior. In the browser it returns the exact recursively frozen\r\n * object installed from the validated hydration payload, never an empty or\r\n * process-wide fallback.\r\n */\r\nexport function useShared(): Readonly<SharedContext> {\r\n if (typeof window !== \"undefined\") return readBrowserSharedSnapshot();\r\n\r\n requireScope(\"read `shared` via useShared()\");\r\n\r\n return shared as Readonly<SharedContext>;\r\n}\r\n"],"mappings":"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"}
|
package/esm/validation.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Infer } from "@warlock.js/seal";
|
|
2
2
|
|
|
3
|
-
//#region web/src/validation.d.ts
|
|
3
|
+
//#region ../web/src/validation.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The page's `validation` export: a Seal schema plus what to validate it
|
|
6
6
|
* against. `schema` is kept structural (`unknown`) at the constraint so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-convention-aliases.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"app-convention-aliases.mjs","names":[],"sources":["../../../../../../../web/src/vite/app-convention-aliases.ts"],"sourcesContent":["import path from \"node:path\";\n\n/**\n * One entry of Vite's array-form `resolve.alias`.\n *\n * Declared structurally rather than imported from `vite` so this module has no\n * dependency on the optional peer: it is read by the dev server's bootstrap and\n * by the production build contribution, and neither should pull Vite in merely\n * to learn what the app-convention aliases are.\n */\nexport type AppConventionAlias = {\n find: RegExp;\n replacement: string;\n};\n\n/**\n * The app-tree import convention — `web/*` and `app/*` — expressed as Vite\n * aliases.\n *\n * These mirror the `paths` an application's own `tsconfig.json` declares. Vite\n * does not read tsconfig `paths` on its own, and no `vite-tsconfig-paths`\n * plugin is installed in this workspace, so every pipeline that resolves app\n * source has to be told about them explicitly.\n *\n * WHY THIS IS A SHARED FUNCTION AND NOT TWO LITERALS\n *\n * It used to be two literals. The dev server had them; the production build did\n * not — it passed `options.aliases ?? {}` — so `warlock dev` resolved\n * `web/components/...` and the production client build died on the first page\n * it met:\n *\n * [vite]: Rollup failed to resolve import \"web/components/checkbox-input\"\n *\n * Dev worked and production could not build at all, for as long as nobody ran\n * the production client build to completion. Copying the pair to a second call\n * site would have fixed that instance and guaranteed the next one, so there is\n * exactly one definition and both pipelines read it.\n *\n * @param appSrcRoot Absolute path to the application's source root — the\n * directory holding `web/` and `app/`. Both callers default it to\n * `<appRoot>/src`.\n */\nexport function appConventionAliases(appSrcRoot: string): AppConventionAlias[] {\n return [\n { find: /^web\\//, replacement: `${path.join(appSrcRoot, \"web\")}/` },\n { find: /^app\\//, replacement: `${path.join(appSrcRoot, \"app\")}/` },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,qBAAqB,YAA0C;CAC7E,OAAO,CACL;EAAE,MAAM;EAAU,aAAa,GAAG,KAAK,KAAK,YAAY,KAAK,EAAE;CAAG,GAClE;EAAE,MAAM;EAAU,aAAa,GAAG,KAAK,KAAK,YAAY,KAAK,EAAE;CAAG,CACpE;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HydrationClientEntry } from "./hydration-entries.mjs";
|
|
2
2
|
import { AliasOptions, PluginOption, Rollup } from "vite";
|
|
3
3
|
|
|
4
|
-
//#region web/src/vite/build-client.d.ts
|
|
4
|
+
//#region ../web/src/vite/build-client.d.ts
|
|
5
5
|
interface BuildHydrationClientOptions {
|
|
6
6
|
/** `@warlock.js/web` root containing the packaged or checkout hydration entry. */
|
|
7
7
|
webRoot: string;
|
|
@@ -2,7 +2,7 @@ import { createHydrationClientEntry } from "./hydration-entries.mjs";
|
|
|
2
2
|
import { statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
//#region web/src/vite/build-client.ts
|
|
5
|
+
//#region ../web/src/vite/build-client.ts
|
|
6
6
|
function assertEntryFile(entry) {
|
|
7
7
|
let entryStat;
|
|
8
8
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-client.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"build-client.mjs","names":[],"sources":["../../../../../../../web/src/vite/build-client.ts"],"sourcesContent":["import { statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { AliasOptions, PluginOption, Rollup } from \"vite\";\nimport { createHydrationClientEntry, type HydrationClientEntry } from \"./hydration-entries\";\n\nexport interface BuildHydrationClientOptions {\n /** `@warlock.js/web` root containing the packaged or checkout hydration entry. */\n webRoot: string;\n /**\n * Absolute output directory for the client bundle — REQUIRED.\n *\n * Was hardcoded to `<webRoot>/dist/client`, which wrote the artifacts into\n * the framework package rather than the app's build output. The caller owns\n * the build layout (`<outdir>/client`, contract §1), so it passes the\n * directory; `manifestPath` is derived from it.\n */\n outDir: string;\n /** The caller-composed projection and boundary-gate plugin pipeline. */\n plugins: readonly PluginOption[];\n /** The caller-owned application and workspace source aliases. */\n resolveAliases: AliasOptions;\n /** Optional peers that must remain external to this bundler pipeline. */\n external?: Rollup.ExternalOption;\n}\n\nexport type HydrationClientBuildOutput = Rollup.RollupOutput | Rollup.RollupOutput[];\n\nexport interface BuildHydrationClientResult {\n entry: HydrationClientEntry;\n outDir: string;\n manifestPath: string;\n output: HydrationClientBuildOutput;\n}\n\nfunction assertEntryFile(entry: HydrationClientEntry): void {\n let entryStat: ReturnType<typeof statSync>;\n\n try {\n entryStat = statSync(entry.sourcePath);\n } catch (error) {\n throw new Error(\n `Cannot build the hydration client: entry \"${entry.sourcePath}\" is missing or unreadable.`,\n { cause: error },\n );\n }\n\n if (!entryStat.isFile()) {\n throw new Error(`Cannot build the hydration client: entry \"${entry.sourcePath}\" is not a file.`);\n }\n}\n\nfunction assertBuildOptions(options: BuildHydrationClientOptions): void {\n if (!options || typeof options !== \"object\") {\n throw new TypeError(\"Cannot build the hydration client: options are required.\");\n }\n\n if (!Array.isArray(options.plugins) || options.plugins.length === 0) {\n throw new TypeError(\n \"Cannot build the hydration client: the caller must provide its composed boundary plugins.\",\n );\n }\n\n if (options.resolveAliases === undefined || options.resolveAliases === null) {\n throw new TypeError(\n \"Cannot build the hydration client: the caller must provide its resolve aliases.\",\n );\n }\n\n // An EMPTY table is rejected too, and that is the whole point of this branch.\n //\n // The not-null check above looks like it already covers a missing alias table.\n // It did not: the call site read `options.aliases ?? {}`, and `??` manufactures\n // an empty object that is neither undefined nor null — so the guard passed and\n // the build proceeded misconfigured, failing much later with a bare Rollup\n // \"failed to resolve import\" that named none of this.\n //\n // An app whose source uses alias imports cannot be built with zero aliases, so\n // absent and present-but-empty are the same error and must be reported the same\n // way, here, by name.\n if (\n (Array.isArray(options.resolveAliases) && options.resolveAliases.length === 0) ||\n (!Array.isArray(options.resolveAliases) && Object.keys(options.resolveAliases).length === 0)\n ) {\n throw new TypeError(\n \"Cannot build the hydration client: the resolve alias table is empty. \" +\n \"App source that imports via `web/*` or `app/*` cannot resolve without it.\",\n );\n }\n\n if (typeof options.outDir !== \"string\" || options.outDir.trim().length === 0) {\n throw new TypeError(\n \"Cannot build the hydration client: the caller must provide an absolute outDir.\",\n );\n }\n}\n\n/**\n * Builds the one browser hydration entry. Vite is an optional peer and is\n * imported only after the caller explicitly invokes this build operation.\n */\nexport async function buildHydrationClient(\n options: BuildHydrationClientOptions,\n): Promise<BuildHydrationClientResult> {\n assertBuildOptions(options);\n\n const entry = createHydrationClientEntry(options.webRoot);\n assertEntryFile(entry);\n\n const outDir = path.resolve(options.outDir);\n const manifestPath = path.join(outDir, \".vite/manifest.json\");\n const { build } = await import(\"vite\");\n const viteResult = await build({\n root: options.webRoot,\n appType: \"custom\",\n configFile: false,\n plugins: [...options.plugins],\n resolve: { alias: options.resolveAliases },\n build: {\n copyPublicDir: false,\n emptyOutDir: true,\n manifest: true,\n outDir,\n target: \"es2022\",\n rollupOptions: {\n external: options.external,\n input: { [entry.name]: entry.sourcePath },\n output: {\n assetFileNames: \"assets/[name]-[hash][extname]\",\n chunkFileNames: \"assets/[name]-[hash].js\",\n entryFileNames: \"assets/[name]-[hash].js\",\n format: \"es\",\n },\n },\n watch: null,\n },\n });\n\n if (!Array.isArray(viteResult) && !(\"output\" in viteResult)) {\n await viteResult.close();\n throw new Error(\"Cannot build the hydration client: Vite unexpectedly returned a watcher.\");\n }\n\n return { entry, outDir, manifestPath, output: viteResult };\n}\n"],"mappings":";;;;;AAkCA,SAAS,gBAAgB,OAAmC;CAC1D,IAAI;CAEJ,IAAI;EACF,YAAY,SAAS,MAAM,UAAU;CACvC,SAAS,OAAO;EACd,MAAM,IAAI,MACR,6CAA6C,MAAM,WAAW,8BAC9D,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,IAAI,CAAC,UAAU,OAAO,GACpB,MAAM,IAAI,MAAM,6CAA6C,MAAM,WAAW,iBAAiB;AAEnG;AAEA,SAAS,mBAAmB,SAA4C;CACtE,IAAI,CAAC,WAAW,OAAO,YAAY,UACjC,MAAM,IAAI,UAAU,0DAA0D;CAGhF,IAAI,CAAC,MAAM,QAAQ,QAAQ,OAAO,KAAK,QAAQ,QAAQ,WAAW,GAChE,MAAM,IAAI,UACR,2FACF;CAGF,IAAI,QAAQ,mBAAmB,UAAa,QAAQ,mBAAmB,MACrE,MAAM,IAAI,UACR,iFACF;CAcF,IACG,MAAM,QAAQ,QAAQ,cAAc,KAAK,QAAQ,eAAe,WAAW,KAC3E,CAAC,MAAM,QAAQ,QAAQ,cAAc,KAAK,OAAO,KAAK,QAAQ,cAAc,EAAE,WAAW,GAE1F,MAAM,IAAI,UACR,gJAEF;CAGF,IAAI,OAAO,QAAQ,WAAW,YAAY,QAAQ,OAAO,KAAK,EAAE,WAAW,GACzE,MAAM,IAAI,UACR,gFACF;AAEJ;;;;;AAMA,eAAsB,qBACpB,SACqC;CACrC,mBAAmB,OAAO;CAE1B,MAAM,QAAQ,2BAA2B,QAAQ,OAAO;CACxD,gBAAgB,KAAK;CAErB,MAAM,SAAS,KAAK,QAAQ,QAAQ,MAAM;CAC1C,MAAM,eAAe,KAAK,KAAK,QAAQ,qBAAqB;CAC5D,MAAM,EAAE,UAAU,MAAM,OAAO;CAC/B,MAAM,aAAa,MAAM,MAAM;EAC7B,MAAM,QAAQ;EACd,SAAS;EACT,YAAY;EACZ,SAAS,CAAC,GAAG,QAAQ,OAAO;EAC5B,SAAS,EAAE,OAAO,QAAQ,eAAe;EACzC,OAAO;GACL,eAAe;GACf,aAAa;GACb,UAAU;GACV;GACA,QAAQ;GACR,eAAe;IACb,UAAU,QAAQ;IAClB,OAAO,GAAG,MAAM,OAAO,MAAM,WAAW;IACxC,QAAQ;KACN,gBAAgB;KAChB,gBAAgB;KAChB,gBAAgB;KAChB,QAAQ;IACV;GACF;GACA,OAAO;EACT;CACF,CAAC;CAED,IAAI,CAAC,MAAM,QAAQ,UAAU,KAAK,EAAE,YAAY,aAAa;EAC3D,MAAM,WAAW,MAAM;EACvB,MAAM,IAAI,MAAM,0EAA0E;CAC5F;CAEA,OAAO;EAAE;EAAO;EAAQ;EAAc,QAAQ;CAAW;AAC3D"}
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { parse } from "@babel/parser";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
//#region web/src/vite/gate-a-resolve.ts
|
|
7
|
+
//#region ../web/src/vite/gate-a-resolve.ts
|
|
8
8
|
/**
|
|
9
9
|
* Gate A — `resolveId` refusal for the client build.
|
|
10
10
|
*
|