@isikk/core 0.2.0 → 0.3.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.
@@ -0,0 +1,18 @@
1
+ import { C as ConfigSchema, P as PublicConfigOptions, a as PublicConfig } from '../../shared-By0kkXDs.js';
2
+ import 'react';
3
+
4
+ /**
5
+ * The browser half of `publicConfig()`, substituted for the server module by the `browser`
6
+ * export condition in package.json rather than branched to at runtime. That substitution is what
7
+ * makes the split safe: this file contains no reference to `process.env` at all, so bundling the
8
+ * config module for the client cannot ship a server value no matter what the schema says. It is a
9
+ * property of what the file contains, not a discipline anyone has to maintain.
10
+ *
11
+ * The schema argument is accepted and ignored - values arrive already cast, through the injected
12
+ * global. It stays in the signature so `InferConfig<S>` produces the identical type on both
13
+ * sides, which is what lets one `app/config.ts` be imported by server and client components
14
+ * alike.
15
+ */
16
+ declare function publicConfig<S extends ConfigSchema>(_schema: S, options?: PublicConfigOptions): PublicConfig<S>;
17
+
18
+ export { publicConfig };
@@ -0,0 +1,54 @@
1
+ // src/node/configError.ts
2
+ var ConfigError = class extends Error {
3
+ };
4
+
5
+ // src/next/config/shared.ts
6
+ var GLOBAL_KEY_BASE = "__ISIK_PUBLIC_CONFIG__";
7
+ function globalKeyFor(prefix) {
8
+ return prefix === "" ? GLOBAL_KEY_BASE : `${GLOBAL_KEY_BASE}${prefix}__`;
9
+ }
10
+ function memoize(resolve) {
11
+ let value;
12
+ let resolved = false;
13
+ return () => {
14
+ if (!resolved) {
15
+ value = resolve();
16
+ resolved = true;
17
+ }
18
+ return value;
19
+ };
20
+ }
21
+ function lazyConfigProxy(resolve) {
22
+ return new Proxy({}, {
23
+ get: (_target, property) => Reflect.get(resolve(), property),
24
+ has: (_target, property) => Reflect.has(resolve(), property),
25
+ ownKeys: () => Reflect.ownKeys(resolve()),
26
+ getOwnPropertyDescriptor: (_target, property) => {
27
+ const descriptor = Reflect.getOwnPropertyDescriptor(resolve(), property);
28
+ return descriptor === void 0 ? void 0 : { ...descriptor, configurable: true };
29
+ }
30
+ });
31
+ }
32
+
33
+ // src/next/config/browser.ts
34
+ function publicConfig(_schema, options = {}) {
35
+ const globalKey = globalKeyFor(options.prefix ?? "");
36
+ return {
37
+ CONFIG: lazyConfigProxy(memoize(() => readInjectedConfig(globalKey))),
38
+ // Injection is a server-render concern; there is nothing to emit once the document exists.
39
+ PublicConfigScript: () => null
40
+ };
41
+ }
42
+ function readInjectedConfig(globalKey) {
43
+ const injected = globalThis[globalKey];
44
+ if (injected === void 0) {
45
+ throw new ConfigError(
46
+ `window.${globalKey} is not set, so there is no public config to read. Render <PublicConfigScript /> once in your root layout (inside a <Suspense> boundary if Cache Components is enabled). If this is a test or a non-Next renderer, assign the object yourself before anything reads the config.`
47
+ );
48
+ }
49
+ return injected;
50
+ }
51
+ export {
52
+ publicConfig
53
+ };
54
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/node/configError.ts","../../../src/next/config/shared.ts","../../../src/next/config/browser.ts"],"sourcesContent":["/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { ReactNode } from 'react'\n\nimport type { ConfigSchema, InferConfig } from '../../node/configCore'\n\nexport interface PublicConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nexport interface PublicConfigScriptProps {\n /** Forwarded to the injected `<script>` so a CSP with a per-request nonce keeps working. */\n nonce?: string\n}\n\nexport type PublicConfigScriptComponent = (props: PublicConfigScriptProps) => ReactNode | Promise<ReactNode>\n\nexport interface PublicConfig<S extends ConfigSchema> {\n CONFIG: InferConfig<S>\n PublicConfigScript: PublicConfigScriptComponent\n}\n\nconst GLOBAL_KEY_BASE = '__ISIK_PUBLIC_CONFIG__'\n\n/**\n * Namespaces the injected global by prefix, so two `publicConfig()` calls land on two properties\n * instead of the second one failing to redefine the first. Prefixes are already required to be\n * distinct from any server namespace, which makes them a usable key.\n */\nexport function globalKeyFor(prefix: string): string {\n return prefix === '' ? GLOBAL_KEY_BASE : `${GLOBAL_KEY_BASE}${prefix}__`\n}\n\n/**\n * Encodes a string as a JavaScript string literal that is safe to interpolate into a `<script>`\n * body. `JSON.stringify` alone is not: `</script>` inside a value closes the tag early and drops\n * the rest of the payload into the document as markup, and U+2028/U+2029 are literal line\n * terminators in JavaScript source, so a value containing one produces a syntax error. Escaping\n * `<` covers the first (the sequence can no longer be written) and the two explicit replacements\n * cover the second. `>` and `&` need no handling - a script element is raw text, so nothing in it\n * is parsed as markup or entities once `<` can't start a closing tag.\n */\nexport function jsStringLiteral(value: string): string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n}\n\n/**\n * The script that hands the resolved config to the browser. Values travel as a JSON string parsed\n * at runtime rather than as an object literal: it parses faster than equivalent JS source, and it\n * narrows everything that needs escaping down to the single quoted string handled above.\n *\n * The definition is deep-frozen and non-writable, so nothing can reshape config after hydration,\n * and re-entrant if the script somehow runs twice - redefining a non-configurable property would\n * throw, so an existing key means there is nothing left to do.\n */\nexport function serializePublicConfigScript(key: string, value: unknown): string {\n return (\n '(function(w,k,v){if(k in w)return;' +\n 'var f=function(o){if(o&&typeof o==\"object\"){for(var p in o)f(o[p]);Object.freeze(o)}};' +\n 'f(v);Object.defineProperty(w,k,{value:v,enumerable:true})})' +\n `(window,${jsStringLiteral(key)},JSON.parse(${jsStringLiteral(JSON.stringify(value))}))`\n )\n}\n\nexport function memoize<T>(resolve: () => T): () => T {\n let value: T\n let resolved = false\n return () => {\n if (!resolved) {\n value = resolve()\n resolved = true\n }\n return value\n }\n}\n\n/**\n * Presents `resolve()`'s result as a plain object without calling it until something is actually\n * read. That deferral is load-bearing on both sides of the package. On the server it keeps\n * `next build` from resolving anything while collecting page data, so a missing variable no\n * longer fails the build - \"can this build\" stops depending on \"is this configured\". In the\n * browser it means the injected global is read at access time rather than at chunk-evaluation\n * time, so an async chunk that happens to run before the inline script still sees the config.\n */\nexport function lazyConfigProxy<T>(resolve: () => T): T {\n return new Proxy({} as object, {\n get: (_target, property) => Reflect.get(resolve() as object, property),\n has: (_target, property) => Reflect.has(resolve() as object, property),\n ownKeys: () => Reflect.ownKeys(resolve() as object),\n getOwnPropertyDescriptor: (_target, property) => {\n const descriptor = Reflect.getOwnPropertyDescriptor(resolve() as object, property)\n // The proxy target is an empty object, and a proxy may not report a non-configurable\n // property that its target doesn't have - so re-mark descriptors as configurable, or\n // Object.keys()/JSON.stringify() over the config throw a TypeError.\n return descriptor === undefined ? undefined : { ...descriptor, configurable: true }\n },\n }) as T\n}\n","import type { ConfigSchema, InferConfig } from '../../node/configCore'\nimport { ConfigError } from '../../node/configError'\nimport { type PublicConfig, type PublicConfigOptions, globalKeyFor, lazyConfigProxy, memoize } from './shared'\n\n/**\n * The browser half of `publicConfig()`, substituted for the server module by the `browser`\n * export condition in package.json rather than branched to at runtime. That substitution is what\n * makes the split safe: this file contains no reference to `process.env` at all, so bundling the\n * config module for the client cannot ship a server value no matter what the schema says. It is a\n * property of what the file contains, not a discipline anyone has to maintain.\n *\n * The schema argument is accepted and ignored - values arrive already cast, through the injected\n * global. It stays in the signature so `InferConfig<S>` produces the identical type on both\n * sides, which is what lets one `app/config.ts` be imported by server and client components\n * alike.\n */\nexport function publicConfig<S extends ConfigSchema>(_schema: S, options: PublicConfigOptions = {}): PublicConfig<S> {\n const globalKey = globalKeyFor(options.prefix ?? '')\n\n return {\n CONFIG: lazyConfigProxy<InferConfig<S>>(memoize(() => readInjectedConfig<S>(globalKey))),\n // Injection is a server-render concern; there is nothing to emit once the document exists.\n PublicConfigScript: () => null,\n }\n}\n\nfunction readInjectedConfig<S extends ConfigSchema>(globalKey: string): InferConfig<S> {\n const injected = (globalThis as unknown as Record<string, unknown>)[globalKey]\n\n if (injected === undefined) {\n throw new ConfigError(\n `window.${globalKey} is not set, so there is no public config to read. Render <PublicConfigScript /> ` +\n 'once in your root layout (inside a <Suspense> boundary if Cache Components is enabled). If this is a ' +\n 'test or a non-Next renderer, assign the object yourself before anything reads the config.'\n )\n }\n\n return injected as InferConfig<S>\n}\n"],"mappings":";AAMO,IAAM,cAAN,cAA0B,MAAM;AAAC;;;ACiBxC,IAAM,kBAAkB;AAOjB,SAAS,aAAa,QAAwB;AACnD,SAAO,WAAW,KAAK,kBAAkB,GAAG,eAAe,GAAG,MAAM;AACtE;AAoCO,SAAS,QAAW,SAA2B;AACpD,MAAI;AACJ,MAAI,WAAW;AACf,SAAO,MAAM;AACX,QAAI,CAAC,UAAU;AACb,cAAQ,QAAQ;AAChB,iBAAW;AAAA,IACb;AACA,WAAO;AAAA,EACT;AACF;AAUO,SAAS,gBAAmB,SAAqB;AACtD,SAAO,IAAI,MAAM,CAAC,GAAa;AAAA,IAC7B,KAAK,CAAC,SAAS,aAAa,QAAQ,IAAI,QAAQ,GAAa,QAAQ;AAAA,IACrE,KAAK,CAAC,SAAS,aAAa,QAAQ,IAAI,QAAQ,GAAa,QAAQ;AAAA,IACrE,SAAS,MAAM,QAAQ,QAAQ,QAAQ,CAAW;AAAA,IAClD,0BAA0B,CAAC,SAAS,aAAa;AAC/C,YAAM,aAAa,QAAQ,yBAAyB,QAAQ,GAAa,QAAQ;AAIjF,aAAO,eAAe,SAAY,SAAY,EAAE,GAAG,YAAY,cAAc,KAAK;AAAA,IACpF;AAAA,EACF,CAAC;AACH;;;ACrFO,SAAS,aAAqC,SAAY,UAA+B,CAAC,GAAoB;AACnH,QAAM,YAAY,aAAa,QAAQ,UAAU,EAAE;AAEnD,SAAO;AAAA,IACL,QAAQ,gBAAgC,QAAQ,MAAM,mBAAsB,SAAS,CAAC,CAAC;AAAA;AAAA,IAEvF,oBAAoB,MAAM;AAAA,EAC5B;AACF;AAEA,SAAS,mBAA2C,WAAmC;AACrF,QAAM,WAAY,WAAkD,SAAS;AAE7E,MAAI,aAAa,QAAW;AAC1B,UAAM,IAAI;AAAA,MACR,UAAU,SAAS;AAAA,IAGrB;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,30 @@
1
+ import { C as ConfigSchema, P as PublicConfigOptions, a as PublicConfig } from '../../shared-By0kkXDs.js';
2
+ export { I as InferConfig, b as PublicConfigScriptComponent, c as PublicConfigScriptProps } from '../../shared-By0kkXDs.js';
3
+ import 'react';
4
+
5
+ /**
6
+ * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser
7
+ * half of `@isikk/core/next/config` can throw the same error type without importing
8
+ * anything that reads the environment. Keeping the split structural means the guarantee holds
9
+ * because of what the file contains, not because a bundler happened to tree-shake it away.
10
+ */
11
+ declare class ConfigError extends Error {
12
+ }
13
+
14
+ /**
15
+ * The browser-visible sibling of `config()`: same schema, same casters, same variable naming, but
16
+ * the resolved values are serialized into the document so client components can read them at
17
+ * runtime. Returns the config object plus the component that injects it, which the root layout
18
+ * renders exactly once.
19
+ *
20
+ * Everything in `schema` ends up in the HTML of every page, in plaintext. The prefix claimed here
21
+ * must not overlap one already claimed by `config()`, so a server-only key pasted into this
22
+ * schema by mistake resolves to nothing and throws rather than getting published.
23
+ *
24
+ * Values are read per request, not baked in at build - which is the entire point next to
25
+ * `NEXT_PUBLIC_*`, and what lets one image run in staging and production. Nothing resolves until
26
+ * something reads it, so `next build` needs none of these variables set.
27
+ */
28
+ declare function publicConfig<S extends ConfigSchema>(schema: S, options?: PublicConfigOptions): PublicConfig<S>;
29
+
30
+ export { ConfigError, ConfigSchema, PublicConfig, PublicConfigOptions, publicConfig };
@@ -0,0 +1,149 @@
1
+ // src/next/config/index.tsx
2
+ import { connection } from "next/server";
3
+
4
+ // src/node/configError.ts
5
+ var ConfigError = class extends Error {
6
+ };
7
+
8
+ // src/node/configCore.ts
9
+ function isCaster(value) {
10
+ return typeof value === "function";
11
+ }
12
+ function environmentKey(prefix, path, sep) {
13
+ return [...prefix ? [prefix] : [], ...path].join(sep);
14
+ }
15
+ function readLeaf(leafCaster, key) {
16
+ const rawValue = process.env[key];
17
+ if (rawValue === void 0) {
18
+ if ("missingDefault" in leafCaster) {
19
+ return leafCaster.missingDefault;
20
+ }
21
+ throw new ConfigError(
22
+ `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`
23
+ );
24
+ }
25
+ try {
26
+ return leafCaster(rawValue);
27
+ } catch (error) {
28
+ if ("errorDefault" in leafCaster) {
29
+ return leafCaster.errorDefault;
30
+ }
31
+ throw new ConfigError(
32
+ `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. Please check the value and the caster, or provide an errorDefault to your caster.`
33
+ );
34
+ }
35
+ }
36
+ function buildConfig(schema, prefix, sep, path = []) {
37
+ const result = {};
38
+ for (const [key, value] of Object.entries(schema)) {
39
+ const keyPath = [...path, key];
40
+ result[key] = isCaster(value) ? readLeaf(value, environmentKey(prefix, keyPath, sep)) : buildConfig(value, prefix, sep, keyPath);
41
+ }
42
+ return result;
43
+ }
44
+
45
+ // src/node/configRegistry.ts
46
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@isikk/core/config-namespace-registry");
47
+ var CALL_NAME = {
48
+ server: "config()",
49
+ public: "publicConfig()"
50
+ };
51
+ function getRegistry() {
52
+ const host = globalThis;
53
+ const existing = host[REGISTRY_KEY];
54
+ if (existing) {
55
+ return existing;
56
+ }
57
+ const created = [];
58
+ host[REGISTRY_KEY] = created;
59
+ return created;
60
+ }
61
+ function namespacesOverlap(a, b) {
62
+ if (a.prefix === b.prefix) {
63
+ return true;
64
+ }
65
+ if (a.prefix === "" || b.prefix === "") {
66
+ return false;
67
+ }
68
+ return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`);
69
+ }
70
+ function describeNamespace(namespace) {
71
+ return namespace.prefix === "" ? "no prefix" : `prefix ${JSON.stringify(namespace.prefix)}`;
72
+ }
73
+ function claimConfigNamespace(claim) {
74
+ const registry = getRegistry();
75
+ for (const existing of registry) {
76
+ if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {
77
+ return `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read the same environment variable namespace, so a key added to the public schema can resolve to a server-only value and be serialized into the browser. Give one of them a prefix the other does not use.`;
78
+ }
79
+ }
80
+ const alreadyClaimed = registry.some(
81
+ (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep
82
+ );
83
+ if (!alreadyClaimed) {
84
+ registry.push(claim);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ // src/next/config/index.tsx
90
+ import { PublicConfigInsert } from "./insert.js";
91
+
92
+ // src/next/config/shared.ts
93
+ var GLOBAL_KEY_BASE = "__ISIK_PUBLIC_CONFIG__";
94
+ function globalKeyFor(prefix) {
95
+ return prefix === "" ? GLOBAL_KEY_BASE : `${GLOBAL_KEY_BASE}${prefix}__`;
96
+ }
97
+ function jsStringLiteral(value) {
98
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
99
+ }
100
+ function serializePublicConfigScript(key, value) {
101
+ return `(function(w,k,v){if(k in w)return;var f=function(o){if(o&&typeof o=="object"){for(var p in o)f(o[p]);Object.freeze(o)}};f(v);Object.defineProperty(w,k,{value:v,enumerable:true})})(window,${jsStringLiteral(key)},JSON.parse(${jsStringLiteral(JSON.stringify(value))}))`;
102
+ }
103
+ function memoize(resolve) {
104
+ let value;
105
+ let resolved = false;
106
+ return () => {
107
+ if (!resolved) {
108
+ value = resolve();
109
+ resolved = true;
110
+ }
111
+ return value;
112
+ };
113
+ }
114
+ function lazyConfigProxy(resolve) {
115
+ return new Proxy({}, {
116
+ get: (_target, property) => Reflect.get(resolve(), property),
117
+ has: (_target, property) => Reflect.has(resolve(), property),
118
+ ownKeys: () => Reflect.ownKeys(resolve()),
119
+ getOwnPropertyDescriptor: (_target, property) => {
120
+ const descriptor = Reflect.getOwnPropertyDescriptor(resolve(), property);
121
+ return descriptor === void 0 ? void 0 : { ...descriptor, configurable: true };
122
+ }
123
+ });
124
+ }
125
+
126
+ // src/next/config/index.tsx
127
+ import { jsx } from "react/jsx-runtime";
128
+ function publicConfig(schema, options = {}) {
129
+ const { prefix, sep = "__" } = options;
130
+ const conflict = claimConfigNamespace({ kind: "public", prefix: prefix ?? "", sep });
131
+ if (conflict) {
132
+ throw new ConfigError(conflict);
133
+ }
134
+ const globalKey = globalKeyFor(prefix ?? "");
135
+ const resolve = memoize(() => buildConfig(schema, prefix, sep));
136
+ async function PublicConfigScript({ nonce }) {
137
+ await connection();
138
+ return /* @__PURE__ */ jsx(PublicConfigInsert, { script: serializePublicConfigScript(globalKey, resolve()), nonce });
139
+ }
140
+ return {
141
+ CONFIG: lazyConfigProxy(resolve),
142
+ PublicConfigScript
143
+ };
144
+ }
145
+ export {
146
+ ConfigError,
147
+ publicConfig
148
+ };
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/next/config/index.tsx","../../../src/node/configError.ts","../../../src/node/configCore.ts","../../../src/node/configRegistry.ts","../../../src/next/config/shared.ts"],"sourcesContent":["import { connection } from 'next/server'\n\nimport { type ConfigSchema, type InferConfig, buildConfig } from '../../node/configCore'\nimport { ConfigError } from '../../node/configError'\nimport { claimConfigNamespace } from '../../node/configRegistry'\n// Imported by built path, and marked external in tsup.next-config.config.ts, so esbuild leaves\n// the import alone instead of inlining insert.tsx into this bundle - which would concatenate\n// modules ahead of its `'use client'` directive and destroy it.\nimport { PublicConfigInsert } from './insert.js'\nimport {\n type PublicConfig,\n type PublicConfigOptions,\n type PublicConfigScriptProps,\n globalKeyFor,\n lazyConfigProxy,\n memoize,\n serializePublicConfigScript,\n} from './shared'\n\nexport { ConfigError } from '../../node/configError'\nexport type { ConfigSchema, InferConfig } from '../../node/configCore'\nexport type { PublicConfig, PublicConfigOptions, PublicConfigScriptComponent, PublicConfigScriptProps } from './shared'\n\n/**\n * The browser-visible sibling of `config()`: same schema, same casters, same variable naming, but\n * the resolved values are serialized into the document so client components can read them at\n * runtime. Returns the config object plus the component that injects it, which the root layout\n * renders exactly once.\n *\n * Everything in `schema` ends up in the HTML of every page, in plaintext. The prefix claimed here\n * must not overlap one already claimed by `config()`, so a server-only key pasted into this\n * schema by mistake resolves to nothing and throws rather than getting published.\n *\n * Values are read per request, not baked in at build - which is the entire point next to\n * `NEXT_PUBLIC_*`, and what lets one image run in staging and production. Nothing resolves until\n * something reads it, so `next build` needs none of these variables set.\n */\nexport function publicConfig<S extends ConfigSchema>(schema: S, options: PublicConfigOptions = {}): PublicConfig<S> {\n const { prefix, sep = '__' } = options\n\n const conflict = claimConfigNamespace({ kind: 'public', prefix: prefix ?? '', sep })\n if (conflict) {\n throw new ConfigError(conflict)\n }\n\n const globalKey = globalKeyFor(prefix ?? '')\n const resolve = memoize(() => buildConfig(schema, prefix, sep))\n\n async function PublicConfigScript({ nonce }: PublicConfigScriptProps) {\n // Opts the route out of static prerendering, so the values are read on the request rather\n // than frozen into the build output. Under Cache Components this has to sit inside a\n // <Suspense> boundary - see docs/next/config.md.\n await connection()\n return <PublicConfigInsert script={serializePublicConfigScript(globalKey, resolve())} nonce={nonce} />\n }\n\n return {\n CONFIG: lazyConfigProxy<InferConfig<S>>(resolve),\n PublicConfigScript,\n }\n}\n","/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { Caster } from './casters'\nimport { ConfigError } from './configError'\n\nexport type ConfigSchema = { [key: string]: Caster<unknown> | ConfigSchema }\n\nexport type InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never\n}\n\nexport interface ConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\n/**\n * Walks the schema and reads every leaf out of `process.env`. Shared by `config()` and by the\n * server half of `publicConfig()` so the two agree on variable naming and on the\n * `missingDefault`/`errorDefault` fallback rules by construction rather than by duplication.\n * Claims no namespace of its own - that is the caller's job, and the two callers claim different\n * kinds.\n */\nexport function buildConfig<S extends ConfigSchema>(\n schema: S,\n prefix: string | undefined,\n sep: string,\n path: string[] = []\n): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : buildConfig(value, prefix, sep, keyPath)\n }\n return result as InferConfig<S>\n}\n","/**\n * Process-wide record of which environment variable namespace each config call has claimed, so a\n * server-only `config()` and a browser-visible `publicConfig()` can be stopped from reading the\n * same one. That overlap is the mistake worth catching: with a shared namespace, a key pasted\n * into the public schema by accident resolves to the real server value and gets serialized into\n * the HTML of every page, silently. With disjoint namespaces it resolves to nothing and throws.\n *\n * Kept on `globalThis` under a `Symbol.for` key rather than in module scope because\n * `@isikk/core/node` and `@isikk/core/next/config` are separate tsup entries built\n * with `splitting: false` - a module-scoped registry would be duplicated into each bundle, giving\n * the two sides one registry each and so nothing to ever collide in.\n *\n * Claims are keyed by kind, so any number of `config()` calls (or any number of `publicConfig()`\n * calls) may share a namespace - two server reads of the same variable are harmless. Only a\n * server/public overlap is a conflict, which is also why re-registration is always safe: Next\n * evaluates the same module once per bundler layer (RSC, SSR, edge) and again on every Fast\n * Refresh, and every one of those repeats is the same kind claiming the same namespace.\n */\nexport type ConfigKind = 'server' | 'public'\n\nexport interface ConfigNamespace {\n kind: ConfigKind\n /** Empty string means \"no prefix\" - the root of the environment. */\n prefix: string\n sep: string\n}\n\nconst REGISTRY_KEY = Symbol.for('@isikk/core/config-namespace-registry')\n\nconst CALL_NAME: Record<ConfigKind, string> = {\n server: 'config()',\n public: 'publicConfig()',\n}\n\nfunction getRegistry(): ConfigNamespace[] {\n const host = globalThis as unknown as Record<symbol, ConfigNamespace[] | undefined>\n const existing = host[REGISTRY_KEY]\n if (existing) {\n return existing\n }\n const created: ConfigNamespace[] = []\n host[REGISTRY_KEY] = created\n return created\n}\n\n/**\n * Two namespaces overlap when one can produce an environment variable name the other can also\n * produce. Identical prefixes always overlap. A prefix nested under another at a separator\n * boundary overlaps too (`APP` and `APP__PUBLIC` both reach `APP__PUBLIC__TOKEN`).\n *\n * An absent prefix is deliberately treated as disjoint from every non-empty one rather than as\n * the root that technically contains them all: unprefixed server config alongside prefixed public\n * config is the most natural setup there is, and the only way it actually collides is a server\n * schema with a top-level key named exactly like the public prefix. Rejecting the whole shape to\n * catch that would cost far more than it buys - docs/next/config.md says so out loud.\n */\nfunction namespacesOverlap(a: ConfigNamespace, b: ConfigNamespace): boolean {\n if (a.prefix === b.prefix) {\n return true\n }\n if (a.prefix === '' || b.prefix === '') {\n return false\n }\n return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`)\n}\n\nfunction describeNamespace(namespace: ConfigNamespace): string {\n return namespace.prefix === '' ? 'no prefix' : `prefix ${JSON.stringify(namespace.prefix)}`\n}\n\n/**\n * Records `claim`, returning `null` when it is allowed or an explanatory message when it overlaps\n * a namespace already claimed by the other kind. Returns the message instead of throwing so each\n * entry point can throw its own bundled copy of `ConfigError`, keeping `instanceof` working\n * against the class imported from the same entry point the call came from.\n */\nexport function claimConfigNamespace(claim: ConfigNamespace): string | null {\n const registry = getRegistry()\n\n for (const existing of registry) {\n if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {\n return (\n `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ` +\n `${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read ` +\n 'the same environment variable namespace, so a key added to the public schema can resolve to ' +\n 'a server-only value and be serialized into the browser. Give one of them a prefix the other ' +\n 'does not use.'\n )\n }\n }\n\n const alreadyClaimed = registry.some(\n (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep\n )\n if (!alreadyClaimed) {\n registry.push(claim)\n }\n\n return null\n}\n\n/** Test-only reset. Deliberately not re-exported from any of the package's public entry points. */\nexport function resetConfigNamespaces(): void {\n getRegistry().length = 0\n}\n","import type { ReactNode } from 'react'\n\nimport type { ConfigSchema, InferConfig } from '../../node/configCore'\n\nexport interface PublicConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nexport interface PublicConfigScriptProps {\n /** Forwarded to the injected `<script>` so a CSP with a per-request nonce keeps working. */\n nonce?: string\n}\n\nexport type PublicConfigScriptComponent = (props: PublicConfigScriptProps) => ReactNode | Promise<ReactNode>\n\nexport interface PublicConfig<S extends ConfigSchema> {\n CONFIG: InferConfig<S>\n PublicConfigScript: PublicConfigScriptComponent\n}\n\nconst GLOBAL_KEY_BASE = '__ISIK_PUBLIC_CONFIG__'\n\n/**\n * Namespaces the injected global by prefix, so two `publicConfig()` calls land on two properties\n * instead of the second one failing to redefine the first. Prefixes are already required to be\n * distinct from any server namespace, which makes them a usable key.\n */\nexport function globalKeyFor(prefix: string): string {\n return prefix === '' ? GLOBAL_KEY_BASE : `${GLOBAL_KEY_BASE}${prefix}__`\n}\n\n/**\n * Encodes a string as a JavaScript string literal that is safe to interpolate into a `<script>`\n * body. `JSON.stringify` alone is not: `</script>` inside a value closes the tag early and drops\n * the rest of the payload into the document as markup, and U+2028/U+2029 are literal line\n * terminators in JavaScript source, so a value containing one produces a syntax error. Escaping\n * `<` covers the first (the sequence can no longer be written) and the two explicit replacements\n * cover the second. `>` and `&` need no handling - a script element is raw text, so nothing in it\n * is parsed as markup or entities once `<` can't start a closing tag.\n */\nexport function jsStringLiteral(value: string): string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n}\n\n/**\n * The script that hands the resolved config to the browser. Values travel as a JSON string parsed\n * at runtime rather than as an object literal: it parses faster than equivalent JS source, and it\n * narrows everything that needs escaping down to the single quoted string handled above.\n *\n * The definition is deep-frozen and non-writable, so nothing can reshape config after hydration,\n * and re-entrant if the script somehow runs twice - redefining a non-configurable property would\n * throw, so an existing key means there is nothing left to do.\n */\nexport function serializePublicConfigScript(key: string, value: unknown): string {\n return (\n '(function(w,k,v){if(k in w)return;' +\n 'var f=function(o){if(o&&typeof o==\"object\"){for(var p in o)f(o[p]);Object.freeze(o)}};' +\n 'f(v);Object.defineProperty(w,k,{value:v,enumerable:true})})' +\n `(window,${jsStringLiteral(key)},JSON.parse(${jsStringLiteral(JSON.stringify(value))}))`\n )\n}\n\nexport function memoize<T>(resolve: () => T): () => T {\n let value: T\n let resolved = false\n return () => {\n if (!resolved) {\n value = resolve()\n resolved = true\n }\n return value\n }\n}\n\n/**\n * Presents `resolve()`'s result as a plain object without calling it until something is actually\n * read. That deferral is load-bearing on both sides of the package. On the server it keeps\n * `next build` from resolving anything while collecting page data, so a missing variable no\n * longer fails the build - \"can this build\" stops depending on \"is this configured\". In the\n * browser it means the injected global is read at access time rather than at chunk-evaluation\n * time, so an async chunk that happens to run before the inline script still sees the config.\n */\nexport function lazyConfigProxy<T>(resolve: () => T): T {\n return new Proxy({} as object, {\n get: (_target, property) => Reflect.get(resolve() as object, property),\n has: (_target, property) => Reflect.has(resolve() as object, property),\n ownKeys: () => Reflect.ownKeys(resolve() as object),\n getOwnPropertyDescriptor: (_target, property) => {\n const descriptor = Reflect.getOwnPropertyDescriptor(resolve() as object, property)\n // The proxy target is an empty object, and a proxy may not report a non-configurable\n // property that its target doesn't have - so re-mark descriptors as configurable, or\n // Object.keys()/JSON.stringify() over the config throw a TypeError.\n return descriptor === undefined ? undefined : { ...descriptor, configurable: true }\n },\n }) as T\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Emits the pre-serialized config script into the document head during SSR.
3
+ *
4
+ * `useServerInsertedHTML` rather than rendering a `<script>` in the layout, because the App
5
+ * Router puts its own chunk scripts in the head with `async` - an inline script sitting in the
6
+ * body can therefore execute *after* a chunk that already tried to read the config. Head
7
+ * insertion puts it ahead of them. (React 19's script hoisting is no help here: it hoists `src`
8
+ * scripts, not inline ones.)
9
+ *
10
+ * Lives in its own module because of the `'use client'` directive, which Next requires to be the
11
+ * literal first statement of the file it applies to - see docs/next/config.md.
12
+ */
13
+ declare function PublicConfigInsert({ script, nonce }: {
14
+ script: string;
15
+ nonce?: string;
16
+ }): null;
17
+
18
+ export { PublicConfigInsert };
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ // src/next/config/insert.tsx
4
+ import { useRef } from "react";
5
+ import { useServerInsertedHTML } from "next/navigation";
6
+ import { jsx } from "react/jsx-runtime";
7
+ function PublicConfigInsert({ script, nonce }) {
8
+ const inserted = useRef(false);
9
+ useServerInsertedHTML(() => {
10
+ if (inserted.current) {
11
+ return null;
12
+ }
13
+ inserted.current = true;
14
+ return /* @__PURE__ */ jsx("script", { nonce, dangerouslySetInnerHTML: { __html: script } });
15
+ });
16
+ return null;
17
+ }
18
+ export {
19
+ PublicConfigInsert
20
+ };
21
+ //# sourceMappingURL=insert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/next/config/insert.tsx"],"sourcesContent":["'use client'\n\nimport { useRef } from 'react'\n\nimport { useServerInsertedHTML } from 'next/navigation'\n\n/**\n * Emits the pre-serialized config script into the document head during SSR.\n *\n * `useServerInsertedHTML` rather than rendering a `<script>` in the layout, because the App\n * Router puts its own chunk scripts in the head with `async` - an inline script sitting in the\n * body can therefore execute *after* a chunk that already tried to read the config. Head\n * insertion puts it ahead of them. (React 19's script hoisting is no help here: it hoists `src`\n * scripts, not inline ones.)\n *\n * Lives in its own module because of the `'use client'` directive, which Next requires to be the\n * literal first statement of the file it applies to - see docs/next/config.md.\n */\nexport function PublicConfigInsert({ script, nonce }: { script: string; nonce?: string }) {\n const inserted = useRef(false)\n\n useServerInsertedHTML(() => {\n // React can invoke the callback more than once; the script guards against re-running itself\n // anyway, but emitting one copy of the payload per flush would be pure page weight.\n if (inserted.current) {\n return null\n }\n inserted.current = true\n return <script nonce={nonce} dangerouslySetInnerHTML={{ __html: script }} />\n })\n\n return null\n}\n"],"mappings":";;;AAEA,SAAS,cAAc;AAEvB,SAAS,6BAA6B;AAwB3B;AAVJ,SAAS,mBAAmB,EAAE,QAAQ,MAAM,GAAuC;AACxF,QAAM,WAAW,OAAO,KAAK;AAE7B,wBAAsB,MAAM;AAG1B,QAAI,SAAS,SAAS;AACpB,aAAO;AAAA,IACT;AACA,aAAS,UAAU;AACnB,WAAO,oBAAC,YAAO,OAAc,yBAAyB,EAAE,QAAQ,OAAO,GAAG;AAAA,EAC5E,CAAC;AAED,SAAO;AACT;","names":[]}
@@ -88,9 +88,11 @@ var commaSeparatedList = caster((value) => value.split(","));
88
88
  var commaSeparatedIntList = caster((value) => value.split(",").map(parseStrictInteger));
89
89
  var commaSeparatedFloatList = caster((value) => value.split(",").map(parseStrictFloat));
90
90
 
91
- // src/node/config.ts
91
+ // src/node/configError.ts
92
92
  var ConfigError = class extends Error {
93
93
  };
94
+
95
+ // src/node/configCore.ts
94
96
  function isCaster(value) {
95
97
  return typeof value === "function";
96
98
  }
@@ -118,17 +120,67 @@ function readLeaf(leafCaster, key) {
118
120
  );
119
121
  }
120
122
  }
121
- function build(schema, path2, prefix, sep) {
123
+ function buildConfig(schema, prefix, sep, path2 = []) {
122
124
  const result = {};
123
125
  for (const [key, value] of Object.entries(schema)) {
124
126
  const keyPath = [...path2, key];
125
- result[key] = isCaster(value) ? readLeaf(value, environmentKey(prefix, keyPath, sep)) : build(value, keyPath, prefix, sep);
127
+ result[key] = isCaster(value) ? readLeaf(value, environmentKey(prefix, keyPath, sep)) : buildConfig(value, prefix, sep, keyPath);
126
128
  }
127
129
  return result;
128
130
  }
131
+
132
+ // src/node/configRegistry.ts
133
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@isikk/core/config-namespace-registry");
134
+ var CALL_NAME = {
135
+ server: "config()",
136
+ public: "publicConfig()"
137
+ };
138
+ function getRegistry() {
139
+ const host = globalThis;
140
+ const existing = host[REGISTRY_KEY];
141
+ if (existing) {
142
+ return existing;
143
+ }
144
+ const created = [];
145
+ host[REGISTRY_KEY] = created;
146
+ return created;
147
+ }
148
+ function namespacesOverlap(a, b) {
149
+ if (a.prefix === b.prefix) {
150
+ return true;
151
+ }
152
+ if (a.prefix === "" || b.prefix === "") {
153
+ return false;
154
+ }
155
+ return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`);
156
+ }
157
+ function describeNamespace(namespace) {
158
+ return namespace.prefix === "" ? "no prefix" : `prefix ${JSON.stringify(namespace.prefix)}`;
159
+ }
160
+ function claimConfigNamespace(claim) {
161
+ const registry2 = getRegistry();
162
+ for (const existing of registry2) {
163
+ if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {
164
+ return `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read the same environment variable namespace, so a key added to the public schema can resolve to a server-only value and be serialized into the browser. Give one of them a prefix the other does not use.`;
165
+ }
166
+ }
167
+ const alreadyClaimed = registry2.some(
168
+ (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep
169
+ );
170
+ if (!alreadyClaimed) {
171
+ registry2.push(claim);
172
+ }
173
+ return null;
174
+ }
175
+
176
+ // src/node/config.ts
129
177
  function config(schema, options = {}) {
130
178
  const { prefix, sep = "__" } = options;
131
- return build(schema, [], prefix, sep);
179
+ const conflict = claimConfigNamespace({ kind: "server", prefix: prefix ?? "", sep });
180
+ if (conflict) {
181
+ throw new ConfigError(conflict);
182
+ }
183
+ return buildConfig(schema, prefix, sep);
132
184
  }
133
185
 
134
186
  // src/node/contextLocal.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/node/index.ts","../../src/node/casters.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export * from './casters'\nexport * from './config'\nexport * from './contextLocal'\nexport * from './getFileAsString'\n","export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","import type { Caster } from './casters'\n\nexport class ConfigError extends Error {}\n\ntype Schema = { [key: string]: Caster<unknown> | Schema }\n\ntype InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends Schema ? InferConfig<S[K]> : never\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\nfunction build<S extends Schema>(schema: S, path: string[], prefix: string | undefined, sep: string): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : build(value, keyPath, prefix, sep)\n }\n return result as InferConfig<S>\n}\n\nexport function config<S extends Schema>(schema: S, options: { prefix?: string; sep?: string } = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n return build(schema, [], prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path","path","fs"]}
1
+ {"version":3,"sources":["../../src/node/index.ts","../../src/node/casters.ts","../../src/node/configError.ts","../../src/node/configCore.ts","../../src/node/configRegistry.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export * from './casters'\nexport * from './config'\nexport * from './contextLocal'\nexport * from './getFileAsString'\n","export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { Caster } from './casters'\nimport { ConfigError } from './configError'\n\nexport type ConfigSchema = { [key: string]: Caster<unknown> | ConfigSchema }\n\nexport type InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never\n}\n\nexport interface ConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\n/**\n * Walks the schema and reads every leaf out of `process.env`. Shared by `config()` and by the\n * server half of `publicConfig()` so the two agree on variable naming and on the\n * `missingDefault`/`errorDefault` fallback rules by construction rather than by duplication.\n * Claims no namespace of its own - that is the caller's job, and the two callers claim different\n * kinds.\n */\nexport function buildConfig<S extends ConfigSchema>(\n schema: S,\n prefix: string | undefined,\n sep: string,\n path: string[] = []\n): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : buildConfig(value, prefix, sep, keyPath)\n }\n return result as InferConfig<S>\n}\n","/**\n * Process-wide record of which environment variable namespace each config call has claimed, so a\n * server-only `config()` and a browser-visible `publicConfig()` can be stopped from reading the\n * same one. That overlap is the mistake worth catching: with a shared namespace, a key pasted\n * into the public schema by accident resolves to the real server value and gets serialized into\n * the HTML of every page, silently. With disjoint namespaces it resolves to nothing and throws.\n *\n * Kept on `globalThis` under a `Symbol.for` key rather than in module scope because\n * `@isikk/core/node` and `@isikk/core/next/config` are separate tsup entries built\n * with `splitting: false` - a module-scoped registry would be duplicated into each bundle, giving\n * the two sides one registry each and so nothing to ever collide in.\n *\n * Claims are keyed by kind, so any number of `config()` calls (or any number of `publicConfig()`\n * calls) may share a namespace - two server reads of the same variable are harmless. Only a\n * server/public overlap is a conflict, which is also why re-registration is always safe: Next\n * evaluates the same module once per bundler layer (RSC, SSR, edge) and again on every Fast\n * Refresh, and every one of those repeats is the same kind claiming the same namespace.\n */\nexport type ConfigKind = 'server' | 'public'\n\nexport interface ConfigNamespace {\n kind: ConfigKind\n /** Empty string means \"no prefix\" - the root of the environment. */\n prefix: string\n sep: string\n}\n\nconst REGISTRY_KEY = Symbol.for('@isikk/core/config-namespace-registry')\n\nconst CALL_NAME: Record<ConfigKind, string> = {\n server: 'config()',\n public: 'publicConfig()',\n}\n\nfunction getRegistry(): ConfigNamespace[] {\n const host = globalThis as unknown as Record<symbol, ConfigNamespace[] | undefined>\n const existing = host[REGISTRY_KEY]\n if (existing) {\n return existing\n }\n const created: ConfigNamespace[] = []\n host[REGISTRY_KEY] = created\n return created\n}\n\n/**\n * Two namespaces overlap when one can produce an environment variable name the other can also\n * produce. Identical prefixes always overlap. A prefix nested under another at a separator\n * boundary overlaps too (`APP` and `APP__PUBLIC` both reach `APP__PUBLIC__TOKEN`).\n *\n * An absent prefix is deliberately treated as disjoint from every non-empty one rather than as\n * the root that technically contains them all: unprefixed server config alongside prefixed public\n * config is the most natural setup there is, and the only way it actually collides is a server\n * schema with a top-level key named exactly like the public prefix. Rejecting the whole shape to\n * catch that would cost far more than it buys - docs/next/config.md says so out loud.\n */\nfunction namespacesOverlap(a: ConfigNamespace, b: ConfigNamespace): boolean {\n if (a.prefix === b.prefix) {\n return true\n }\n if (a.prefix === '' || b.prefix === '') {\n return false\n }\n return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`)\n}\n\nfunction describeNamespace(namespace: ConfigNamespace): string {\n return namespace.prefix === '' ? 'no prefix' : `prefix ${JSON.stringify(namespace.prefix)}`\n}\n\n/**\n * Records `claim`, returning `null` when it is allowed or an explanatory message when it overlaps\n * a namespace already claimed by the other kind. Returns the message instead of throwing so each\n * entry point can throw its own bundled copy of `ConfigError`, keeping `instanceof` working\n * against the class imported from the same entry point the call came from.\n */\nexport function claimConfigNamespace(claim: ConfigNamespace): string | null {\n const registry = getRegistry()\n\n for (const existing of registry) {\n if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {\n return (\n `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ` +\n `${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read ` +\n 'the same environment variable namespace, so a key added to the public schema can resolve to ' +\n 'a server-only value and be serialized into the browser. Give one of them a prefix the other ' +\n 'does not use.'\n )\n }\n }\n\n const alreadyClaimed = registry.some(\n (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep\n )\n if (!alreadyClaimed) {\n registry.push(claim)\n }\n\n return null\n}\n\n/** Test-only reset. Deliberately not re-exported from any of the package's public entry points. */\nexport function resetConfigNamespaces(): void {\n getRegistry().length = 0\n}\n","import { type ConfigOptions, type ConfigSchema, type InferConfig, buildConfig } from './configCore'\nimport { ConfigError } from './configError'\nimport { claimConfigNamespace } from './configRegistry'\n\nexport { ConfigError } from './configError'\nexport type { ConfigOptions, ConfigSchema, InferConfig } from './configCore'\n\n/**\n * Builds a typed config object by reading and casting environment variables against a schema,\n * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the\n * caster for that key was given a `missingDefault`/`errorDefault`).\n *\n * Values read here are server-only: nothing in this module serializes them anywhere. Claims its\n * prefix as a server namespace, so a `publicConfig()` call that would read the same variable\n * names throws instead of quietly publishing them - see `@isikk/core/next/config`.\n */\nexport function config<S extends ConfigSchema>(schema: S, options: ConfigOptions = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n\n const conflict = claimConfigNamespace({ kind: 'server', prefix: prefix ?? '', sep })\n if (conflict) {\n throw new ConfigError(conflict)\n }\n\n return buildConfig(schema, prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path","registry","path","fs"]}
@@ -37,21 +37,41 @@ declare const commaSeparatedFloatList: (options?: {
37
37
  errorDefault?: number[] | undefined;
38
38
  }) => Caster<number[]>;
39
39
 
40
- declare class ConfigError extends Error {
41
- }
42
- type Schema = {
43
- [key: string]: Caster<unknown> | Schema;
40
+ type ConfigSchema = {
41
+ [key: string]: Caster<unknown> | ConfigSchema;
44
42
  };
45
43
  type InferConfig<S> = {
46
- [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends Schema ? InferConfig<S[K]> : never;
44
+ [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never;
47
45
  };
48
- declare function config<S extends Schema>(schema: S, options?: {
46
+ interface ConfigOptions {
47
+ /** Prepended to every environment variable name this call reads, joined with `sep`. */
49
48
  prefix?: string;
49
+ /** Joins the prefix and the nested key path into a variable name. Defaults to `"__"`. */
50
50
  sep?: string;
51
- }): InferConfig<S>;
51
+ }
52
+
53
+ /**
54
+ * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser
55
+ * half of `@isikk/core/next/config` can throw the same error type without importing
56
+ * anything that reads the environment. Keeping the split structural means the guarantee holds
57
+ * because of what the file contains, not because a bundler happened to tree-shake it away.
58
+ */
59
+ declare class ConfigError extends Error {
60
+ }
61
+
62
+ /**
63
+ * Builds a typed config object by reading and casting environment variables against a schema,
64
+ * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the
65
+ * caster for that key was given a `missingDefault`/`errorDefault`).
66
+ *
67
+ * Values read here are server-only: nothing in this module serializes them anywhere. Claims its
68
+ * prefix as a server namespace, so a `publicConfig()` call that would read the same variable
69
+ * names throws instead of quietly publishing them - see `@isikk/core/next/config`.
70
+ */
71
+ declare function config<S extends ConfigSchema>(schema: S, options?: ConfigOptions): InferConfig<S>;
52
72
 
53
73
  declare function contextLocal<T>(name: string): AsyncLocalStorage<T>;
54
74
 
55
75
  declare function getFileAsString(filename: string): Promise<string>;
56
76
 
57
- export { type Caster, ConfigError, boolean, caster, commaSeparatedFloatList, commaSeparatedIntList, commaSeparatedList, config, contextLocal, float, getFileAsString, integer, string };
77
+ export { type Caster, ConfigError, type ConfigOptions, type ConfigSchema, type InferConfig, boolean, caster, commaSeparatedFloatList, commaSeparatedIntList, commaSeparatedList, config, contextLocal, float, getFileAsString, integer, string };
@@ -37,21 +37,41 @@ declare const commaSeparatedFloatList: (options?: {
37
37
  errorDefault?: number[] | undefined;
38
38
  }) => Caster<number[]>;
39
39
 
40
- declare class ConfigError extends Error {
41
- }
42
- type Schema = {
43
- [key: string]: Caster<unknown> | Schema;
40
+ type ConfigSchema = {
41
+ [key: string]: Caster<unknown> | ConfigSchema;
44
42
  };
45
43
  type InferConfig<S> = {
46
- [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends Schema ? InferConfig<S[K]> : never;
44
+ [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never;
47
45
  };
48
- declare function config<S extends Schema>(schema: S, options?: {
46
+ interface ConfigOptions {
47
+ /** Prepended to every environment variable name this call reads, joined with `sep`. */
49
48
  prefix?: string;
49
+ /** Joins the prefix and the nested key path into a variable name. Defaults to `"__"`. */
50
50
  sep?: string;
51
- }): InferConfig<S>;
51
+ }
52
+
53
+ /**
54
+ * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser
55
+ * half of `@isikk/core/next/config` can throw the same error type without importing
56
+ * anything that reads the environment. Keeping the split structural means the guarantee holds
57
+ * because of what the file contains, not because a bundler happened to tree-shake it away.
58
+ */
59
+ declare class ConfigError extends Error {
60
+ }
61
+
62
+ /**
63
+ * Builds a typed config object by reading and casting environment variables against a schema,
64
+ * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the
65
+ * caster for that key was given a `missingDefault`/`errorDefault`).
66
+ *
67
+ * Values read here are server-only: nothing in this module serializes them anywhere. Claims its
68
+ * prefix as a server namespace, so a `publicConfig()` call that would read the same variable
69
+ * names throws instead of quietly publishing them - see `@isikk/core/next/config`.
70
+ */
71
+ declare function config<S extends ConfigSchema>(schema: S, options?: ConfigOptions): InferConfig<S>;
52
72
 
53
73
  declare function contextLocal<T>(name: string): AsyncLocalStorage<T>;
54
74
 
55
75
  declare function getFileAsString(filename: string): Promise<string>;
56
76
 
57
- export { type Caster, ConfigError, boolean, caster, commaSeparatedFloatList, commaSeparatedIntList, commaSeparatedList, config, contextLocal, float, getFileAsString, integer, string };
77
+ export { type Caster, ConfigError, type ConfigOptions, type ConfigSchema, type InferConfig, boolean, caster, commaSeparatedFloatList, commaSeparatedIntList, commaSeparatedList, config, contextLocal, float, getFileAsString, integer, string };
@@ -41,9 +41,11 @@ var commaSeparatedList = caster((value) => value.split(","));
41
41
  var commaSeparatedIntList = caster((value) => value.split(",").map(parseStrictInteger));
42
42
  var commaSeparatedFloatList = caster((value) => value.split(",").map(parseStrictFloat));
43
43
 
44
- // src/node/config.ts
44
+ // src/node/configError.ts
45
45
  var ConfigError = class extends Error {
46
46
  };
47
+
48
+ // src/node/configCore.ts
47
49
  function isCaster(value) {
48
50
  return typeof value === "function";
49
51
  }
@@ -71,17 +73,67 @@ function readLeaf(leafCaster, key) {
71
73
  );
72
74
  }
73
75
  }
74
- function build(schema, path2, prefix, sep) {
76
+ function buildConfig(schema, prefix, sep, path2 = []) {
75
77
  const result = {};
76
78
  for (const [key, value] of Object.entries(schema)) {
77
79
  const keyPath = [...path2, key];
78
- result[key] = isCaster(value) ? readLeaf(value, environmentKey(prefix, keyPath, sep)) : build(value, keyPath, prefix, sep);
80
+ result[key] = isCaster(value) ? readLeaf(value, environmentKey(prefix, keyPath, sep)) : buildConfig(value, prefix, sep, keyPath);
79
81
  }
80
82
  return result;
81
83
  }
84
+
85
+ // src/node/configRegistry.ts
86
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@isikk/core/config-namespace-registry");
87
+ var CALL_NAME = {
88
+ server: "config()",
89
+ public: "publicConfig()"
90
+ };
91
+ function getRegistry() {
92
+ const host = globalThis;
93
+ const existing = host[REGISTRY_KEY];
94
+ if (existing) {
95
+ return existing;
96
+ }
97
+ const created = [];
98
+ host[REGISTRY_KEY] = created;
99
+ return created;
100
+ }
101
+ function namespacesOverlap(a, b) {
102
+ if (a.prefix === b.prefix) {
103
+ return true;
104
+ }
105
+ if (a.prefix === "" || b.prefix === "") {
106
+ return false;
107
+ }
108
+ return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`);
109
+ }
110
+ function describeNamespace(namespace) {
111
+ return namespace.prefix === "" ? "no prefix" : `prefix ${JSON.stringify(namespace.prefix)}`;
112
+ }
113
+ function claimConfigNamespace(claim) {
114
+ const registry2 = getRegistry();
115
+ for (const existing of registry2) {
116
+ if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {
117
+ return `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read the same environment variable namespace, so a key added to the public schema can resolve to a server-only value and be serialized into the browser. Give one of them a prefix the other does not use.`;
118
+ }
119
+ }
120
+ const alreadyClaimed = registry2.some(
121
+ (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep
122
+ );
123
+ if (!alreadyClaimed) {
124
+ registry2.push(claim);
125
+ }
126
+ return null;
127
+ }
128
+
129
+ // src/node/config.ts
82
130
  function config(schema, options = {}) {
83
131
  const { prefix, sep = "__" } = options;
84
- return build(schema, [], prefix, sep);
132
+ const conflict = claimConfigNamespace({ kind: "server", prefix: prefix ?? "", sep });
133
+ if (conflict) {
134
+ throw new ConfigError(conflict);
135
+ }
136
+ return buildConfig(schema, prefix, sep);
85
137
  }
86
138
 
87
139
  // src/node/contextLocal.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/node/casters.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","import type { Caster } from './casters'\n\nexport class ConfigError extends Error {}\n\ntype Schema = { [key: string]: Caster<unknown> | Schema }\n\ntype InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends Schema ? InferConfig<S[K]> : never\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\nfunction build<S extends Schema>(schema: S, path: string[], prefix: string | undefined, sep: string): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : build(value, keyPath, prefix, sep)\n }\n return result as InferConfig<S>\n}\n\nexport function config<S extends Schema>(schema: S, options: { prefix?: string; sep?: string } = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n return build(schema, [], prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path"]}
1
+ {"version":3,"sources":["../../src/node/casters.ts","../../src/node/configError.ts","../../src/node/configCore.ts","../../src/node/configRegistry.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { Caster } from './casters'\nimport { ConfigError } from './configError'\n\nexport type ConfigSchema = { [key: string]: Caster<unknown> | ConfigSchema }\n\nexport type InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never\n}\n\nexport interface ConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\n/**\n * Walks the schema and reads every leaf out of `process.env`. Shared by `config()` and by the\n * server half of `publicConfig()` so the two agree on variable naming and on the\n * `missingDefault`/`errorDefault` fallback rules by construction rather than by duplication.\n * Claims no namespace of its own - that is the caller's job, and the two callers claim different\n * kinds.\n */\nexport function buildConfig<S extends ConfigSchema>(\n schema: S,\n prefix: string | undefined,\n sep: string,\n path: string[] = []\n): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : buildConfig(value, prefix, sep, keyPath)\n }\n return result as InferConfig<S>\n}\n","/**\n * Process-wide record of which environment variable namespace each config call has claimed, so a\n * server-only `config()` and a browser-visible `publicConfig()` can be stopped from reading the\n * same one. That overlap is the mistake worth catching: with a shared namespace, a key pasted\n * into the public schema by accident resolves to the real server value and gets serialized into\n * the HTML of every page, silently. With disjoint namespaces it resolves to nothing and throws.\n *\n * Kept on `globalThis` under a `Symbol.for` key rather than in module scope because\n * `@isikk/core/node` and `@isikk/core/next/config` are separate tsup entries built\n * with `splitting: false` - a module-scoped registry would be duplicated into each bundle, giving\n * the two sides one registry each and so nothing to ever collide in.\n *\n * Claims are keyed by kind, so any number of `config()` calls (or any number of `publicConfig()`\n * calls) may share a namespace - two server reads of the same variable are harmless. Only a\n * server/public overlap is a conflict, which is also why re-registration is always safe: Next\n * evaluates the same module once per bundler layer (RSC, SSR, edge) and again on every Fast\n * Refresh, and every one of those repeats is the same kind claiming the same namespace.\n */\nexport type ConfigKind = 'server' | 'public'\n\nexport interface ConfigNamespace {\n kind: ConfigKind\n /** Empty string means \"no prefix\" - the root of the environment. */\n prefix: string\n sep: string\n}\n\nconst REGISTRY_KEY = Symbol.for('@isikk/core/config-namespace-registry')\n\nconst CALL_NAME: Record<ConfigKind, string> = {\n server: 'config()',\n public: 'publicConfig()',\n}\n\nfunction getRegistry(): ConfigNamespace[] {\n const host = globalThis as unknown as Record<symbol, ConfigNamespace[] | undefined>\n const existing = host[REGISTRY_KEY]\n if (existing) {\n return existing\n }\n const created: ConfigNamespace[] = []\n host[REGISTRY_KEY] = created\n return created\n}\n\n/**\n * Two namespaces overlap when one can produce an environment variable name the other can also\n * produce. Identical prefixes always overlap. A prefix nested under another at a separator\n * boundary overlaps too (`APP` and `APP__PUBLIC` both reach `APP__PUBLIC__TOKEN`).\n *\n * An absent prefix is deliberately treated as disjoint from every non-empty one rather than as\n * the root that technically contains them all: unprefixed server config alongside prefixed public\n * config is the most natural setup there is, and the only way it actually collides is a server\n * schema with a top-level key named exactly like the public prefix. Rejecting the whole shape to\n * catch that would cost far more than it buys - docs/next/config.md says so out loud.\n */\nfunction namespacesOverlap(a: ConfigNamespace, b: ConfigNamespace): boolean {\n if (a.prefix === b.prefix) {\n return true\n }\n if (a.prefix === '' || b.prefix === '') {\n return false\n }\n return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`)\n}\n\nfunction describeNamespace(namespace: ConfigNamespace): string {\n return namespace.prefix === '' ? 'no prefix' : `prefix ${JSON.stringify(namespace.prefix)}`\n}\n\n/**\n * Records `claim`, returning `null` when it is allowed or an explanatory message when it overlaps\n * a namespace already claimed by the other kind. Returns the message instead of throwing so each\n * entry point can throw its own bundled copy of `ConfigError`, keeping `instanceof` working\n * against the class imported from the same entry point the call came from.\n */\nexport function claimConfigNamespace(claim: ConfigNamespace): string | null {\n const registry = getRegistry()\n\n for (const existing of registry) {\n if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {\n return (\n `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ` +\n `${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read ` +\n 'the same environment variable namespace, so a key added to the public schema can resolve to ' +\n 'a server-only value and be serialized into the browser. Give one of them a prefix the other ' +\n 'does not use.'\n )\n }\n }\n\n const alreadyClaimed = registry.some(\n (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep\n )\n if (!alreadyClaimed) {\n registry.push(claim)\n }\n\n return null\n}\n\n/** Test-only reset. Deliberately not re-exported from any of the package's public entry points. */\nexport function resetConfigNamespaces(): void {\n getRegistry().length = 0\n}\n","import { type ConfigOptions, type ConfigSchema, type InferConfig, buildConfig } from './configCore'\nimport { ConfigError } from './configError'\nimport { claimConfigNamespace } from './configRegistry'\n\nexport { ConfigError } from './configError'\nexport type { ConfigOptions, ConfigSchema, InferConfig } from './configCore'\n\n/**\n * Builds a typed config object by reading and casting environment variables against a schema,\n * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the\n * caster for that key was given a `missingDefault`/`errorDefault`).\n *\n * Values read here are server-only: nothing in this module serializes them anywhere. Claims its\n * prefix as a server namespace, so a `publicConfig()` call that would read the same variable\n * names throws instead of quietly publishing them - see `@isikk/core/next/config`.\n */\nexport function config<S extends ConfigSchema>(schema: S, options: ConfigOptions = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n\n const conflict = claimConfigNamespace({ kind: 'server', prefix: prefix ?? '', sep })\n if (conflict) {\n throw new ConfigError(conflict)\n }\n\n return buildConfig(schema, prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path","registry"]}
@@ -0,0 +1,31 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ type Caster<T> = ((value: string) => T) & {
4
+ missingDefault?: T;
5
+ errorDefault?: T;
6
+ };
7
+
8
+ type ConfigSchema = {
9
+ [key: string]: Caster<unknown> | ConfigSchema;
10
+ };
11
+ type InferConfig<S> = {
12
+ [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never;
13
+ };
14
+
15
+ interface PublicConfigOptions {
16
+ /** Prepended to every environment variable name this call reads, joined with `sep`. */
17
+ prefix?: string;
18
+ /** Joins the prefix and the nested key path into a variable name. Defaults to `"__"`. */
19
+ sep?: string;
20
+ }
21
+ interface PublicConfigScriptProps {
22
+ /** Forwarded to the injected `<script>` so a CSP with a per-request nonce keeps working. */
23
+ nonce?: string;
24
+ }
25
+ type PublicConfigScriptComponent = (props: PublicConfigScriptProps) => ReactNode | Promise<ReactNode>;
26
+ interface PublicConfig<S extends ConfigSchema> {
27
+ CONFIG: InferConfig<S>;
28
+ PublicConfigScript: PublicConfigScriptComponent;
29
+ }
30
+
31
+ export type { ConfigSchema as C, InferConfig as I, PublicConfigOptions as P, PublicConfig as a, PublicConfigScriptComponent as b, PublicConfigScriptProps as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isikk/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Everyday TypeScript utilities.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -61,10 +61,18 @@
61
61
  "./next/cookies": {
62
62
  "types": "./dist/next/cookies/index.d.ts",
63
63
  "import": "./dist/next/cookies/index.js"
64
+ },
65
+ "./next/config": {
66
+ "types": "./dist/next/config/index.d.ts",
67
+ "edge-light": "./dist/next/config/index.js",
68
+ "worker": "./dist/next/config/index.js",
69
+ "node": "./dist/next/config/index.js",
70
+ "browser": "./dist/next/config/browser.js",
71
+ "default": "./dist/next/config/index.js"
64
72
  }
65
73
  },
66
74
  "scripts": {
67
- "build": "tsup --config tsup.config.ts && tsup --config tsup.next-cookies.config.ts",
75
+ "build": "tsup --config tsup.config.ts && tsup --config tsup.next-cookies.config.ts && tsup --config tsup.next-config.config.ts",
68
76
  "dev": "tsup --config tsup.config.ts --watch",
69
77
  "test": "vitest run",
70
78
  "test:coverage": "vitest run --coverage",