@thomasfosterau/effect-sveltekit 0.6.0 → 0.8.0

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 (119) hide show
  1. package/README.md +37 -21
  2. package/dist/Hooks.d.ts +3 -3
  3. package/dist/Hooks.js +1 -1
  4. package/dist/Hooks.js.map +1 -1
  5. package/dist/Navigation.d.ts +11 -11
  6. package/dist/Navigation.js +15 -2
  7. package/dist/Navigation.js.map +1 -1
  8. package/dist/Remote.d.ts +18 -17
  9. package/dist/Remote.js +2 -4
  10. package/dist/Remote.js.map +1 -1
  11. package/dist/State.d.ts +10 -6
  12. package/dist/State.js +2 -1
  13. package/dist/State.js.map +1 -1
  14. package/dist/adapter/assets.d.ts +24 -7
  15. package/dist/adapter/assets.js +58 -37
  16. package/dist/adapter/assets.js.map +1 -1
  17. package/dist/adapter/handler.d.ts +27 -12
  18. package/dist/adapter/handler.js +53 -26
  19. package/dist/adapter/handler.js.map +1 -1
  20. package/dist/adapter/index.d.ts +3 -3
  21. package/dist/adapter/index.js +1 -1
  22. package/dist/adapter/index.js.map +1 -1
  23. package/dist/adapter/serve.d.ts +6 -5
  24. package/dist/adapter/serve.js +3 -1
  25. package/dist/adapter/serve.js.map +1 -1
  26. package/dist/api-client.d.ts +3 -4
  27. package/dist/api-client.js +2 -1
  28. package/dist/api-client.js.map +1 -1
  29. package/dist/app-runtime.d.ts +12 -9
  30. package/dist/app-runtime.js +23 -14
  31. package/dist/app-runtime.js.map +1 -1
  32. package/dist/client/Hooks.d.ts +4 -4
  33. package/dist/client/Hooks.js +2 -1
  34. package/dist/client/Hooks.js.map +1 -1
  35. package/dist/client.d.ts +2 -2
  36. package/dist/config.d.ts +7 -5
  37. package/dist/config.js +5 -34
  38. package/dist/config.js.map +1 -1
  39. package/dist/errors.d.ts +14 -16
  40. package/dist/errors.js +20 -12
  41. package/dist/errors.js.map +1 -1
  42. package/dist/forms.d.ts +9 -10
  43. package/dist/forms.js +5 -2
  44. package/dist/forms.js.map +1 -1
  45. package/dist/index.d.ts +5 -4
  46. package/dist/index.js +5 -4
  47. package/dist/internal/app-state.d.ts +16 -14
  48. package/dist/internal/app-state.js +9 -3
  49. package/dist/internal/app-state.js.map +1 -1
  50. package/dist/internal/auto.d.ts +23 -14
  51. package/dist/internal/auto.js +50 -40
  52. package/dist/internal/auto.js.map +1 -1
  53. package/dist/internal/enhance.d.ts +20 -18
  54. package/dist/internal/enhance.js +3 -1
  55. package/dist/internal/enhance.js.map +1 -1
  56. package/dist/internal/error.d.ts +13 -0
  57. package/dist/internal/error.js +4 -3
  58. package/dist/internal/error.js.map +1 -1
  59. package/dist/internal/navigation-events.js +3 -1
  60. package/dist/internal/navigation-events.js.map +1 -1
  61. package/dist/internal/remote-run.d.ts +6 -6
  62. package/dist/internal/remote-run.js +13 -16
  63. package/dist/internal/remote-run.js.map +1 -1
  64. package/dist/internal/remote-validators.js +4 -3
  65. package/dist/internal/remote-validators.js.map +1 -1
  66. package/dist/internal/route-params.d.ts +1 -2
  67. package/dist/internal/sse.js +2 -1
  68. package/dist/internal/sse.js.map +1 -1
  69. package/dist/kit.d.ts +12 -11
  70. package/dist/kit.js +6 -2
  71. package/dist/kit.js.map +1 -1
  72. package/dist/realtime.d.ts +29 -31
  73. package/dist/realtime.js +23 -4
  74. package/dist/realtime.js.map +1 -1
  75. package/dist/remote-functions.d.ts +34 -18
  76. package/dist/remote-functions.js +78 -46
  77. package/dist/remote-functions.js.map +1 -1
  78. package/dist/remote-reactive.d.ts +8 -4
  79. package/dist/remote-reactive.js.map +1 -1
  80. package/dist/request-parse.d.ts +13 -13
  81. package/dist/request-parse.js +29 -15
  82. package/dist/request-parse.js.map +1 -1
  83. package/dist/request-runtime.d.ts +16 -11
  84. package/dist/request-runtime.js +28 -29
  85. package/dist/request-runtime.js.map +1 -1
  86. package/dist/request-services.d.ts +10 -8
  87. package/dist/request-services.js +3 -1
  88. package/dist/request-services.js.map +1 -1
  89. package/dist/response-headers.d.ts +2 -3
  90. package/dist/response-headers.js +2 -1
  91. package/dist/response-headers.js.map +1 -1
  92. package/dist/runtime.d.ts +8 -5
  93. package/dist/runtime.js +4 -1
  94. package/dist/runtime.js.map +1 -1
  95. package/dist/server/Endpoint.d.ts +17 -11
  96. package/dist/server/Endpoint.js +18 -4
  97. package/dist/server/Endpoint.js.map +1 -1
  98. package/dist/server/Hooks.d.ts +6 -6
  99. package/dist/server/Hooks.js +1 -1
  100. package/dist/server/Hooks.js.map +1 -1
  101. package/dist/server.d.ts +8 -7
  102. package/dist/server.js +8 -8
  103. package/dist/server.js.map +1 -1
  104. package/dist/streamed-load.d.ts +2 -3
  105. package/dist/streamed-load.js +1 -1
  106. package/dist/streamed-load.js.map +1 -1
  107. package/dist/testing.d.ts +15 -14
  108. package/dist/testing.js +27 -7
  109. package/dist/testing.js.map +1 -1
  110. package/dist/transport.d.ts +6 -7
  111. package/dist/transport.js +3 -6
  112. package/dist/transport.js.map +1 -1
  113. package/dist/universal.d.ts +3 -3
  114. package/dist/universal.js +1 -1
  115. package/dist/universal.js.map +1 -1
  116. package/dist/vite.d.ts +4 -4
  117. package/dist/vite.js +102 -103
  118. package/dist/vite.js.map +1 -1
  119. package/package.json +12 -9
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Framework-agnostic test utilities for `@thomasfosterau/effect-sveltekit`.\n *\n * Exports a minimal {@link TestRequestEvent} constructor and plain-async\n * runner helpers ({@link runServerLoad}, {@link runAction},\n * {@link runEndpoint}, {@link runRemote}) so Effect-based SvelteKit handlers\n * can be unit-tested without mocking the SvelteKit module graph or installing\n * `$app/server`.\n *\n * Ships under the `./testing` subpath of `@thomasfosterau/effect-sveltekit`.\n * It has no coupling to Vitest, Jest, or any other test framework — import\n * it from whichever test runner you use.\n *\n * @example\n * ```ts\n * import {\n * TestRequestEvent,\n * runServerLoad,\n * runAction,\n * runRemote,\n * } from \"@thomasfosterau/effect-sveltekit/testing\";\n * import { Effect } from \"effect\";\n *\n * const event = TestRequestEvent({ params: { id: \"1\" }, locals: { user } });\n *\n * // Test a server load:\n * const data = await runServerLoad(\n * (event) =>\n * Effect.gen(function* () {\n * const db = yield* Database;\n * return { item: yield* db.findById(event.params.id) };\n * }),\n * event,\n * { layer: TestDatabase.layer },\n * );\n *\n * // Test a remote function handler directly:\n * const { run } = runRemote(event, { layer: TestLayer });\n * const items = await run(listItems());\n * ```\n */\n\nimport type { RequestEvent } from \"@sveltejs/kit\";\nimport { Effect, Layer } from \"effect\";\nimport {\n makeRuntimeAdapter,\n RequestRuntime,\n type ErrorTranslator,\n type RequestRuntimeProvider,\n} from \"./request-runtime.js\";\nimport { SvelteKit } from \"./kit.js\";\nimport { makeRunHelpers, type FormErrorMapper, type RunHelpers } from \"./internal/remote-run.js\";\n\nexport type { RunHelpers, FormErrorMapper } from \"./internal/remote-run.js\";\n\n// ─── TestCookies ─────────────────────────────────────────────────────────────\n\n/** Map-backed Cookies implementation for use in {@link TestRequestEvent}. */\nclass TestCookies {\n private readonly store: Map<string, string>;\n\n constructor(initial: Record<string, string> = {}) {\n this.store = new Map(Object.entries(initial));\n }\n\n get(name: string, _opts?: unknown): string | undefined {\n return this.store.get(name);\n }\n\n getAll(_opts?: unknown): Array<{ name: string; value: string }> {\n return [...this.store.entries()].map(([name, value]) => ({ name, value }));\n }\n\n set(name: string, value: string, _opts?: unknown): void {\n this.store.set(name, value);\n }\n\n delete(name: string, _opts?: unknown): void {\n this.store.delete(name);\n }\n\n serialize(name: string, value: string, _opts?: unknown): string {\n return `${name}=${encodeURIComponent(value)}`;\n }\n}\n\n// ─── TestRequestEvent ─────────────────────────────────────────────────────────\n\n/** Options for {@link TestRequestEvent}. */\nexport interface TestRequestEventOptions {\n /**\n * The request URL. Accepts a `string` or a `URL` instance.\n * Defaults to `\"http://localhost/\"`.\n */\n readonly url?: string | URL;\n /** The HTTP method. Defaults to `\"GET\"`. */\n readonly method?: string;\n /** Additional HTTP headers added to the underlying `Request`. */\n readonly headers?: HeadersInit;\n /** The request body. Only usable with methods that accept a body. */\n readonly body?: BodyInit | null;\n /**\n * Route params (e.g. `{ id: \"123\" }` for a route like `/items/[id]`).\n * Defaults to `{}`.\n */\n readonly params?: Record<string, string>;\n /**\n * Initial `event.locals` value. Defaults to `{}`.\n * Cast to `App.Locals` — extend it with the properties your handlers need.\n */\n readonly locals?: Record<string, unknown>;\n /**\n * Pre-seeded cookies as `{ name: value }` pairs.\n * The backing store supports `get`, `getAll`, `set`, `delete`, and\n * `serialize`; the object is cast to SvelteKit's `Cookies` interface.\n */\n readonly cookies?: Record<string, string>;\n /**\n * The `event.platform` value, if any.\n * Pass a partial platform object matching the environment the handler targets.\n */\n readonly platform?: unknown;\n /**\n * The `fetch` implementation used by `event.fetch`.\n * Defaults to `globalThis.fetch`. Pass a custom function to intercept or\n * stub network calls made by the handler.\n */\n readonly fetch?: typeof globalThis.fetch;\n}\n\n/**\n * Create a minimal {@link RequestEvent} for use in unit tests.\n *\n * All fields are optional — omit what the handler under test doesn't inspect.\n * Cookies are backed by an in-memory `Map`; mutations (`set`, `delete`) are\n * reflected in subsequent `get`/`getAll` calls on the same event.\n *\n * @example\n * ```ts\n * const event = TestRequestEvent({\n * url: \"http://localhost/items/42\",\n * params: { id: \"42\" },\n * locals: { user: { id: \"u1\", role: \"admin\" } },\n * cookies: { session: \"tok_test\" },\n * });\n * ```\n */\nexport function TestRequestEvent(opts: TestRequestEventOptions = {}): RequestEvent {\n const rawUrl = opts.url instanceof URL ? opts.url.toString() : (opts.url ?? \"http://localhost/\");\n const url = new URL(rawUrl);\n\n const init: RequestInit = { method: opts.method ?? \"GET\" };\n if (opts.headers !== undefined) init.headers = opts.headers;\n if (opts.body !== undefined) init.body = opts.body;\n\n const request = new Request(url.toString(), init);\n const cookies = new TestCookies(opts.cookies ?? {});\n const fetchFn = opts.fetch ?? globalThis.fetch;\n\n return {\n request,\n url,\n params: (opts.params ?? {}) as any,\n route: { id: null },\n fetch: fetchFn,\n cookies: cookies as any,\n locals: (opts.locals ?? {}) as any,\n platform: opts.platform as any,\n isDataRequest: false,\n isSubRequest: false,\n setHeaders: () => {},\n getClientAddress: () => \"127.0.0.1\",\n } as unknown as RequestEvent;\n}\n\n// ─── Options ──────────────────────────────────────────────────────────────────\n\n/**\n * Options shared by {@link runServerLoad}, {@link runAction}, and\n * {@link runEndpoint}.\n */\nexport interface TestRunOptions<R> {\n /**\n * The `Layer` providing the handler effect's required services `R`.\n * Omit when the handler has no service requirements (`R` is `never`).\n */\n readonly layer?: Layer.Layer<R>;\n /**\n * Translate a failed `Cause` at the boundary. Defaults to\n * {@link SvelteKit.translate} (Kit control-flow through; anything else\n * becomes an `EffectCauseError`).\n */\n readonly translateError?: ErrorTranslator;\n}\n\n/**\n * Options for {@link runRemote}.\n */\nexport interface RunRemoteOptions<R> {\n /** The `Layer` providing the remote handler's service requirements. */\n readonly layer?: Layer.Layer<R>;\n /**\n * Translate a failed `Cause` at the boundary. Defaults to\n * {@link SvelteKit.translate}.\n */\n readonly translateError?: ErrorTranslator;\n /**\n * Map a remote form handler's failure to SvelteKit field issues. Receives\n * the squashed handler failure and the `{ issue, data }` context. When\n * provided it is called instead of `translateError` on the form error path.\n */\n readonly formError?: FormErrorMapper;\n}\n\n// ─── Internal ─────────────────────────────────────────────────────────────────\n\nconst makeAdapter = <R>(opts: TestRunOptions<R> | undefined) =>\n makeRuntimeAdapter(RequestRuntime.fromLayer((opts?.layer ?? Layer.empty) as Layer.Layer<R>), {\n translateError: opts?.translateError ?? SvelteKit.translate,\n });\n\n// ─── runServerLoad ────────────────────────────────────────────────────────────\n\n/**\n * Run an Effect-based server `load` function under test.\n *\n * Wraps `fn` with `makeRuntimeAdapter` using the provided `opts.layer` (or\n * `Layer.empty` when omitted) and calls the resulting `ServerLoad` with\n * `event`. Errors are translated by `opts.translateError` (defaults to\n * {@link SvelteKit.translate}).\n *\n * @example\n * ```ts\n * const data = await runServerLoad(\n * (event) => Effect.gen(function* () {\n * const db = yield* Database;\n * return { items: yield* db.list() };\n * }),\n * TestRequestEvent(),\n * { layer: TestDatabase.layer },\n * );\n * expect(data.items).toHaveLength(0);\n * ```\n */\nexport const runServerLoad = <R, A extends Record<string, any> | void = void>(\n fn: (event: RequestEvent) => Effect.Effect<A, unknown, R>,\n event: RequestEvent,\n opts?: TestRunOptions<R>,\n): Promise<A> => (makeAdapter(opts).serverLoad(fn) as (e: RequestEvent) => Promise<A>)(event);\n\n// ─── runAction ───────────────────────────────────────────────────────────────\n\n/**\n * Run an Effect-based form action handler under test.\n *\n * @example\n * ```ts\n * const result = await runAction(\n * (_event) => Effect.succeed({ success: true }),\n * TestRequestEvent({ method: \"POST\" }),\n * );\n * expect((result as any).success).toBe(true);\n * ```\n */\nexport const runAction = <R, A extends Record<string, any> | void = void>(\n fn: (event: RequestEvent) => Effect.Effect<A, unknown, R>,\n event: RequestEvent,\n opts?: TestRunOptions<R>,\n): Promise<A> => (makeAdapter(opts).action(fn) as (e: RequestEvent) => Promise<A>)(event);\n\n// ─── runEndpoint ──────────────────────────────────────────────────────────────\n\n/**\n * Run an Effect-based `+server.ts` endpoint handler under test.\n *\n * @example\n * ```ts\n * const response = await runEndpoint(\n * (_event) => Effect.succeed(new Response(JSON.stringify({ ok: true }))),\n * TestRequestEvent({ url: \"http://localhost/api/ping\" }),\n * );\n * expect(response.status).toBe(200);\n * ```\n */\nexport const runEndpoint = <R>(\n fn: (event: RequestEvent) => Effect.Effect<Response, unknown, R>,\n event: RequestEvent,\n opts?: TestRunOptions<R>,\n): Promise<Response> => Promise.resolve(makeAdapter(opts).endpoint(fn)(event as any));\n\n// ─── runRemote ────────────────────────────────────────────────────────────────\n\n/**\n * Build a `{ run, runForm }` helper pair for directly invoking remote-function\n * Effect handlers under test.\n *\n * Unlike the production `query` / `command` / `form` helpers, which resolve the\n * request event via `$app/server.getRequestEvent()`, this resolves it from the\n * `event` argument you supply — no SvelteKit module-graph mocking required.\n *\n * Use `run` for `query` / `command` handlers and `runForm` for `form` handlers\n * that need the `issue` / `data` context.\n *\n * @example\n * ```ts\n * import { TestRequestEvent, runRemote } from \"@thomasfosterau/effect-sveltekit/testing\";\n *\n * const event = TestRequestEvent();\n * const { run } = runRemote(event, { layer: TestDatabase.layer });\n *\n * // Invoke the underlying Effect directly — no AppServer needed:\n * const items = await run(listItemsEffect());\n * expect(items).toEqual([]);\n * ```\n */\nexport const runRemote = <R = never>(\n event: RequestEvent,\n opts?: RunRemoteOptions<R>,\n): RunHelpers => {\n const translate = opts?.translateError ?? SvelteKit.translate;\n const layer = (opts?.layer ?? Layer.empty) as Layer.Layer<R>;\n const provider: RequestRuntimeProvider<R> = RequestRuntime.fromLayer(layer);\n return makeRunHelpers(\n () => ({ runExit: (effect) => provider.run(event).runPromiseExit(effect) }),\n translate,\n opts?.formError,\n );\n};\n"],"mappings":";;;;;;AA0DA,IAAM,cAAN,MAAkB;CAChB;CAEA,YAAY,UAAkC,CAAC,GAAG;EAChD,KAAK,QAAQ,IAAI,IAAI,OAAO,QAAQ,OAAO,CAAC;CAC9C;CAEA,IAAI,MAAc,OAAqC;EACrD,OAAO,KAAK,MAAM,IAAI,IAAI;CAC5B;CAEA,OAAO,OAAyD;EAC9D,OAAO,CAAC,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY;GAAE;GAAM;EAAM,EAAE;CAC3E;CAEA,IAAI,MAAc,OAAe,OAAuB;EACtD,KAAK,MAAM,IAAI,MAAM,KAAK;CAC5B;CAEA,OAAO,MAAc,OAAuB;EAC1C,KAAK,MAAM,OAAO,IAAI;CACxB;CAEA,UAAU,MAAc,OAAe,OAAyB;EAC9D,OAAO,GAAG,KAAK,GAAG,mBAAmB,KAAK;CAC5C;AACF;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,iBAAiB,OAAgC,CAAC,GAAiB;CACjF,MAAM,SAAS,KAAK,eAAe,MAAM,KAAK,IAAI,SAAS,IAAK,KAAK,OAAO;CAC5E,MAAM,MAAM,IAAI,IAAI,MAAM;CAE1B,MAAM,OAAoB,EAAE,QAAQ,KAAK,UAAU,MAAM;CACzD,IAAI,KAAK,YAAY,KAAA,GAAW,KAAK,UAAU,KAAK;CACpD,IAAI,KAAK,SAAS,KAAA,GAAW,KAAK,OAAO,KAAK;CAE9C,MAAM,UAAU,IAAI,QAAQ,IAAI,SAAS,GAAG,IAAI;CAChD,MAAM,UAAU,IAAI,YAAY,KAAK,WAAW,CAAC,CAAC;CAClD,MAAM,UAAU,KAAK,SAAS,WAAW;CAEzC,OAAO;EACL;EACA;EACA,QAAS,KAAK,UAAU,CAAC;EACzB,OAAO,EAAE,IAAI,KAAK;EAClB,OAAO;EACE;EACT,QAAS,KAAK,UAAU,CAAC;EACzB,UAAU,KAAK;EACf,eAAe;EACf,cAAc;EACd,kBAAkB,CAAC;EACnB,wBAAwB;CAC1B;AACF;AA2CA,MAAM,eAAkB,SACtB,mBAAmB,eAAe,UAAW,MAAM,SAAS,MAAM,KAAwB,GAAG,EAC3F,gBAAgB,MAAM,kBAAkB,UAAU,UACpD,CAAC;;;;;;;;;;;;;;;;;;;;;;AAyBH,MAAa,iBACX,IACA,OACA,SACgB,YAAY,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAqC,KAAK;;;;;;;;;;;;;AAgB5F,MAAa,aACX,IACA,OACA,SACgB,YAAY,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAqC,KAAK;;;;;;;;;;;;;AAgBxF,MAAa,eACX,IACA,OACA,SACsB,QAAQ,QAAQ,YAAY,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,KAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AA2BpF,MAAa,aACX,OACA,SACe;CACf,MAAM,YAAY,MAAM,kBAAkB,UAAU;CACpD,MAAM,QAAS,MAAM,SAAS,MAAM;CACpC,MAAM,WAAsC,eAAe,UAAU,KAAK;CAC1E,OAAO,sBACE,EAAE,UAAU,WAAW,SAAS,IAAI,KAAK,CAAC,CAAC,eAAe,MAAM,EAAE,IACzE,WACA,MAAM,SACR;AACF"}
1
+ {"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Framework-agnostic test utilities for `@thomasfosterau/effect-sveltekit`.\n *\n * Exports a minimal {@link TestRequestEvent} constructor and plain-async\n * runner helpers ({@link runServerLoad}, {@link runAction},\n * {@link runEndpoint}, {@link runRemote}) so Effect-based SvelteKit handlers\n * can be unit-tested without mocking the SvelteKit module graph or installing\n * `$app/server`.\n *\n * Ships under the `./testing` subpath of `@thomasfosterau/effect-sveltekit`.\n * It has no coupling to Vitest, Jest, or any other test framework — import\n * it from whichever test runner you use.\n *\n * @example\n * ```ts\n * import {\n * TestRequestEvent,\n * runServerLoad,\n * runAction,\n * runRemote,\n * } from \"@thomasfosterau/effect-sveltekit/testing\";\n * import { Effect } from \"effect\";\n *\n * const event = TestRequestEvent({ params: { id: \"1\" }, locals: { user } });\n *\n * // Test a server load:\n * const data = await runServerLoad(\n * (event) =>\n * Effect.gen(function* () {\n * const db = yield* Database;\n * return { item: yield* db.findById(event.params.id) };\n * }),\n * event,\n * { layer: TestDatabase.layer },\n * );\n *\n * // Test a remote function handler directly:\n * const { run } = runRemote(event, { layer: TestLayer });\n * const items = await run(listItems());\n * ```\n */\n\nimport type { Cookies, RequestEvent } from \"@sveltejs/kit\";\nimport * as Effect from \"effect/Effect\";\nimport * as Arr from \"effect/Array\";\nimport * as Layer from \"effect/Layer\";\nimport * as Option from \"effect/Option\";\nimport * as Rec from \"effect/Record\";\nimport {\n makeRuntimeAdapter,\n RequestRuntime,\n type ErrorTranslator,\n type RequestRuntimeProvider,\n} from \"./request-runtime.js\";\nimport { SvelteKit } from \"./kit.js\";\nimport { makeRunHelpers, type FormErrorMapper, type RunHelpers } from \"./internal/remote-run.js\";\n\nexport type { RunHelpers, FormErrorMapper } from \"./internal/remote-run.js\";\n\n// ─── TestCookies ─────────────────────────────────────────────────────────────\n\ntype CookieParseOptions = Parameters<Cookies[\"get\"]>[1];\ntype CookieSerializeOptions = Parameters<Cookies[\"set\"]>[2];\n\n/** One stored cookie. */\ninterface TestCookie {\n readonly name: string;\n readonly value: string;\n}\n\n/**\n * In-memory Cookies implementation for use in {@link TestRequestEvent}: an\n * ordered list, so `getAll()` returns cookies in the order they were first set\n * (as SvelteKit's does), and `set` on an existing name replaces it in place.\n */\nclass TestCookies {\n private store: ReadonlyArray<TestCookie>;\n\n constructor(initial: Readonly<Record<string, string>> = {}) {\n this.store = Arr.map(Rec.toEntries(initial), ([name, value]) => ({ name, value }));\n }\n\n get(name: string, _opts?: CookieParseOptions): string | undefined {\n return Option.getOrUndefined(\n Option.map(\n Arr.findFirst(this.store, (cookie) => cookie.name === name),\n (cookie) => cookie.value,\n ),\n );\n }\n\n getAll(_opts?: CookieParseOptions): Array<{ name: string; value: string }> {\n return Arr.map(this.store, ({ name, value }) => ({ name, value }));\n }\n\n set(name: string, value: string, _opts?: CookieSerializeOptions): void {\n this.store = Option.match(\n Arr.findFirstIndex(this.store, (cookie) => cookie.name === name),\n {\n onNone: () => Arr.append(this.store, { name, value }),\n onSome: (index) =>\n Arr.map(this.store, (cookie, i) => (i === index ? { name, value } : cookie)),\n },\n );\n }\n\n delete(name: string, _opts?: CookieSerializeOptions): void {\n this.store = Arr.filter(this.store, (cookie) => cookie.name !== name);\n }\n\n serialize(name: string, value: string, _opts?: CookieSerializeOptions): string {\n return `${name}=${encodeURIComponent(value)}`;\n }\n}\n\n// ─── TestRequestEvent ─────────────────────────────────────────────────────────\n\n/** Options for {@link TestRequestEvent}. */\nexport interface TestRequestEventOptions {\n /**\n * The request URL. Accepts a `string` or a `URL` instance.\n * Defaults to `\"http://localhost/\"`.\n */\n readonly url?: string | URL;\n /** The HTTP method. Defaults to `\"GET\"`. */\n readonly method?: string;\n /** Additional HTTP headers added to the underlying `Request`. */\n readonly headers?: HeadersInit;\n /** The request body. Only usable with methods that accept a body. */\n readonly body?: BodyInit | null;\n /**\n * Route params (e.g. `{ id: \"123\" }` for a route like `/items/[id]`).\n * Defaults to `{}`.\n */\n readonly params?: Record<string, string>;\n /**\n * Initial `event.locals` value, typed as your app's `App.Locals` (declare its\n * fields in `src/app.d.ts`). Defaults to `{}`.\n */\n readonly locals?: App.Locals;\n /**\n * Pre-seeded cookies as `{ name: value }` pairs.\n * The backing store supports `get`, `getAll`, `set`, `delete`, and\n * `serialize`; the object is cast to SvelteKit's `Cookies` interface.\n */\n readonly cookies?: Record<string, string>;\n /**\n * The `event.platform` value, if any.\n * Pass a partial platform object matching the environment the handler targets.\n */\n readonly platform?: App.Platform;\n /**\n * The `fetch` implementation used by `event.fetch`.\n * Defaults to `globalThis.fetch`. Pass a custom function to intercept or\n * stub network calls made by the handler.\n */\n readonly fetch?: typeof globalThis.fetch;\n}\n\n/**\n * The fields {@link TestRequestEvent} actually provides: a {@link RequestEvent}\n * without `tracing` / `isRemoteRequest`, with Map-backed cookies (no `parse`)\n * and the test's own `locals` / `platform` values.\n */\ntype TestRequestEventFixture = Omit<\n RequestEvent,\n \"cookies\" | \"locals\" | \"platform\" | \"tracing\" | \"isRemoteRequest\"\n> & {\n readonly cookies: Omit<Cookies, \"parse\">;\n readonly locals: App.Locals;\n readonly platform: App.Platform | undefined;\n};\n\n/**\n * Create a minimal {@link RequestEvent} for use in unit tests.\n *\n * All fields are optional — omit what the handler under test doesn't inspect.\n * Cookies are backed by an in-memory `Map`; mutations (`set`, `delete`) are\n * reflected in subsequent `get`/`getAll` calls on the same event.\n *\n * @example\n * ```ts\n * const event = TestRequestEvent({\n * url: \"http://localhost/items/42\",\n * params: { id: \"42\" },\n * locals: { user: { id: \"u1\", role: \"admin\" } },\n * cookies: { session: \"tok_test\" },\n * });\n * ```\n */\nexport function TestRequestEvent(opts: TestRequestEventOptions = {}): RequestEvent {\n const rawUrl = opts.url instanceof URL ? opts.url.toString() : (opts.url ?? \"http://localhost/\");\n const url = new URL(rawUrl);\n\n const init: RequestInit = { method: opts.method ?? \"GET\" };\n if (opts.headers !== undefined) init.headers = opts.headers;\n if (opts.body !== undefined) init.body = opts.body;\n\n const request = new Request(url.toString(), init);\n const cookies = new TestCookies(opts.cookies ?? {});\n const fetchFn = opts.fetch ?? globalThis.fetch;\n\n const fixture: TestRequestEventFixture = {\n request,\n url,\n params: opts.params ?? {},\n route: { id: null },\n fetch: fetchFn,\n cookies,\n locals: opts.locals ?? {},\n platform: opts.platform,\n isDataRequest: false,\n isSubRequest: false,\n setHeaders: () => {},\n getClientAddress: () => \"127.0.0.1\",\n };\n // SAFETY: a deliberately partial fixture, as the TSDoc above says: it has\n // the fields load/action/endpoint/remote handlers read, while `tracing`,\n // `isRemoteRequest` and `cookies.parse` are absent, and `locals` / `platform`\n // are whatever the test passed in place of `App.Locals` / `App.Platform`.\n return fixture as RequestEvent;\n}\n\n// ─── Options ──────────────────────────────────────────────────────────────────\n\n/**\n * Options shared by {@link runServerLoad}, {@link runAction}, and\n * {@link runEndpoint}.\n */\nexport interface TestRunOptions<R> {\n /**\n * The `Layer` providing the handler effect's required services `R`.\n * Omit when the handler has no service requirements (`R` is `never`).\n */\n readonly layer?: Layer.Layer<R>;\n /**\n * Translate a failed `Cause` at the boundary. Defaults to\n * {@link SvelteKit.translate} (Kit control-flow through; anything else\n * becomes an `EffectCauseError`).\n */\n readonly translateError?: ErrorTranslator;\n}\n\n/**\n * Options for {@link runRemote}.\n */\nexport interface RunRemoteOptions<R> {\n /** The `Layer` providing the remote handler's service requirements. */\n readonly layer?: Layer.Layer<R>;\n /**\n * Translate a failed `Cause` at the boundary. Defaults to\n * {@link SvelteKit.translate}.\n */\n readonly translateError?: ErrorTranslator;\n /**\n * Map a remote form handler's failure to SvelteKit field issues. Receives\n * the squashed handler failure and the `{ issue, data }` context. When\n * provided it is called instead of `translateError` on the form error path.\n */\n readonly formError?: FormErrorMapper;\n}\n\n// ─── Internal ─────────────────────────────────────────────────────────────────\n\n// SAFETY: `layer` may be omitted only when the handler needs no services\n// (`R` is `never`, as `TestRunOptions.layer` documents), and then `Layer.empty`\n// provides all of `R`.\nconst makeAdapter = <R>(opts: TestRunOptions<R> | undefined) =>\n makeRuntimeAdapter(RequestRuntime.fromLayer((opts?.layer ?? Layer.empty) as Layer.Layer<R>), {\n translateError: opts?.translateError ?? SvelteKit.translate,\n });\n\n// ─── runServerLoad ────────────────────────────────────────────────────────────\n\n/**\n * Run an Effect-based server `load` function under test.\n *\n * Wraps `fn` with `makeRuntimeAdapter` using the provided `opts.layer` (or\n * `Layer.empty` when omitted) and calls the resulting `ServerLoad` with\n * `event`. Errors are translated by `opts.translateError` (defaults to\n * {@link SvelteKit.translate}).\n *\n * @example\n * ```ts\n * const data = await runServerLoad(\n * (event) => Effect.gen(function* () {\n * const db = yield* Database;\n * return { items: yield* db.list() };\n * }),\n * TestRequestEvent(),\n * { layer: TestDatabase.layer },\n * );\n * expect(data.items).toHaveLength(0);\n * ```\n */\n// SAFETY: the adapter's `serverLoad` handler is an async function that reads\n// only `RequestEvent` fields (`route.id` plus whatever `fn` reads), so it\n// accepts a plain `RequestEvent` and always returns a `Promise`.\nexport const runServerLoad = <R, A extends Record<string, any> | void = void>(\n fn: (event: RequestEvent) => Effect.Effect<A, unknown, R>,\n event: RequestEvent,\n opts?: TestRunOptions<R>,\n): Promise<A> => (makeAdapter(opts).serverLoad(fn) as (e: RequestEvent) => Promise<A>)(event);\n\n// ─── runAction ───────────────────────────────────────────────────────────────\n\n/**\n * Run an Effect-based form action handler under test.\n *\n * @example\n * ```ts\n * const result = await runAction(\n * (_event) => Effect.succeed({ success: true }),\n * TestRequestEvent({ method: \"POST\" }),\n * );\n * expect((result as any).success).toBe(true);\n * ```\n */\n// SAFETY: the adapter's `action` handler is an async function over a\n// `RequestEvent`, so it always returns a `Promise` of the action's value.\nexport const runAction = <R, A extends Record<string, any> | void = void>(\n fn: (event: RequestEvent) => Effect.Effect<A, unknown, R>,\n event: RequestEvent,\n opts?: TestRunOptions<R>,\n): Promise<A> => (makeAdapter(opts).action(fn) as (e: RequestEvent) => Promise<A>)(event);\n\n// ─── runEndpoint ──────────────────────────────────────────────────────────────\n\n/**\n * Run an Effect-based `+server.ts` endpoint handler under test.\n *\n * @example\n * ```ts\n * const response = await runEndpoint(\n * (_event) => Effect.succeed(new Response(JSON.stringify({ ok: true }))),\n * TestRequestEvent({ url: \"http://localhost/api/ping\" }),\n * );\n * expect(response.status).toBe(200);\n * ```\n */\nexport const runEndpoint = <R>(\n fn: (event: RequestEvent) => Effect.Effect<Response, unknown, R>,\n event: RequestEvent,\n opts?: TestRunOptions<R>,\n): Promise<Response> => Promise.resolve(makeAdapter(opts).endpoint(fn)(event));\n\n// ─── runRemote ────────────────────────────────────────────────────────────────\n\n/**\n * Build a `{ run, runForm }` helper pair for directly invoking remote-function\n * Effect handlers under test.\n *\n * Unlike the production `query` / `command` / `form` helpers, which resolve the\n * request event via `$app/server.getRequestEvent()`, this resolves it from the\n * `event` argument you supply — no SvelteKit module-graph mocking required.\n *\n * Use `run` for `query` / `command` handlers and `runForm` for `form` handlers\n * that need the `issue` / `data` context.\n *\n * @example\n * ```ts\n * import { TestRequestEvent, runRemote } from \"@thomasfosterau/effect-sveltekit/testing\";\n *\n * const event = TestRequestEvent();\n * const { run } = runRemote(event, { layer: TestDatabase.layer });\n *\n * // Invoke the underlying Effect directly — no AppServer needed:\n * const items = await run(listItemsEffect());\n * expect(items).toEqual([]);\n * ```\n */\nexport const runRemote = <R = never>(\n event: RequestEvent,\n opts?: RunRemoteOptions<R>,\n): RunHelpers => {\n const translate = opts?.translateError ?? SvelteKit.translate;\n // SAFETY: `layer` may be omitted only when the handlers need no services\n // (`R = never`, the default), and then `Layer.empty` provides all of `R`.\n const layer = (opts?.layer ?? Layer.empty) as Layer.Layer<R>;\n const provider: RequestRuntimeProvider<R> = RequestRuntime.fromLayer(layer);\n return makeRunHelpers(\n () => ({ runExit: (effect) => provider.run(event).runPromiseExit(effect) }),\n translate,\n opts?.formError,\n );\n};\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { Schema } from "effect";
1
+ import * as Schema from "effect/Schema";
2
2
  import { Transport, Transporter } from "@sveltejs/kit/hooks";
3
3
  //#region src/transport.d.ts
4
4
  /**
@@ -10,7 +10,7 @@ import { Transport, Transporter } from "@sveltejs/kit/hooks";
10
10
  * (e.g. an encoded `0`, `""`, `false`, or `null`) as "not this type" — and is
11
11
  * unwrapped again by `decode` on the client.
12
12
  */
13
- declare const transporter: <T, E>(schema: Schema.Codec<T, E>) => Transporter<T, readonly [E]>;
13
+ export declare const transporter: <T, E>(schema: Schema.Codec<T, E>) => Transporter<T, readonly [E]>;
14
14
  /**
15
15
  * Builds a deliberately-lossy {@link Transporter} for a non-POJO wrapper whose
16
16
  * contents should *not* round-trip — the value is detected on the server and
@@ -32,9 +32,9 @@ declare const transporter: <T, E>(schema: Schema.Codec<T, E>) => Transporter<T,
32
32
  * });
33
33
  * ```
34
34
  */
35
- declare const markerTransporter: <T>(options: MarkerTransporterOptions<T>) => Transporter<T, unknown>;
35
+ export declare const markerTransporter: <T>(options: MarkerTransporterOptions<T>) => Transporter<T, unknown>;
36
36
  /** Options for {@link markerTransporter}. */
37
- interface MarkerTransporterOptions<T> {
37
+ export interface MarkerTransporterOptions<T> {
38
38
  /** Detect, on the server, values this transporter handles. */
39
39
  readonly guard: (value: unknown) => value is T;
40
40
  /** Produce a fresh value on the client (the payload is discarded). */
@@ -46,7 +46,7 @@ interface MarkerTransporterOptions<T> {
46
46
  readonly marker?: unknown;
47
47
  }
48
48
  /** An entry accepted by {@link transport}: an Effect schema or a ready Transporter. */
49
- type TransportEntry = Schema.Codec<any, any> | Transporter<any, any>;
49
+ export type TransportEntry = Schema.Codec<any, any> | Transporter<any, any>;
50
50
  /**
51
51
  * Assembles a SvelteKit `transport` object from a record of entries. Each entry
52
52
  * is either an Effect {@link Schema.Codec} (wrapped with {@link transporter}) or
@@ -54,7 +54,6 @@ type TransportEntry = Schema.Codec<any, any> | Transporter<any, any>;
54
54
  * round-tripping and marker-style values can be mixed. The keys are arbitrary
55
55
  * labels (they only need to be stable between server and client).
56
56
  */
57
- declare const transport: (entries: Record<string, TransportEntry>) => Transport;
57
+ export declare const transport: (entries: Record<string, TransportEntry>) => Transport;
58
58
  //#endregion
59
- export { MarkerTransporterOptions, TransportEntry, markerTransporter, transport, transporter };
60
59
  //# sourceMappingURL=transport.d.ts.map
package/dist/transport.js CHANGED
@@ -1,4 +1,5 @@
1
- import { Schema } from "effect";
1
+ import * as Rec from "effect/Record";
2
+ import * as Schema from "effect/Schema";
2
3
  //#region src/transport.ts
3
4
  /**
4
5
  * Builds a SvelteKit {@link Transporter} from an Effect {@link Schema.Codec}.
@@ -53,11 +54,7 @@ const markerTransporter = (options) => {
53
54
  * round-tripping and marker-style values can be mixed. The keys are arbitrary
54
55
  * labels (they only need to be stable between server and client).
55
56
  */
56
- const transport = (entries) => {
57
- const result = {};
58
- for (const [name, entry] of Object.entries(entries)) result[name] = Schema.isSchema(entry) ? transporter(entry) : entry;
59
- return result;
60
- };
57
+ const transport = (entries) => Rec.map(entries, (entry) => Schema.isSchema(entry) ? transporter(entry) : entry);
61
58
  //#endregion
62
59
  export { markerTransporter, transport, transporter };
63
60
 
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","names":[],"sources":["../src/transport.ts"],"sourcesContent":["/**\n * Effect Schema ⇄ SvelteKit `transport` hook.\n *\n * SvelteKit's universal [`transport`](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport)\n * hook moves custom (non-POJO) values across the server/client boundary — for\n * data returned from `load` functions and from remote functions. Each entry is\n * a {@link Transporter}: a sync `encode` (server) / `decode` (client) pair,\n * where `encode` returns `false` for values it does not handle.\n *\n * An Effect {@link Schema.Codec} is exactly such a pair. {@link transporter}\n * turns a schema into a `Transporter`, and {@link transport} assembles a whole\n * `transport` object from a record of schemas — so `Option`, `DateTime`,\n * branded types, `Schema.Class` instances, tagged errors, etc. round-trip with\n * no hand-written serialization.\n *\n * ```ts\n * // src/hooks.ts\n * import { Hooks } from \"@thomasfosterau/effect-sveltekit\";\n * import { Schema } from \"effect\";\n *\n * export const transport = Hooks.transport({\n * Option: Schema.Option(Schema.Number),\n * Date: Schema.Date,\n * });\n * ```\n */\n\nimport type { Transport, Transporter } from \"@sveltejs/kit/hooks\";\nimport { Schema } from \"effect\";\n\n/**\n * Builds a SvelteKit {@link Transporter} from an Effect {@link Schema.Codec}.\n *\n * The decoded `Type` is detected with `Schema.is`; matching values are encoded\n * with `Schema.encodeSync` and wrapped in a single-element tuple. The wrapper\n * guarantees the encoding is truthy — SvelteKit treats a falsy `encode` result\n * (e.g. an encoded `0`, `\"\"`, `false`, or `null`) as \"not this type\" — and is\n * unwrapped again by `decode` on the client.\n */\nexport const transporter = <T, E>(schema: Schema.Codec<T, E>): Transporter<T, readonly [E]> => {\n const guard = Schema.is(schema);\n const encode = Schema.encodeSync(schema);\n const decode = Schema.decodeSync(schema);\n return {\n encode: (value) => (guard(value) ? ([encode(value)] as const) : false),\n decode: ([data]) => decode(data),\n };\n};\n\n/**\n * Builds a deliberately-lossy {@link Transporter} for a non-POJO wrapper whose\n * contents should *not* round-trip — the value is detected on the server and\n * reconstructed fresh on the client.\n *\n * Unlike {@link transporter} (which needs a round-tripping {@link Schema.Codec}),\n * this is for values that have no meaningful serialization but still need a\n * `transport` entry so SvelteKit doesn't reject them: `encode` returns a truthy\n * marker for matching values, and `decode` ignores it and produces a fresh value\n * from {@link MarkerTransporterOptions.make}.\n *\n * ```ts\n * // A non-serializable handle reconstructed as a fresh, empty instance:\n * export const transport = Hooks.transport({\n * Live: Hooks.markerTransporter({\n * guard: (v): v is LiveHandle => v instanceof LiveHandle,\n * make: () => LiveHandle.empty(),\n * }),\n * });\n * ```\n */\nexport const markerTransporter = <T>(\n options: MarkerTransporterOptions<T>,\n): Transporter<T, unknown> => {\n const marker = options.marker ?? 1;\n return {\n encode: (value) => (options.guard(value) ? marker : false),\n decode: () => options.make(),\n };\n};\n\n/** Options for {@link markerTransporter}. */\nexport interface MarkerTransporterOptions<T> {\n /** Detect, on the server, values this transporter handles. */\n readonly guard: (value: unknown) => value is T;\n /** Produce a fresh value on the client (the payload is discarded). */\n readonly make: () => T;\n /**\n * The truthy marker `encode` emits (SvelteKit treats a falsy `encode` result\n * as \"not this type\"). Defaults to `1`.\n */\n readonly marker?: unknown;\n}\n\n/** An entry accepted by {@link transport}: an Effect schema or a ready Transporter. */\nexport type TransportEntry = Schema.Codec<any, any> | Transporter<any, any>;\n\n/**\n * Assembles a SvelteKit `transport` object from a record of entries. Each entry\n * is either an Effect {@link Schema.Codec} (wrapped with {@link transporter}) or\n * a ready {@link Transporter} (e.g. from {@link markerTransporter}), so\n * round-tripping and marker-style values can be mixed. The keys are arbitrary\n * labels (they only need to be stable between server and client).\n */\nexport const transport = (entries: Record<string, TransportEntry>): Transport => {\n const result: Transport = {};\n for (const [name, entry] of Object.entries(entries)) {\n result[name] = Schema.isSchema(entry)\n ? transporter(entry as Schema.Codec<any, any>)\n : (entry as Transporter<any, any>);\n }\n return result;\n};\n"],"mappings":";;;;;;;;;;;AAuCA,MAAa,eAAqB,WAA6D;CAC7F,MAAM,QAAQ,OAAO,GAAG,MAAM;CAC9B,MAAM,SAAS,OAAO,WAAW,MAAM;CACvC,MAAM,SAAS,OAAO,WAAW,MAAM;CACvC,OAAO;EACL,SAAS,UAAW,MAAM,KAAK,IAAK,CAAC,OAAO,KAAK,CAAC,IAAc;EAChE,SAAS,CAAC,UAAU,OAAO,IAAI;CACjC;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,qBACX,YAC4B;CAC5B,MAAM,SAAS,QAAQ,UAAU;CACjC,OAAO;EACL,SAAS,UAAW,QAAQ,MAAM,KAAK,IAAI,SAAS;EACpD,cAAc,QAAQ,KAAK;CAC7B;AACF;;;;;;;;AAyBA,MAAa,aAAa,YAAuD;CAC/E,MAAM,SAAoB,CAAC;CAC3B,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,OAAO,QAAQ,OAAO,SAAS,KAAK,IAChC,YAAY,KAA+B,IAC1C;CAEP,OAAO;AACT"}
1
+ {"version":3,"file":"transport.js","names":[],"sources":["../src/transport.ts"],"sourcesContent":["/**\n * Effect Schema ⇄ SvelteKit `transport` hook.\n *\n * SvelteKit's universal [`transport`](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport)\n * hook moves custom (non-POJO) values across the server/client boundary — for\n * data returned from `load` functions and from remote functions. Each entry is\n * a {@link Transporter}: a sync `encode` (server) / `decode` (client) pair,\n * where `encode` returns `false` for values it does not handle.\n *\n * An Effect {@link Schema.Codec} is exactly such a pair. {@link transporter}\n * turns a schema into a `Transporter`, and {@link transport} assembles a whole\n * `transport` object from a record of schemas — so `Option`, `DateTime`,\n * branded types, `Schema.Class` instances, tagged errors, etc. round-trip with\n * no hand-written serialization.\n *\n * ```ts\n * // src/hooks.ts\n * import { Hooks } from \"@thomasfosterau/effect-sveltekit\";\n * import { Schema } from \"effect\";\n *\n * export const transport = Hooks.transport({\n * Option: Schema.Option(Schema.Number),\n * Date: Schema.Date,\n * });\n * ```\n */\n\nimport type { Transport, Transporter } from \"@sveltejs/kit/hooks\";\nimport * as Rec from \"effect/Record\";\nimport * as Schema from \"effect/Schema\";\n\n/**\n * Builds a SvelteKit {@link Transporter} from an Effect {@link Schema.Codec}.\n *\n * The decoded `Type` is detected with `Schema.is`; matching values are encoded\n * with `Schema.encodeSync` and wrapped in a single-element tuple. The wrapper\n * guarantees the encoding is truthy — SvelteKit treats a falsy `encode` result\n * (e.g. an encoded `0`, `\"\"`, `false`, or `null`) as \"not this type\" — and is\n * unwrapped again by `decode` on the client.\n */\nexport const transporter = <T, E>(schema: Schema.Codec<T, E>): Transporter<T, readonly [E]> => {\n const guard = Schema.is(schema);\n const encode = Schema.encodeSync(schema);\n const decode = Schema.decodeSync(schema);\n return {\n encode: (value) => (guard(value) ? ([encode(value)] as const) : false),\n decode: ([data]) => decode(data),\n };\n};\n\n/**\n * Builds a deliberately-lossy {@link Transporter} for a non-POJO wrapper whose\n * contents should *not* round-trip — the value is detected on the server and\n * reconstructed fresh on the client.\n *\n * Unlike {@link transporter} (which needs a round-tripping {@link Schema.Codec}),\n * this is for values that have no meaningful serialization but still need a\n * `transport` entry so SvelteKit doesn't reject them: `encode` returns a truthy\n * marker for matching values, and `decode` ignores it and produces a fresh value\n * from {@link MarkerTransporterOptions.make}.\n *\n * ```ts\n * // A non-serializable handle reconstructed as a fresh, empty instance:\n * export const transport = Hooks.transport({\n * Live: Hooks.markerTransporter({\n * guard: (v): v is LiveHandle => v instanceof LiveHandle,\n * make: () => LiveHandle.empty(),\n * }),\n * });\n * ```\n */\nexport const markerTransporter = <T>(\n options: MarkerTransporterOptions<T>,\n): Transporter<T, unknown> => {\n const marker = options.marker ?? 1;\n return {\n encode: (value) => (options.guard(value) ? marker : false),\n decode: () => options.make(),\n };\n};\n\n/** Options for {@link markerTransporter}. */\nexport interface MarkerTransporterOptions<T> {\n /** Detect, on the server, values this transporter handles. */\n readonly guard: (value: unknown) => value is T;\n /** Produce a fresh value on the client (the payload is discarded). */\n readonly make: () => T;\n /**\n * The truthy marker `encode` emits (SvelteKit treats a falsy `encode` result\n * as \"not this type\"). Defaults to `1`.\n */\n readonly marker?: unknown;\n}\n\n/** An entry accepted by {@link transport}: an Effect schema or a ready Transporter. */\nexport type TransportEntry = Schema.Codec<any, any> | Transporter<any, any>;\n\n/**\n * Assembles a SvelteKit `transport` object from a record of entries. Each entry\n * is either an Effect {@link Schema.Codec} (wrapped with {@link transporter}) or\n * a ready {@link Transporter} (e.g. from {@link markerTransporter}), so\n * round-tripping and marker-style values can be mixed. The keys are arbitrary\n * labels (they only need to be stable between server and client).\n */\nexport const transport = (entries: Record<string, TransportEntry>): Transport =>\n Rec.map(entries, (entry): Transporter<any, any> =>\n Schema.isSchema(entry) ? transporter(entry) : entry,\n );\n"],"mappings":";;;;;;;;;;;;AAwCA,MAAa,eAAqB,WAA6D;CAC7F,MAAM,QAAQ,OAAO,GAAG,MAAM;CAC9B,MAAM,SAAS,OAAO,WAAW,MAAM;CACvC,MAAM,SAAS,OAAO,WAAW,MAAM;CACvC,OAAO;EACL,SAAS,UAAW,MAAM,KAAK,IAAK,CAAC,OAAO,KAAK,CAAC,IAAc;EAChE,SAAS,CAAC,UAAU,OAAO,IAAI;CACjC;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,qBACX,YAC4B;CAC5B,MAAM,SAAS,QAAQ,UAAU;CACjC,OAAO;EACL,SAAS,UAAW,QAAQ,MAAM,KAAK,IAAI,SAAS;EACpD,cAAc,QAAQ,KAAK;CAC7B;AACF;;;;;;;;AAyBA,MAAa,aAAa,YACxB,IAAI,IAAI,UAAU,UAChB,OAAO,SAAS,KAAK,IAAI,YAAY,KAAK,IAAI,KAChD"}
@@ -1,6 +1,6 @@
1
1
  import { SvelteRouteParams } from "./internal/route-params.js";
2
2
  import { UniversalRuntime, makeUniversalRuntime } from "./app-runtime.js";
3
- import { Effect } from "effect";
3
+ import * as Effect from "effect/Effect";
4
4
  import * as HttpClient from "effect/http/HttpClient";
5
5
  import { Load } from "@sveltejs/kit";
6
6
  //#region src/universal.d.ts
@@ -29,7 +29,7 @@ import { Load } from "@sveltejs/kit";
29
29
  * );
30
30
  * ```
31
31
  */
32
- declare function load<Params extends SvelteRouteParams = SvelteRouteParams, InputData extends Record<string, unknown> | null = any, ParentData extends Record<string, unknown> = any, OutputData extends Record<string, unknown> | void = any, RouteId extends string | null = any>(fn: (event: Parameters<Load<Params, InputData, ParentData, OutputData, RouteId>>[0]) => Effect.Effect<OutputData, unknown, HttpClient.HttpClient>): Load<Params, InputData, ParentData, OutputData, RouteId>;
32
+ export declare function load<Params extends SvelteRouteParams = SvelteRouteParams, InputData extends Record<string, unknown> | null = any, ParentData extends Record<string, unknown> = any, OutputData extends Record<string, unknown> | void = any, RouteId extends string | null = any>(fn: (event: Parameters<Load<Params, InputData, ParentData, OutputData, RouteId>>[0]) => Effect.Effect<OutputData, unknown, HttpClient.HttpClient>): Load<Params, InputData, ParentData, OutputData, RouteId>;
33
33
  //#endregion
34
- export { type UniversalRuntime, load, makeUniversalRuntime };
34
+ export { type UniversalRuntime, makeUniversalRuntime };
35
35
  //# sourceMappingURL=universal.d.ts.map
package/dist/universal.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { makeUniversalRuntime } from "./app-runtime.js";
2
2
  import { defaultUniversalRuntime } from "./internal/default-runtime.js";
3
- import "effect";
3
+ import "effect/Effect";
4
4
  //#region src/universal.ts
5
5
  /**
6
6
  * Wraps an Effect-based function to work as a SvelteKit universal load function.
@@ -1 +1 @@
1
- {"version":3,"file":"universal.js","names":["runtime"],"sources":["../src/universal.ts"],"sourcesContent":["import type { Load } from \"@sveltejs/kit\";\nimport { Effect } from \"effect\";\nimport type * as HttpClient from \"effect/http/HttpClient\";\nimport type { SvelteRouteParams } from \"./internal/route-params.js\";\nimport { defaultUniversalRuntime as runtime } from \"./internal/default-runtime.js\";\n\n/**\n * Wraps an Effect-based function to work as a SvelteKit universal load function.\n *\n * The effect has access to `HttpClient` on both the server and the client:\n * it is backed by the load event's `fetch`, so relative URLs and SvelteKit's\n * fetch behaviours (deduplication, cookies forwarding) work as expected.\n *\n * To give effects access to your own application services, create an\n * app-wide runtime with `makeUniversalRuntime` and use its `load` instead.\n *\n * @example\n * ```ts\n * import { load as effectLoad } from '@thomasfosterau/effect-sveltekit';\n * import * as HttpClient from 'effect/http/HttpClient';\n * import { Effect } from 'effect';\n *\n * export const load = effectLoad((event) =>\n * Effect.gen(function* () {\n * const client = yield* HttpClient.HttpClient;\n * const response = yield* client.get(`/api/items/${event.params.id}`);\n * return { item: yield* response.json };\n * })\n * );\n * ```\n */\nexport function load<\n Params extends SvelteRouteParams = SvelteRouteParams,\n InputData extends Record<string, unknown> | null = any,\n ParentData extends Record<string, unknown> = any,\n OutputData extends Record<string, unknown> | void = any,\n RouteId extends string | null = any,\n>(\n fn: (\n event: Parameters<Load<Params, InputData, ParentData, OutputData, RouteId>>[0],\n ) => Effect.Effect<OutputData, unknown, HttpClient.HttpClient>,\n): Load<Params, InputData, ParentData, OutputData, RouteId> {\n return runtime().load(fn);\n}\n\n/**\n * App-wide runtime: define your application's (universal) Effect services\n * once and share them across every `load` invocation.\n */\nexport { makeUniversalRuntime, type UniversalRuntime } from \"./app-runtime.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,KAOd,IAG0D;CAC1D,OAAOA,wBAAQ,CAAC,CAAC,KAAK,EAAE;AAC1B"}
1
+ {"version":3,"file":"universal.js","names":["runtime"],"sources":["../src/universal.ts"],"sourcesContent":["import type { Load } from \"@sveltejs/kit\";\nimport * as Effect from \"effect/Effect\";\nimport type * as HttpClient from \"effect/http/HttpClient\";\nimport type { SvelteRouteParams } from \"./internal/route-params.js\";\nimport { defaultUniversalRuntime as runtime } from \"./internal/default-runtime.js\";\n\n/**\n * Wraps an Effect-based function to work as a SvelteKit universal load function.\n *\n * The effect has access to `HttpClient` on both the server and the client:\n * it is backed by the load event's `fetch`, so relative URLs and SvelteKit's\n * fetch behaviours (deduplication, cookies forwarding) work as expected.\n *\n * To give effects access to your own application services, create an\n * app-wide runtime with `makeUniversalRuntime` and use its `load` instead.\n *\n * @example\n * ```ts\n * import { load as effectLoad } from '@thomasfosterau/effect-sveltekit';\n * import * as HttpClient from 'effect/http/HttpClient';\n * import { Effect } from 'effect';\n *\n * export const load = effectLoad((event) =>\n * Effect.gen(function* () {\n * const client = yield* HttpClient.HttpClient;\n * const response = yield* client.get(`/api/items/${event.params.id}`);\n * return { item: yield* response.json };\n * })\n * );\n * ```\n */\nexport function load<\n Params extends SvelteRouteParams = SvelteRouteParams,\n InputData extends Record<string, unknown> | null = any,\n ParentData extends Record<string, unknown> = any,\n OutputData extends Record<string, unknown> | void = any,\n RouteId extends string | null = any,\n>(\n fn: (\n event: Parameters<Load<Params, InputData, ParentData, OutputData, RouteId>>[0],\n ) => Effect.Effect<OutputData, unknown, HttpClient.HttpClient>,\n): Load<Params, InputData, ParentData, OutputData, RouteId> {\n return runtime().load(fn);\n}\n\n/**\n * App-wide runtime: define your application's (universal) Effect services\n * once and share them across every `load` invocation.\n */\nexport { makeUniversalRuntime, type UniversalRuntime } from \"./app-runtime.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,KAOd,IAG0D;CAC1D,OAAOA,wBAAQ,CAAC,CAAC,KAAK,EAAE;AAC1B"}
package/dist/vite.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Plugin } from "vite";
2
2
  //#region src/vite.d.ts
3
3
  /** The SvelteKit route and hook file kinds `effectKit()` can rewrite. */
4
- type EffectKitFileKind = "pageServer" | "page" | "layoutServer" | "layout" | "server" | "hooks" | "hooksServer" | "hooksClient";
4
+ export type EffectKitFileKind = "pageServer" | "page" | "layoutServer" | "layout" | "server" | "hooks" | "hooksServer" | "hooksClient";
5
5
  /** Options for {@link effectKit}. */
6
- interface EffectKitPluginOptions {
6
+ export interface EffectKitPluginOptions {
7
7
  /**
8
8
  * Restrict transformation to a subset of SvelteKit file kinds.
9
9
  * Defaults to all standard route files.
@@ -37,7 +37,7 @@ interface EffectKitPluginOptions {
37
37
  * functions returning Effects, are accepted wherever SvelteKit expects a
38
38
  * function-shaped export.
39
39
  */
40
- declare function effectKit(options?: EffectKitPluginOptions): Plugin;
40
+ export declare function effectKit(options?: EffectKitPluginOptions): Plugin;
41
41
  //#endregion
42
- export { EffectKitFileKind, EffectKitPluginOptions, effectKit as default, effectKit };
42
+ export { effectKit as default };
43
43
  //# sourceMappingURL=vite.d.ts.map
package/dist/vite.js CHANGED
@@ -1,3 +1,7 @@
1
+ import * as Rec from "effect/Record";
2
+ import * as Result from "effect/Result";
3
+ import * as Arr from "effect/Array";
4
+ import * as Option from "effect/Option";
1
5
  import MagicString from "magic-string";
2
6
  //#region src/vite.ts
3
7
  /**
@@ -35,12 +39,12 @@ const INTERNAL_IMPORT = "@thomasfosterau/effect-sveltekit/internal/auto";
35
39
  const ORIG_PREFIX = "__effect_kit_orig_";
36
40
  const RUNTIME_IDENT = "__effect_kit_runtime";
37
41
  /** File kinds whose exports only ever run on the server. */
38
- const serverKinds = /* @__PURE__ */ new Set([
42
+ const serverKinds = [
39
43
  "pageServer",
40
44
  "layoutServer",
41
45
  "server",
42
46
  "hooksServer"
43
- ]);
47
+ ];
44
48
  const wrapperByKind = {
45
49
  pageServer: {
46
50
  load: "wrapServerLoad",
@@ -72,7 +76,7 @@ const wrapperByKind = {
72
76
  hooks: { reroute: "wrapReroute" }
73
77
  };
74
78
  function getFileKind(id) {
75
- const path = id.split("?")[0].replace(/\\/g, "/");
79
+ const path = id.replace(/\?[\s\S]*$/, "").replace(/\\/g, "/");
76
80
  if (/\/\+page\.server\.(?:ts|js|mts|mjs)$/.test(path)) return "pageServer";
77
81
  if (/\/\+page\.(?:ts|js|mts|mjs)$/.test(path)) return "page";
78
82
  if (/\/\+layout\.server\.(?:ts|js|mts|mjs)$/.test(path)) return "layoutServer";
@@ -83,95 +87,93 @@ function getFileKind(id) {
83
87
  if (/\/hooks\.(?:ts|js|mts|mjs)$/.test(path)) return "hooks";
84
88
  return null;
85
89
  }
90
+ /** `text` with every character but a newline replaced by a space. */
91
+ const blank = (text) => text.replace(/[^\n]/g, " ");
92
+ /**
93
+ * A string or template literal's text, blanked: an escape (a backslash and the
94
+ * character after it, even a newline) becomes two spaces, and every other
95
+ * character but a newline one space.
96
+ */
97
+ const blankLiteral = (text) => text.replace(/\\[\s\S]|[^\n]/g, (match) => " ".repeat(match.length));
98
+ /** A line comment, or a block comment (unterminated: to the end of the source). */
99
+ const COMMENT = /\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/y;
100
+ /** A single- or double-quoted string (unterminated: to the end of the source). */
101
+ const QUOTED = /(["'])((?:\\[\s\S]?|(?!\1)[^\\])*)(\1?)/y;
102
+ /** A run of characters that start no comment or literal, or a lone `/`. */
103
+ const PLAIN = /[^/"'`]+|\//y;
104
+ /** Template literal text up to its closing backtick, a `${`, or the end of the source. */
105
+ const TEMPLATE_TEXT = /(?:\\[\s\S]?|\$(?!\{)|[^`\\$])*/y;
106
+ /** The text the sticky `pattern` matches at `index`, if any. */
107
+ const matchAt = (pattern, source, index) => {
108
+ pattern.lastIndex = index;
109
+ return pattern.exec(source);
110
+ };
111
+ /**
112
+ * The index just past the `}` closing a template expression whose body starts
113
+ * at `from` (braces counted naively, as raw text), or the end of the source.
114
+ */
115
+ const expressionEnd = (source, from) => {
116
+ let depth = 1;
117
+ const closing = Arr.findFirst(source.slice(from).matchAll(/[{}]/g), (brace) => {
118
+ depth += brace[0] === "{" ? 1 : -1;
119
+ return depth === 0;
120
+ });
121
+ return Option.match(closing, {
122
+ onNone: () => source.length,
123
+ onSome: (brace) => from + brace.index + 1
124
+ });
125
+ };
126
+ /** The masked chunk at a position inside a template literal, and the position after it. */
127
+ const maskTemplateChunk = (source, index) => {
128
+ const text = matchAt(TEMPLATE_TEXT, source, index)?.[0] ?? "";
129
+ const end = index + text.length;
130
+ if (source.startsWith("${", end)) {
131
+ const close = expressionEnd(source, end + 2);
132
+ return [blankLiteral(text) + source.slice(end, close), {
133
+ index: close,
134
+ inTemplate: true
135
+ }];
136
+ }
137
+ return [blankLiteral(text) + source.slice(end, end + 1), {
138
+ index: end + 1,
139
+ inTemplate: false
140
+ }];
141
+ };
142
+ /** The masked chunk at a position outside any template literal, and the position after it. */
143
+ const maskChunk = (source, index) => {
144
+ const comment = matchAt(COMMENT, source, index);
145
+ if (comment !== null) return [blank(comment[0]), {
146
+ index: index + comment[0].length,
147
+ inTemplate: false
148
+ }];
149
+ const quoted = matchAt(QUOTED, source, index);
150
+ if (quoted !== null) {
151
+ const [whole, quote = "", body = "", closing = ""] = quoted;
152
+ return [quote + blankLiteral(body) + closing, {
153
+ index: index + whole.length,
154
+ inTemplate: false
155
+ }];
156
+ }
157
+ if (source.startsWith("`", index)) return ["`", {
158
+ index: index + 1,
159
+ inTemplate: true
160
+ }];
161
+ const plain = matchAt(PLAIN, source, index)?.[0] ?? source.charAt(index);
162
+ return [plain, {
163
+ index: index + plain.length,
164
+ inTemplate: false
165
+ }];
166
+ };
86
167
  /**
87
168
  * Returns a copy of `source` with line/block comments and string/template
88
169
  * contents replaced by spaces (newlines preserved). Byte offsets in the
89
170
  * returned string line up with the original, so regex scans don't trip on
90
171
  * tokens inside comments or strings.
91
172
  */
92
- function maskCommentsAndStrings(source) {
93
- const out = [];
94
- let i = 0;
95
- const len = source.length;
96
- while (i < len) {
97
- const c = source[i];
98
- const next = source[i + 1];
99
- if (c === "/" && next === "/") {
100
- out.push(" ");
101
- i += 2;
102
- while (i < len && source[i] !== "\n") {
103
- out.push(" ");
104
- i++;
105
- }
106
- continue;
107
- }
108
- if (c === "/" && next === "*") {
109
- out.push(" ");
110
- i += 2;
111
- while (i < len && !(source[i] === "*" && source[i + 1] === "/")) {
112
- out.push(source[i] === "\n" ? "\n" : " ");
113
- i++;
114
- }
115
- if (i < len) {
116
- out.push(" ");
117
- i += 2;
118
- }
119
- continue;
120
- }
121
- if (c === "\"" || c === "'") {
122
- const quote = c;
123
- out.push(quote);
124
- i++;
125
- while (i < len && source[i] !== quote) {
126
- if (source[i] === "\\" && i + 1 < len) {
127
- out.push(" ");
128
- i += 2;
129
- continue;
130
- }
131
- out.push(source[i] === "\n" ? "\n" : " ");
132
- i++;
133
- }
134
- if (i < len) {
135
- out.push(quote);
136
- i++;
137
- }
138
- continue;
139
- }
140
- if (c === "`") {
141
- out.push("`");
142
- i++;
143
- while (i < len && source[i] !== "`") {
144
- if (source[i] === "\\" && i + 1 < len) {
145
- out.push(" ");
146
- i += 2;
147
- continue;
148
- }
149
- if (source[i] === "$" && source[i + 1] === "{") {
150
- out.push("${");
151
- i += 2;
152
- let depth = 1;
153
- while (i < len && depth > 0) {
154
- out.push(source[i]);
155
- if (source[i] === "{") depth++;
156
- else if (source[i] === "}") depth--;
157
- i++;
158
- }
159
- continue;
160
- }
161
- out.push(source[i] === "\n" ? "\n" : " ");
162
- i++;
163
- }
164
- if (i < len) {
165
- out.push("`");
166
- i++;
167
- }
168
- continue;
169
- }
170
- out.push(c);
171
- i++;
172
- }
173
- return out.join("");
174
- }
173
+ const maskCommentsAndStrings = (source) => Arr.unfold({
174
+ index: 0,
175
+ inTemplate: false
176
+ }, (position) => position.index >= source.length ? Option.none() : Option.some(position.inTemplate ? maskTemplateChunk(source, position.index) : maskChunk(source, position.index))).join("");
175
177
  /**
176
178
  * Find top-level `export const|let|var|function NAME` declarations whose
177
179
  * NAME is in `targetNames`. Returns the slice of the original source to
@@ -180,25 +182,21 @@ function maskCommentsAndStrings(source) {
180
182
  * `export { NAME }`, `export default`, and destructuring forms are not
181
183
  * supported — users who need them should use the explicit wrappers.
182
184
  */
183
- function findExports(masked, targetNames) {
184
- const matches = [];
185
- const pattern = /^[ \t]*(export)[ \t]+(?:async[ \t]+)?(?:const|let|var|function)[ \t]+([A-Za-z_$][\w$]*)/gm;
186
- let m;
187
- while (m = pattern.exec(masked)) {
185
+ function findExports(masked, isTarget) {
186
+ return Arr.filterMap(masked.matchAll(/^[ \t]*(export)[ \t]+(?:async[ \t]+)?(?:const|let|var|function)[ \t]+([A-Za-z_$][\w$]*)/gm), (m) => {
188
187
  const [full, , name] = m;
189
- if (!targetNames.has(name)) continue;
188
+ if (name === void 0 || !isTarget(name)) return Result.failVoid;
190
189
  const exportStart = m.index + full.indexOf("export");
191
190
  const nameEnd = m.index + full.length;
192
191
  const renamed = `${ORIG_PREFIX}${name}`;
193
192
  const replacement = masked.slice(exportStart, nameEnd).replace(/^export[ \t]+/, "").replace(new RegExp(`\\b${name}$`), renamed);
194
- matches.push({
193
+ return Result.succeed({
195
194
  name,
196
195
  start: exportStart,
197
196
  end: nameEnd,
198
197
  replacement
199
198
  });
200
- }
201
- return matches;
199
+ });
202
200
  }
203
201
  /**
204
202
  * Vite plugin that rewrites SvelteKit route exports so that Effects, or
@@ -206,22 +204,23 @@ function findExports(masked, targetNames) {
206
204
  * function-shaped export.
207
205
  */
208
206
  function effectKit(options = {}) {
209
- const include = options.include ? new Set(options.include) : null;
207
+ const { include } = options;
210
208
  return {
211
209
  name: "effect-kit",
212
210
  enforce: "pre",
213
211
  transform(code, id) {
214
212
  const kind = getFileKind(id);
215
213
  if (!kind) return null;
216
- if (include && !include.has(kind)) return null;
214
+ if (include && !Arr.contains(include, kind)) return null;
217
215
  const wrappers = wrapperByKind[kind];
218
- const targetNames = new Set(Object.keys(wrappers));
219
- const found = findExports(maskCommentsAndStrings(code), targetNames);
220
- if (found.length === 0) return null;
216
+ const found = findExports(maskCommentsAndStrings(code), (name) => Rec.has(wrappers, name));
217
+ if (Arr.isReadonlyArrayEmpty(found)) return null;
221
218
  const s = new MagicString(code);
222
- for (const match of found) s.overwrite(match.start, match.end, match.replacement);
223
- const wrapperImports = Array.from(new Set(found.map((m) => wrappers[m.name])));
224
- const runtimeSpecifier = serverKinds.has(kind) ? options.serverRuntime : options.clientRuntime;
219
+ Arr.forEach(found, (match) => {
220
+ s.overwrite(match.start, match.end, match.replacement);
221
+ });
222
+ const wrapperImports = Arr.dedupe(Arr.filterMap(found, (m) => Result.fromOption(Rec.get(wrappers, m.name), () => m.name)));
223
+ const runtimeSpecifier = Arr.contains(serverKinds, kind) ? options.serverRuntime : options.clientRuntime;
225
224
  const imports = `\nimport { ${wrapperImports.join(", ")} } from ${JSON.stringify(INTERNAL_IMPORT)};\n` + (runtimeSpecifier ? `import * as ${RUNTIME_IDENT} from ${JSON.stringify(runtimeSpecifier)};\n` : "");
226
225
  const runtimeArg = runtimeSpecifier ? `, ${RUNTIME_IDENT}` : "";
227
226
  const exports = found.map((m) => `export const ${m.name} = ${wrappers[m.name]}(${ORIG_PREFIX}${m.name}${runtimeArg});`).join("\n");
package/dist/vite.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vite.js","names":[],"sources":["../src/vite.ts"],"sourcesContent":["/**\n * Vite plugin that lets you write SvelteKit route exports as Effects (or\n * functions returning Effects) without manually wrapping them.\n *\n * ```ts\n * // vite.config.ts\n * import { sveltekit } from \"@sveltejs/kit/vite\";\n * import { effectKit } from \"@thomasfosterau/effect-sveltekit/vite\";\n *\n * export default {\n * plugins: [effectKit(), sveltekit()],\n * };\n * ```\n *\n * ```ts\n * // +page.server.ts\n * import { Effect } from \"effect\";\n * import * as HttpClient from \"effect/http/HttpClient\";\n *\n * export const load = (event) =>\n * Effect.gen(function* () {\n * const client = yield* HttpClient.HttpClient;\n * return { hello: \"world\" };\n * });\n * ```\n *\n * Under the hood the plugin rewrites the export to call the same wrappers\n * that the explicit API (`serverLoad`, `actions`, `handle`, `endpoint`, ...)\n * uses. Plain functions that return promises or values pass through\n * unchanged, so the plugin is safe to enable on existing projects.\n */\n\nimport MagicString from \"magic-string\";\nimport type { Plugin } from \"vite\";\n\nconst INTERNAL_IMPORT = \"@thomasfosterau/effect-sveltekit/internal/auto\";\nconst ORIG_PREFIX = \"__effect_kit_orig_\";\nconst RUNTIME_IDENT = \"__effect_kit_runtime\";\n\n/** The SvelteKit route and hook file kinds `effectKit()` can rewrite. */\nexport type EffectKitFileKind =\n | \"pageServer\"\n | \"page\"\n | \"layoutServer\"\n | \"layout\"\n | \"server\"\n | \"hooks\"\n | \"hooksServer\"\n | \"hooksClient\";\n\n/** File kinds whose exports only ever run on the server. */\nconst serverKinds: ReadonlySet<EffectKitFileKind> = new Set([\n \"pageServer\",\n \"layoutServer\",\n \"server\",\n \"hooksServer\",\n]);\n\nconst wrapperByKind: Record<EffectKitFileKind, Record<string, string>> = {\n pageServer: { load: \"wrapServerLoad\", actions: \"wrapActions\" },\n page: { load: \"wrapLoad\" },\n layoutServer: { load: \"wrapServerLoad\" },\n layout: { load: \"wrapLoad\" },\n server: {\n GET: \"wrapEndpoint\",\n POST: \"wrapEndpoint\",\n PUT: \"wrapEndpoint\",\n PATCH: \"wrapEndpoint\",\n DELETE: \"wrapEndpoint\",\n OPTIONS: \"wrapEndpoint\",\n HEAD: \"wrapEndpoint\",\n fallback: \"wrapEndpoint\",\n },\n hooksServer: {\n handle: \"wrapHandle\",\n handleFetch: \"wrapHandleFetch\",\n handleError: \"wrapHandleError\",\n init: \"wrapInit\",\n },\n hooksClient: {\n handleError: \"wrapHandleError\",\n init: \"wrapInit\",\n },\n // Universal `src/hooks.ts`: only `reroute` is function-shaped (the `transport`\n // export is plain data and is intentionally left untouched).\n hooks: {\n reroute: \"wrapReroute\",\n },\n};\n\nfunction getFileKind(id: string): EffectKitFileKind | null {\n const path = id.split(\"?\")[0]!.replace(/\\\\/g, \"/\");\n if (/\\/\\+page\\.server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"pageServer\";\n if (/\\/\\+page\\.(?:ts|js|mts|mjs)$/.test(path)) return \"page\";\n if (/\\/\\+layout\\.server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"layoutServer\";\n if (/\\/\\+layout\\.(?:ts|js|mts|mjs)$/.test(path)) return \"layout\";\n if (/\\/\\+server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"server\";\n if (/\\/hooks\\.server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"hooksServer\";\n if (/\\/hooks\\.client\\.(?:ts|js|mts|mjs)$/.test(path)) return \"hooksClient\";\n // Plain `hooks.ts` is the universal hooks file; checked last so the\n // `.server.`/`.client.` variants above take precedence.\n if (/\\/hooks\\.(?:ts|js|mts|mjs)$/.test(path)) return \"hooks\";\n return null;\n}\n\n/**\n * Returns a copy of `source` with line/block comments and string/template\n * contents replaced by spaces (newlines preserved). Byte offsets in the\n * returned string line up with the original, so regex scans don't trip on\n * tokens inside comments or strings.\n */\nfunction maskCommentsAndStrings(source: string): string {\n const out: string[] = [];\n let i = 0;\n const len = source.length;\n\n while (i < len) {\n const c = source[i]!;\n const next = source[i + 1];\n\n if (c === \"/\" && next === \"/\") {\n out.push(\" \");\n i += 2;\n while (i < len && source[i] !== \"\\n\") {\n out.push(\" \");\n i++;\n }\n continue;\n }\n\n if (c === \"/\" && next === \"*\") {\n out.push(\" \");\n i += 2;\n while (i < len && !(source[i] === \"*\" && source[i + 1] === \"/\")) {\n out.push(source[i] === \"\\n\" ? \"\\n\" : \" \");\n i++;\n }\n if (i < len) {\n out.push(\" \");\n i += 2;\n }\n continue;\n }\n\n if (c === '\"' || c === \"'\") {\n const quote = c;\n out.push(quote);\n i++;\n while (i < len && source[i] !== quote) {\n if (source[i] === \"\\\\\" && i + 1 < len) {\n out.push(\" \");\n i += 2;\n continue;\n }\n out.push(source[i] === \"\\n\" ? \"\\n\" : \" \");\n i++;\n }\n if (i < len) {\n out.push(quote);\n i++;\n }\n continue;\n }\n\n if (c === \"`\") {\n out.push(\"`\");\n i++;\n while (i < len && source[i] !== \"`\") {\n if (source[i] === \"\\\\\" && i + 1 < len) {\n out.push(\" \");\n i += 2;\n continue;\n }\n if (source[i] === \"$\" && source[i + 1] === \"{\") {\n // Preserve template expression so nested strings are masked by\n // the outer loop.\n out.push(\"${\");\n i += 2;\n let depth = 1;\n while (i < len && depth > 0) {\n out.push(source[i]!);\n if (source[i] === \"{\") depth++;\n else if (source[i] === \"}\") depth--;\n i++;\n }\n continue;\n }\n out.push(source[i] === \"\\n\" ? \"\\n\" : \" \");\n i++;\n }\n if (i < len) {\n out.push(\"`\");\n i++;\n }\n continue;\n }\n\n out.push(c);\n i++;\n }\n\n return out.join(\"\");\n}\n\ninterface ExportMatch {\n name: string;\n start: number;\n end: number;\n replacement: string;\n}\n\n/**\n * Find top-level `export const|let|var|function NAME` declarations whose\n * NAME is in `targetNames`. Returns the slice of the original source to\n * replace, along with a renamed declaration to put in its place.\n *\n * `export { NAME }`, `export default`, and destructuring forms are not\n * supported — users who need them should use the explicit wrappers.\n */\nfunction findExports(masked: string, targetNames: ReadonlySet<string>): ExportMatch[] {\n const matches: ExportMatch[] = [];\n const pattern =\n /^[ \\t]*(export)[ \\t]+(?:async[ \\t]+)?(?:const|let|var|function)[ \\t]+([A-Za-z_$][\\w$]*)/gm;\n\n let m: RegExpExecArray | null;\n while ((m = pattern.exec(masked))) {\n const [full, , name] = m;\n if (!targetNames.has(name!)) continue;\n\n const exportStart = m.index + full.indexOf(\"export\");\n const nameEnd = m.index + full.length;\n const renamed = `${ORIG_PREFIX}${name}`;\n\n const slice = masked.slice(exportStart, nameEnd);\n const replacement = slice\n .replace(/^export[ \\t]+/, \"\")\n .replace(new RegExp(`\\\\b${name}$`), renamed);\n\n matches.push({ name: name!, start: exportStart, end: nameEnd, replacement });\n }\n\n return matches;\n}\n\n/** Options for {@link effectKit}. */\nexport interface EffectKitPluginOptions {\n /**\n * Restrict transformation to a subset of SvelteKit file kinds.\n * Defaults to all standard route files.\n */\n include?: ReadonlyArray<EffectKitFileKind>;\n\n /**\n * Import specifier of a server-only module whose `runtime` (or default)\n * export is an app-wide runtime created with `makeServerRuntime`.\n *\n * When set, Effects returned from server route files (`+page.server.ts`,\n * `+layout.server.ts`, `+server.ts`, `hooks.server.ts`) have access to the\n * services of that runtime's layer.\n *\n * @example \"#lib/server/runtime.ts\"\n */\n serverRuntime?: string;\n\n /**\n * Import specifier of a universal module whose `runtime` (or default)\n * export is an app-wide runtime created with `makeUniversalRuntime`.\n *\n * When set, Effects returned from universal route files (`+page.ts`,\n * `+layout.ts`, `hooks.client.ts`) have access to the services of that\n * runtime's layer. The module must be safe to import in the browser.\n *\n * @example \"#lib/runtime.ts\"\n */\n clientRuntime?: string;\n}\n\n/**\n * Vite plugin that rewrites SvelteKit route exports so that Effects, or\n * functions returning Effects, are accepted wherever SvelteKit expects a\n * function-shaped export.\n */\nexport function effectKit(options: EffectKitPluginOptions = {}): Plugin {\n const include = options.include ? new Set(options.include) : null;\n\n return {\n name: \"effect-kit\",\n enforce: \"pre\",\n transform(code, id) {\n const kind = getFileKind(id);\n if (!kind) return null;\n if (include && !include.has(kind)) return null;\n\n const wrappers = wrapperByKind[kind];\n const targetNames = new Set(Object.keys(wrappers));\n\n const masked = maskCommentsAndStrings(code);\n const found = findExports(masked, targetNames);\n if (found.length === 0) return null;\n\n const s = new MagicString(code);\n\n for (const match of found) {\n s.overwrite(match.start, match.end, match.replacement);\n }\n\n const wrapperImports = Array.from(new Set(found.map((m) => wrappers[m.name]!)));\n\n // Server-only files get the server runtime module; universal/client\n // files get the client runtime module (it must be browser-safe).\n const runtimeSpecifier = serverKinds.has(kind)\n ? options.serverRuntime\n : options.clientRuntime;\n\n const imports =\n `\\nimport { ${wrapperImports.join(\", \")} } from ${JSON.stringify(INTERNAL_IMPORT)};\\n` +\n (runtimeSpecifier\n ? `import * as ${RUNTIME_IDENT} from ${JSON.stringify(runtimeSpecifier)};\\n`\n : \"\");\n const runtimeArg = runtimeSpecifier ? `, ${RUNTIME_IDENT}` : \"\";\n const exports = found\n .map(\n (m) =>\n `export const ${m.name} = ${wrappers[m.name]}(${ORIG_PREFIX}${m.name}${runtimeArg});`,\n )\n .join(\"\\n\");\n\n s.append(`\\n${imports}${exports}\\n`);\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: \"boundary\", source: id }),\n };\n },\n };\n}\n\nexport default effectKit;\n"],"mappings":"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"}
1
+ {"version":3,"file":"vite.js","names":[],"sources":["../src/vite.ts"],"sourcesContent":["/**\n * Vite plugin that lets you write SvelteKit route exports as Effects (or\n * functions returning Effects) without manually wrapping them.\n *\n * ```ts\n * // vite.config.ts\n * import { sveltekit } from \"@sveltejs/kit/vite\";\n * import { effectKit } from \"@thomasfosterau/effect-sveltekit/vite\";\n *\n * export default {\n * plugins: [effectKit(), sveltekit()],\n * };\n * ```\n *\n * ```ts\n * // +page.server.ts\n * import { Effect } from \"effect\";\n * import * as HttpClient from \"effect/http/HttpClient\";\n *\n * export const load = (event) =>\n * Effect.gen(function* () {\n * const client = yield* HttpClient.HttpClient;\n * return { hello: \"world\" };\n * });\n * ```\n *\n * Under the hood the plugin rewrites the export to call the same wrappers\n * that the explicit API (`serverLoad`, `actions`, `handle`, `endpoint`, ...)\n * uses. Plain functions that return promises or values pass through\n * unchanged, so the plugin is safe to enable on existing projects.\n */\n\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Rec from \"effect/Record\";\nimport * as Result from \"effect/Result\";\nimport MagicString from \"magic-string\";\nimport type { Plugin } from \"vite\";\n\nconst INTERNAL_IMPORT = \"@thomasfosterau/effect-sveltekit/internal/auto\";\nconst ORIG_PREFIX = \"__effect_kit_orig_\";\nconst RUNTIME_IDENT = \"__effect_kit_runtime\";\n\n/** The SvelteKit route and hook file kinds `effectKit()` can rewrite. */\nexport type EffectKitFileKind =\n | \"pageServer\"\n | \"page\"\n | \"layoutServer\"\n | \"layout\"\n | \"server\"\n | \"hooks\"\n | \"hooksServer\"\n | \"hooksClient\";\n\n/** File kinds whose exports only ever run on the server. */\nconst serverKinds: ReadonlyArray<EffectKitFileKind> = [\n \"pageServer\",\n \"layoutServer\",\n \"server\",\n \"hooksServer\",\n];\n\nconst wrapperByKind: Record<EffectKitFileKind, Record<string, string>> = {\n pageServer: { load: \"wrapServerLoad\", actions: \"wrapActions\" },\n page: { load: \"wrapLoad\" },\n layoutServer: { load: \"wrapServerLoad\" },\n layout: { load: \"wrapLoad\" },\n server: {\n GET: \"wrapEndpoint\",\n POST: \"wrapEndpoint\",\n PUT: \"wrapEndpoint\",\n PATCH: \"wrapEndpoint\",\n DELETE: \"wrapEndpoint\",\n OPTIONS: \"wrapEndpoint\",\n HEAD: \"wrapEndpoint\",\n fallback: \"wrapEndpoint\",\n },\n hooksServer: {\n handle: \"wrapHandle\",\n handleFetch: \"wrapHandleFetch\",\n handleError: \"wrapHandleError\",\n init: \"wrapInit\",\n },\n hooksClient: {\n handleError: \"wrapHandleError\",\n init: \"wrapInit\",\n },\n // Universal `src/hooks.ts`: only `reroute` is function-shaped (the `transport`\n // export is plain data and is intentionally left untouched).\n hooks: {\n reroute: \"wrapReroute\",\n },\n};\n\nfunction getFileKind(id: string): EffectKitFileKind | null {\n // Drop the query (everything from the first `?`), then normalise separators.\n const path = id.replace(/\\?[\\s\\S]*$/, \"\").replace(/\\\\/g, \"/\");\n if (/\\/\\+page\\.server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"pageServer\";\n if (/\\/\\+page\\.(?:ts|js|mts|mjs)$/.test(path)) return \"page\";\n if (/\\/\\+layout\\.server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"layoutServer\";\n if (/\\/\\+layout\\.(?:ts|js|mts|mjs)$/.test(path)) return \"layout\";\n if (/\\/\\+server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"server\";\n if (/\\/hooks\\.server\\.(?:ts|js|mts|mjs)$/.test(path)) return \"hooksServer\";\n if (/\\/hooks\\.client\\.(?:ts|js|mts|mjs)$/.test(path)) return \"hooksClient\";\n // Plain `hooks.ts` is the universal hooks file; checked last so the\n // `.server.`/`.client.` variants above take precedence.\n if (/\\/hooks\\.(?:ts|js|mts|mjs)$/.test(path)) return \"hooks\";\n return null;\n}\n\n/** `text` with every character but a newline replaced by a space. */\nconst blank = (text: string): string => text.replace(/[^\\n]/g, \" \");\n\n/**\n * A string or template literal's text, blanked: an escape (a backslash and the\n * character after it, even a newline) becomes two spaces, and every other\n * character but a newline one space.\n */\nconst blankLiteral = (text: string): string =>\n text.replace(/\\\\[\\s\\S]|[^\\n]/g, (match) => \" \".repeat(match.length));\n\n/** A line comment, or a block comment (unterminated: to the end of the source). */\nconst COMMENT = /\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y;\n/** A single- or double-quoted string (unterminated: to the end of the source). */\nconst QUOTED = /([\"'])((?:\\\\[\\s\\S]?|(?!\\1)[^\\\\])*)(\\1?)/y;\n/** A run of characters that start no comment or literal, or a lone `/`. */\nconst PLAIN = /[^/\"'`]+|\\//y;\n/** Template literal text up to its closing backtick, a `${`, or the end of the source. */\nconst TEMPLATE_TEXT = /(?:\\\\[\\s\\S]?|\\$(?!\\{)|[^`\\\\$])*/y;\n\n/** The text the sticky `pattern` matches at `index`, if any. */\nconst matchAt = (pattern: RegExp, source: string, index: number): RegExpExecArray | null => {\n pattern.lastIndex = index;\n return pattern.exec(source);\n};\n\n/**\n * The index just past the `}` closing a template expression whose body starts\n * at `from` (braces counted naively, as raw text), or the end of the source.\n */\nconst expressionEnd = (source: string, from: number): number => {\n let depth = 1;\n const closing = Arr.findFirst(source.slice(from).matchAll(/[{}]/g), (brace) => {\n depth += brace[0] === \"{\" ? 1 : -1;\n return depth === 0;\n });\n return Option.match(closing, {\n onNone: () => source.length,\n onSome: (brace) => from + brace.index + 1,\n });\n};\n\n/** Where {@link maskCommentsAndStrings} is: an offset, inside a template literal or not. */\ninterface MaskPosition {\n readonly index: number;\n readonly inTemplate: boolean;\n}\n\n/** The masked chunk at a position inside a template literal, and the position after it. */\nconst maskTemplateChunk = (source: string, index: number): readonly [string, MaskPosition] => {\n const text = matchAt(TEMPLATE_TEXT, source, index)?.[0] ?? \"\";\n const end = index + text.length;\n if (source.startsWith(\"${\", end)) {\n // Keep a template expression verbatim, up to its closing brace.\n const close = expressionEnd(source, end + 2);\n return [blankLiteral(text) + source.slice(end, close), { index: close, inTemplate: true }];\n }\n // The closing backtick, or nothing at the end of the source.\n return [blankLiteral(text) + source.slice(end, end + 1), { index: end + 1, inTemplate: false }];\n};\n\n/** The masked chunk at a position outside any template literal, and the position after it. */\nconst maskChunk = (source: string, index: number): readonly [string, MaskPosition] => {\n const comment = matchAt(COMMENT, source, index);\n if (comment !== null) {\n return [blank(comment[0]), { index: index + comment[0].length, inTemplate: false }];\n }\n const quoted = matchAt(QUOTED, source, index);\n if (quoted !== null) {\n const [whole, quote = \"\", body = \"\", closing = \"\"] = quoted;\n return [\n quote + blankLiteral(body) + closing,\n { index: index + whole.length, inTemplate: false },\n ];\n }\n if (source.startsWith(\"`\", index)) return [\"`\", { index: index + 1, inTemplate: true }];\n const plain = matchAt(PLAIN, source, index)?.[0] ?? source.charAt(index);\n return [plain, { index: index + plain.length, inTemplate: false }];\n};\n\n/**\n * Returns a copy of `source` with line/block comments and string/template\n * contents replaced by spaces (newlines preserved). Byte offsets in the\n * returned string line up with the original, so regex scans don't trip on\n * tokens inside comments or strings.\n */\nconst maskCommentsAndStrings = (source: string): string =>\n Arr.unfold<MaskPosition, string>({ index: 0, inTemplate: false }, (position) =>\n position.index >= source.length\n ? Option.none()\n : Option.some(\n position.inTemplate\n ? maskTemplateChunk(source, position.index)\n : maskChunk(source, position.index),\n ),\n ).join(\"\");\n\ninterface ExportMatch {\n name: string;\n start: number;\n end: number;\n replacement: string;\n}\n\n/**\n * Find top-level `export const|let|var|function NAME` declarations whose\n * NAME is in `targetNames`. Returns the slice of the original source to\n * replace, along with a renamed declaration to put in its place.\n *\n * `export { NAME }`, `export default`, and destructuring forms are not\n * supported — users who need them should use the explicit wrappers.\n */\nfunction findExports(masked: string, isTarget: (name: string) => boolean): Array<ExportMatch> {\n const pattern =\n /^[ \\t]*(export)[ \\t]+(?:async[ \\t]+)?(?:const|let|var|function)[ \\t]+([A-Za-z_$][\\w$]*)/gm;\n\n return Arr.filterMap(masked.matchAll(pattern), (m) => {\n const [full, , name] = m;\n if (name === undefined || !isTarget(name)) return Result.failVoid;\n\n const exportStart = m.index + full.indexOf(\"export\");\n const nameEnd = m.index + full.length;\n const renamed = `${ORIG_PREFIX}${name}`;\n\n const slice = masked.slice(exportStart, nameEnd);\n const replacement = slice\n .replace(/^export[ \\t]+/, \"\")\n .replace(new RegExp(`\\\\b${name}$`), renamed);\n\n return Result.succeed({ name, start: exportStart, end: nameEnd, replacement });\n });\n}\n\n/** Options for {@link effectKit}. */\nexport interface EffectKitPluginOptions {\n /**\n * Restrict transformation to a subset of SvelteKit file kinds.\n * Defaults to all standard route files.\n */\n include?: ReadonlyArray<EffectKitFileKind>;\n\n /**\n * Import specifier of a server-only module whose `runtime` (or default)\n * export is an app-wide runtime created with `makeServerRuntime`.\n *\n * When set, Effects returned from server route files (`+page.server.ts`,\n * `+layout.server.ts`, `+server.ts`, `hooks.server.ts`) have access to the\n * services of that runtime's layer.\n *\n * @example \"#lib/server/runtime.ts\"\n */\n serverRuntime?: string;\n\n /**\n * Import specifier of a universal module whose `runtime` (or default)\n * export is an app-wide runtime created with `makeUniversalRuntime`.\n *\n * When set, Effects returned from universal route files (`+page.ts`,\n * `+layout.ts`, `hooks.client.ts`) have access to the services of that\n * runtime's layer. The module must be safe to import in the browser.\n *\n * @example \"#lib/runtime.ts\"\n */\n clientRuntime?: string;\n}\n\n/**\n * Vite plugin that rewrites SvelteKit route exports so that Effects, or\n * functions returning Effects, are accepted wherever SvelteKit expects a\n * function-shaped export.\n */\nexport function effectKit(options: EffectKitPluginOptions = {}): Plugin {\n const { include } = options;\n\n return {\n name: \"effect-kit\",\n enforce: \"pre\",\n transform(code, id) {\n const kind = getFileKind(id);\n if (!kind) return null;\n if (include && !Arr.contains(include, kind)) return null;\n\n const wrappers = wrapperByKind[kind];\n\n const masked = maskCommentsAndStrings(code);\n const found = findExports(masked, (name) => Rec.has(wrappers, name));\n if (Arr.isReadonlyArrayEmpty(found)) return null;\n\n const s = new MagicString(code);\n\n Arr.forEach(found, (match) => {\n s.overwrite(match.start, match.end, match.replacement);\n });\n\n const wrapperImports = Arr.dedupe(\n Arr.filterMap(found, (m) => Result.fromOption(Rec.get(wrappers, m.name), () => m.name)),\n );\n\n // Server-only files get the server runtime module; universal/client\n // files get the client runtime module (it must be browser-safe).\n const runtimeSpecifier = Arr.contains(serverKinds, kind)\n ? options.serverRuntime\n : options.clientRuntime;\n\n const imports =\n `\\nimport { ${wrapperImports.join(\", \")} } from ${JSON.stringify(INTERNAL_IMPORT)};\\n` +\n (runtimeSpecifier\n ? `import * as ${RUNTIME_IDENT} from ${JSON.stringify(runtimeSpecifier)};\\n`\n : \"\");\n const runtimeArg = runtimeSpecifier ? `, ${RUNTIME_IDENT}` : \"\";\n const exports = found\n .map(\n (m) =>\n `export const ${m.name} = ${wrappers[m.name]}(${ORIG_PREFIX}${m.name}${runtimeArg});`,\n )\n .join(\"\\n\");\n\n s.append(`\\n${imports}${exports}\\n`);\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: \"boundary\", source: id }),\n };\n },\n };\n}\n\nexport default effectKit;\n"],"mappings":"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"}