@pyreon/zero 0.12.2 → 0.12.3

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 (138) hide show
  1. package/lib/actions.js +97 -0
  2. package/lib/actions.js.map +1 -0
  3. package/lib/ai.js +503 -0
  4. package/lib/ai.js.map +1 -0
  5. package/lib/api-routes.js +137 -0
  6. package/lib/api-routes.js.map +1 -0
  7. package/lib/compression.js +80 -0
  8. package/lib/compression.js.map +1 -0
  9. package/lib/cors.js +57 -0
  10. package/lib/cors.js.map +1 -0
  11. package/lib/csp.js +119 -0
  12. package/lib/csp.js.map +1 -0
  13. package/lib/env.js +217 -0
  14. package/lib/env.js.map +1 -0
  15. package/lib/favicon.js +424 -0
  16. package/lib/favicon.js.map +1 -0
  17. package/lib/i18n-routing.js +167 -0
  18. package/lib/i18n-routing.js.map +1 -0
  19. package/lib/index.js +80 -22
  20. package/lib/index.js.map +1 -1
  21. package/lib/link.js +5 -0
  22. package/lib/link.js.map +1 -1
  23. package/lib/logger.js +78 -0
  24. package/lib/logger.js.map +1 -0
  25. package/lib/meta.js +336 -0
  26. package/lib/meta.js.map +1 -0
  27. package/lib/middleware.js +53 -0
  28. package/lib/middleware.js.map +1 -0
  29. package/lib/og-image.js +233 -0
  30. package/lib/og-image.js.map +1 -0
  31. package/lib/rate-limit.js +76 -0
  32. package/lib/rate-limit.js.map +1 -0
  33. package/lib/testing.js +179 -0
  34. package/lib/testing.js.map +1 -0
  35. package/lib/theme.js +11 -2
  36. package/lib/theme.js.map +1 -1
  37. package/lib/types/actions.d.ts +27 -24
  38. package/lib/types/actions.d.ts.map +1 -1
  39. package/lib/types/ai.d.ts +76 -95
  40. package/lib/types/ai.d.ts.map +1 -1
  41. package/lib/types/api-routes.d.ts +37 -33
  42. package/lib/types/api-routes.d.ts.map +1 -1
  43. package/lib/types/cache.d.ts +26 -22
  44. package/lib/types/cache.d.ts.map +1 -1
  45. package/lib/types/client.d.ts +13 -9
  46. package/lib/types/client.d.ts.map +1 -1
  47. package/lib/types/compression.d.ts +14 -10
  48. package/lib/types/compression.d.ts.map +1 -1
  49. package/lib/types/config.d.ts +39 -4
  50. package/lib/types/config.d.ts.map +1 -1
  51. package/lib/types/cors.d.ts +20 -16
  52. package/lib/types/cors.d.ts.map +1 -1
  53. package/lib/types/csp.d.ts +42 -61
  54. package/lib/types/csp.d.ts.map +1 -1
  55. package/lib/types/env.d.ts +26 -26
  56. package/lib/types/env.d.ts.map +1 -1
  57. package/lib/types/favicon.d.ts +58 -54
  58. package/lib/types/favicon.d.ts.map +1 -1
  59. package/lib/types/font.d.ts +68 -65
  60. package/lib/types/font.d.ts.map +1 -1
  61. package/lib/types/i18n-routing.d.ts +43 -37
  62. package/lib/types/i18n-routing.d.ts.map +1 -1
  63. package/lib/types/image-plugin.d.ts +49 -45
  64. package/lib/types/image-plugin.d.ts.map +1 -1
  65. package/lib/types/image.d.ts +47 -36
  66. package/lib/types/image.d.ts.map +1 -1
  67. package/lib/types/index.d.ts +1961 -56
  68. package/lib/types/index.d.ts.map +1 -1
  69. package/lib/types/link.d.ts +61 -56
  70. package/lib/types/link.d.ts.map +1 -1
  71. package/lib/types/logger.d.ts +37 -48
  72. package/lib/types/logger.d.ts.map +1 -1
  73. package/lib/types/meta.d.ts +180 -105
  74. package/lib/types/meta.d.ts.map +1 -1
  75. package/lib/types/middleware.d.ts +8 -4
  76. package/lib/types/middleware.d.ts.map +1 -1
  77. package/lib/types/og-image.d.ts +63 -59
  78. package/lib/types/og-image.d.ts.map +1 -1
  79. package/lib/types/rate-limit.d.ts +20 -16
  80. package/lib/types/rate-limit.d.ts.map +1 -1
  81. package/lib/types/script.d.ts +23 -19
  82. package/lib/types/script.d.ts.map +1 -1
  83. package/lib/types/seo.d.ts +47 -43
  84. package/lib/types/seo.d.ts.map +1 -1
  85. package/lib/types/testing.d.ts +64 -27
  86. package/lib/types/testing.d.ts.map +1 -1
  87. package/lib/types/theme.d.ts +22 -12
  88. package/lib/types/theme.d.ts.map +1 -1
  89. package/package.json +12 -12
  90. package/src/actions.ts +1 -3
  91. package/src/adapters/bun.ts +2 -0
  92. package/src/adapters/cloudflare.ts +2 -0
  93. package/src/adapters/netlify.ts +2 -0
  94. package/src/adapters/node.ts +2 -0
  95. package/src/adapters/validate.ts +16 -0
  96. package/src/adapters/vercel.ts +2 -0
  97. package/src/compression.ts +19 -3
  98. package/src/entry-server.ts +28 -5
  99. package/src/index.ts +1 -0
  100. package/src/link.tsx +6 -0
  101. package/src/meta.tsx +41 -13
  102. package/src/rate-limit.ts +11 -9
  103. package/src/theme.tsx +12 -1
  104. package/src/vite-plugin.ts +5 -1
  105. package/lib/types/adapters/bun.d.ts +0 -6
  106. package/lib/types/adapters/bun.d.ts.map +0 -1
  107. package/lib/types/adapters/cloudflare.d.ts +0 -26
  108. package/lib/types/adapters/cloudflare.d.ts.map +0 -1
  109. package/lib/types/adapters/index.d.ts +0 -13
  110. package/lib/types/adapters/index.d.ts.map +0 -1
  111. package/lib/types/adapters/netlify.d.ts +0 -21
  112. package/lib/types/adapters/netlify.d.ts.map +0 -1
  113. package/lib/types/adapters/node.d.ts +0 -6
  114. package/lib/types/adapters/node.d.ts.map +0 -1
  115. package/lib/types/adapters/static.d.ts +0 -7
  116. package/lib/types/adapters/static.d.ts.map +0 -1
  117. package/lib/types/adapters/vercel.d.ts +0 -21
  118. package/lib/types/adapters/vercel.d.ts.map +0 -1
  119. package/lib/types/app.d.ts +0 -24
  120. package/lib/types/app.d.ts.map +0 -1
  121. package/lib/types/entry-server.d.ts +0 -37
  122. package/lib/types/entry-server.d.ts.map +0 -1
  123. package/lib/types/error-overlay.d.ts +0 -6
  124. package/lib/types/error-overlay.d.ts.map +0 -1
  125. package/lib/types/fs-router.d.ts +0 -47
  126. package/lib/types/fs-router.d.ts.map +0 -1
  127. package/lib/types/isr.d.ts +0 -9
  128. package/lib/types/isr.d.ts.map +0 -1
  129. package/lib/types/not-found.d.ts +0 -7
  130. package/lib/types/not-found.d.ts.map +0 -1
  131. package/lib/types/types.d.ts +0 -111
  132. package/lib/types/types.d.ts.map +0 -1
  133. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  134. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  135. package/lib/types/utils/with-headers.d.ts +0 -6
  136. package/lib/types/utils/with-headers.d.ts.map +0 -1
  137. package/lib/types/vite-plugin.d.ts +0 -17
  138. package/lib/types/vite-plugin.d.ts.map +0 -1
package/lib/env.js ADDED
@@ -0,0 +1,217 @@
1
+ //#region src/env.ts
2
+ /**
3
+ * String validator — accepts any non-empty string.
4
+ */
5
+ function str(options) {
6
+ return {
7
+ __type: "env-validator",
8
+ required: options?.default === void 0 && options?.required !== false,
9
+ defaultValue: options?.default,
10
+ parse(raw, key) {
11
+ if (raw === void 0 || raw === "") {
12
+ if (options?.default !== void 0) return options.default;
13
+ throw new EnvError(key, "is required but not set", options?.description);
14
+ }
15
+ return raw;
16
+ }
17
+ };
18
+ }
19
+ /**
20
+ * Number validator — parses to a number, rejects NaN.
21
+ */
22
+ function num(options) {
23
+ return {
24
+ __type: "env-validator",
25
+ required: options?.default === void 0 && options?.required !== false,
26
+ defaultValue: options?.default,
27
+ parse(raw, key) {
28
+ if (raw === void 0 || raw === "") {
29
+ if (options?.default !== void 0) return options.default;
30
+ throw new EnvError(key, "is required but not set", options?.description);
31
+ }
32
+ const n = Number(raw);
33
+ if (Number.isNaN(n)) throw new EnvError(key, `must be a number, got "${raw}"`, options?.description);
34
+ return n;
35
+ }
36
+ };
37
+ }
38
+ /**
39
+ * Boolean validator — accepts "true"/"1" as true, "false"/"0" as false.
40
+ */
41
+ function bool(options) {
42
+ return {
43
+ __type: "env-validator",
44
+ required: options?.default === void 0 && options?.required !== false,
45
+ defaultValue: options?.default,
46
+ parse(raw, key) {
47
+ if (raw === void 0 || raw === "") {
48
+ if (options?.default !== void 0) return options.default;
49
+ throw new EnvError(key, "is required but not set", options?.description);
50
+ }
51
+ const lower = raw.toLowerCase();
52
+ if (lower === "true" || lower === "1") return true;
53
+ if (lower === "false" || lower === "0") return false;
54
+ throw new EnvError(key, `must be "true" or "false", got "${raw}"`, options?.description);
55
+ }
56
+ };
57
+ }
58
+ /**
59
+ * URL validator — validates that the value is a valid URL.
60
+ */
61
+ function url(options) {
62
+ return {
63
+ __type: "env-validator",
64
+ required: options?.default === void 0 && options?.required !== false,
65
+ defaultValue: options?.default,
66
+ parse(raw, key) {
67
+ if (raw === void 0 || raw === "") {
68
+ if (options?.default !== void 0) return options.default;
69
+ throw new EnvError(key, "is required but not set", options?.description);
70
+ }
71
+ try {
72
+ new URL(raw);
73
+ return raw;
74
+ } catch {
75
+ throw new EnvError(key, `must be a valid URL, got "${raw}"`, options?.description);
76
+ }
77
+ }
78
+ };
79
+ }
80
+ /**
81
+ * Enum validator — value must be one of the allowed values.
82
+ */
83
+ function oneOf(values, options) {
84
+ return {
85
+ __type: "env-validator",
86
+ required: options?.default === void 0 && options?.required !== false,
87
+ defaultValue: options?.default,
88
+ parse(raw, key) {
89
+ if (raw === void 0 || raw === "") {
90
+ if (options?.default !== void 0) return options.default;
91
+ throw new EnvError(key, "is required but not set", options?.description);
92
+ }
93
+ if (!values.includes(raw)) throw new EnvError(key, `must be one of [${values.join(", ")}], got "${raw}"`, options?.description);
94
+ return raw;
95
+ }
96
+ };
97
+ }
98
+ var EnvError = class extends Error {
99
+ constructor(key, message, description) {
100
+ const desc = description ? ` (${description})` : "";
101
+ super(`[zero:env] ${key}${desc}: ${message}`);
102
+ this.name = "EnvError";
103
+ }
104
+ };
105
+ function isEnvValidator(v) {
106
+ return typeof v === "object" && v !== null && v.__type === "env-validator";
107
+ }
108
+ /**
109
+ * Convert a plain schema value to an EnvValidator.
110
+ *
111
+ * - `3000` → num({ default: 3000 })
112
+ * - `false` → bool({ default: false })
113
+ * - `"localhost"` → str({ default: "localhost" })
114
+ * - `String` → str() (required)
115
+ * - `Number` → num() (required)
116
+ * - `Boolean` → bool() (required)
117
+ * - EnvValidator → pass through
118
+ */
119
+ function toValidator(value) {
120
+ if (isEnvValidator(value)) return value;
121
+ if (value === String) return str();
122
+ if (value === Number) return num();
123
+ if (value === Boolean) return bool();
124
+ if (typeof value === "number") return num({ default: value });
125
+ if (typeof value === "boolean") return bool({ default: value });
126
+ if (typeof value === "string") return str({ default: value });
127
+ throw new Error(`[zero:env] Invalid schema value: ${String(value)}. Use a default value, String/Number/Boolean, or a validator like url().`);
128
+ }
129
+ /**
130
+ * Validate environment variables.
131
+ *
132
+ * Schema values can be:
133
+ * - **Default values**: `3000`, `false`, `"localhost"` → type inferred, used as default
134
+ * - **Constructors**: `String`, `Number`, `Boolean` → required, no default
135
+ * - **Validators**: `url()`, `oneOf([...])`, `str()`, `num()`, `bool()` → explicit validation
136
+ * - **Custom**: `schema(raw => z.coerce.number().parse(raw))` — bridge to any schema library
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * import { validateEnv, url, oneOf } from "@pyreon/zero/env"
141
+ *
142
+ * const env = validateEnv({
143
+ * PORT: 3000, // optional, default 3000
144
+ * DATABASE_URL: url(), // required, validated URL
145
+ * NODE_ENV: oneOf(["dev", "prod", "test"]), // required, must be one of
146
+ * API_KEY: String, // required string
147
+ * DEBUG: false, // optional, default false
148
+ * })
149
+ * ```
150
+ */
151
+ function validateEnv(schema, source) {
152
+ const env = source ?? (typeof process !== "undefined" ? process.env : {});
153
+ const result = {};
154
+ const errors = [];
155
+ for (const [key, entry] of Object.entries(schema)) {
156
+ const validator = toValidator(entry);
157
+ try {
158
+ result[key] = validator.parse(env[key], key);
159
+ } catch (e) {
160
+ errors.push(e.message);
161
+ }
162
+ }
163
+ if (errors.length > 0) {
164
+ const header = `\n[zero:env] Environment validation failed (${errors.length} error${errors.length > 1 ? "s" : ""}):\n`;
165
+ const body = errors.map((e) => ` ✗ ${e.replace("[zero:env] ", "")}`).join("\n");
166
+ throw new Error(header + body + "\n");
167
+ }
168
+ return result;
169
+ }
170
+ function publicEnv(schema) {
171
+ const prefix = "ZERO_PUBLIC_";
172
+ const env = typeof process !== "undefined" ? process.env : {};
173
+ if (!schema) {
174
+ const result = {};
175
+ for (const [key, value] of Object.entries(env)) if (key.startsWith(prefix) && value !== void 0) result[key.slice(12)] = value;
176
+ return result;
177
+ }
178
+ const prefixedSource = {};
179
+ for (const key of Object.keys(schema)) prefixedSource[key] = env[`${prefix}${key}`];
180
+ return validateEnv(schema, prefixedSource);
181
+ }
182
+ /**
183
+ * Create an env validator from a custom parse function.
184
+ * Use this to integrate any schema library (Zod, Valibot, ArkType, etc.).
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * import { z } from "zod"
189
+ * import { validateEnv, schema } from "@pyreon/zero/env"
190
+ *
191
+ * const env = validateEnv({
192
+ * PORT: schema(raw => z.coerce.number().parse(raw)),
193
+ * DATABASE_URL: schema(raw => z.string().url().parse(raw)),
194
+ * HOST: "localhost", // plain defaults still work alongside
195
+ * })
196
+ * ```
197
+ */
198
+ function schema(parse) {
199
+ return {
200
+ __type: "env-validator",
201
+ required: true,
202
+ defaultValue: void 0,
203
+ parse(raw, key) {
204
+ if (raw === void 0 || raw === "") throw new Error(`[zero:env] ${key}: is required but not set`);
205
+ try {
206
+ return parse(raw);
207
+ } catch (e) {
208
+ const msg = e instanceof Error ? e.message : String(e);
209
+ throw new Error(`[zero:env] ${key}: ${msg}`);
210
+ }
211
+ }
212
+ };
213
+ }
214
+
215
+ //#endregion
216
+ export { bool, num, oneOf, publicEnv, schema, str, url, validateEnv };
217
+ //# sourceMappingURL=env.js.map
package/lib/env.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","names":[],"sources":["../src/env.ts"],"sourcesContent":["/**\n * Environment variable validation.\n *\n * Infers types from default values — no verbose validator imports needed.\n * Explicit validators (`url()`, `oneOf()`) available for special cases.\n *\n * @example\n * ```ts\n * import { validateEnv, url, oneOf } from \"@pyreon/zero/env\"\n *\n * const env = validateEnv({\n * PORT: 3000, // number, default 3000\n * DEBUG: false, // boolean, default false\n * HOST: \"localhost\", // string, default \"localhost\"\n * DATABASE_URL: url(), // validated URL, required\n * NODE_ENV: oneOf([\"development\", \"production\", \"test\"]),\n * API_KEY: String, // required string, no default\n * MAX_RETRIES: Number, // required number, no default\n * })\n * ```\n */\n\nexport interface EnvValidatorOptions<T = string> {\n /** Whether this variable is required. Default: true */\n required?: boolean\n /** Default value when not set. Makes the variable optional. */\n default?: T\n /** Human-readable description for error messages. */\n description?: string\n}\n\nexport interface EnvValidator<T> {\n __type: 'env-validator'\n parse: (raw: string | undefined, key: string) => T\n required: boolean\n defaultValue?: T | undefined\n}\n\n// ─── Explicit validators (for special cases) ────────────────────────────────\n\n/**\n * String validator — accepts any non-empty string.\n */\nexport function str(options?: EnvValidatorOptions<string>): EnvValidator<string> {\n const required = options?.default === undefined && options?.required !== false\n return {\n __type: 'env-validator',\n required,\n defaultValue: options?.default,\n parse(raw, key) {\n if (raw === undefined || raw === '') {\n if (options?.default !== undefined) return options.default\n throw new EnvError(key, 'is required but not set', options?.description)\n }\n return raw\n },\n }\n}\n\n/**\n * Number validator — parses to a number, rejects NaN.\n */\nexport function num(options?: EnvValidatorOptions<number>): EnvValidator<number> {\n const required = options?.default === undefined && options?.required !== false\n return {\n __type: 'env-validator',\n required,\n defaultValue: options?.default,\n parse(raw, key) {\n if (raw === undefined || raw === '') {\n if (options?.default !== undefined) return options.default\n throw new EnvError(key, 'is required but not set', options?.description)\n }\n const n = Number(raw)\n if (Number.isNaN(n)) {\n throw new EnvError(key, `must be a number, got \"${raw}\"`, options?.description)\n }\n return n\n },\n }\n}\n\n/**\n * Boolean validator — accepts \"true\"/\"1\" as true, \"false\"/\"0\" as false.\n */\nexport function bool(options?: EnvValidatorOptions<boolean>): EnvValidator<boolean> {\n const required = options?.default === undefined && options?.required !== false\n return {\n __type: 'env-validator',\n required,\n defaultValue: options?.default,\n parse(raw, key) {\n if (raw === undefined || raw === '') {\n if (options?.default !== undefined) return options.default\n throw new EnvError(key, 'is required but not set', options?.description)\n }\n const lower = raw.toLowerCase()\n if (lower === 'true' || lower === '1') return true\n if (lower === 'false' || lower === '0') return false\n throw new EnvError(key, `must be \"true\" or \"false\", got \"${raw}\"`, options?.description)\n },\n }\n}\n\n/**\n * URL validator — validates that the value is a valid URL.\n */\nexport function url(options?: EnvValidatorOptions<string>): EnvValidator<string> {\n const required = options?.default === undefined && options?.required !== false\n return {\n __type: 'env-validator',\n required,\n defaultValue: options?.default,\n parse(raw, key) {\n if (raw === undefined || raw === '') {\n if (options?.default !== undefined) return options.default\n throw new EnvError(key, 'is required but not set', options?.description)\n }\n try {\n new URL(raw)\n return raw\n } catch {\n throw new EnvError(key, `must be a valid URL, got \"${raw}\"`, options?.description)\n }\n },\n }\n}\n\n/**\n * Enum validator — value must be one of the allowed values.\n */\nexport function oneOf<T extends string>(\n values: readonly T[],\n options?: EnvValidatorOptions<T>,\n): EnvValidator<T> {\n const required = options?.default === undefined && options?.required !== false\n return {\n __type: 'env-validator',\n required,\n defaultValue: options?.default,\n parse(raw, key) {\n if (raw === undefined || raw === '') {\n if (options?.default !== undefined) return options.default\n throw new EnvError(key, 'is required but not set', options?.description)\n }\n if (!values.includes(raw as T)) {\n throw new EnvError(\n key,\n `must be one of [${values.join(', ')}], got \"${raw}\"`,\n options?.description,\n )\n }\n return raw as T\n },\n }\n}\n\n// ─── Internal helpers ───────────────────────────────────────────────────────\n\nclass EnvError extends Error {\n constructor(key: string, message: string, description?: string) {\n const desc = description ? ` (${description})` : ''\n super(`[zero:env] ${key}${desc}: ${message}`)\n this.name = 'EnvError'\n }\n}\n\nfunction isEnvValidator(v: unknown): v is EnvValidator<unknown> {\n return typeof v === 'object' && v !== null && (v as any).__type === 'env-validator'\n}\n\n/**\n * Convert a plain schema value to an EnvValidator.\n *\n * - `3000` → num({ default: 3000 })\n * - `false` → bool({ default: false })\n * - `\"localhost\"` → str({ default: \"localhost\" })\n * - `String` → str() (required)\n * - `Number` → num() (required)\n * - `Boolean` → bool() (required)\n * - EnvValidator → pass through\n */\nfunction toValidator(value: unknown): EnvValidator<unknown> {\n if (isEnvValidator(value)) return value\n\n // Constructor markers → required, no default\n if (value === String) return str()\n if (value === Number) return num()\n if (value === Boolean) return bool()\n\n // Plain values → infer type + use as default\n if (typeof value === 'number') return num({ default: value })\n if (typeof value === 'boolean') return bool({ default: value })\n if (typeof value === 'string') return str({ default: value })\n\n throw new Error(`[zero:env] Invalid schema value: ${String(value)}. Use a default value, String/Number/Boolean, or a validator like url().`)\n}\n\n// ─── Type inference ─────────────────────────────────────────────────────────\n\n/** Schema entry: plain value, constructor, or explicit validator. */\ntype SchemaEntry =\n | string | number | boolean\n | StringConstructor | NumberConstructor | BooleanConstructor\n | EnvValidator<any>\n\n/** Infer the output type from a schema entry. */\ntype InferEntry<T> =\n T extends EnvValidator<infer V> ? V :\n T extends StringConstructor ? string :\n T extends NumberConstructor ? number :\n T extends BooleanConstructor ? boolean :\n T extends string ? string :\n T extends number ? number :\n T extends boolean ? boolean :\n never\n\ntype InferEnvSchema<T> = {\n [K in keyof T]: InferEntry<T[K]>\n}\n\n// ─── Main API ───────────────────────────────────────────────────────────────\n\n/**\n * Validate environment variables.\n *\n * Schema values can be:\n * - **Default values**: `3000`, `false`, `\"localhost\"` → type inferred, used as default\n * - **Constructors**: `String`, `Number`, `Boolean` → required, no default\n * - **Validators**: `url()`, `oneOf([...])`, `str()`, `num()`, `bool()` → explicit validation\n * - **Custom**: `schema(raw => z.coerce.number().parse(raw))` — bridge to any schema library\n *\n * @example\n * ```ts\n * import { validateEnv, url, oneOf } from \"@pyreon/zero/env\"\n *\n * const env = validateEnv({\n * PORT: 3000, // optional, default 3000\n * DATABASE_URL: url(), // required, validated URL\n * NODE_ENV: oneOf([\"dev\", \"prod\", \"test\"]), // required, must be one of\n * API_KEY: String, // required string\n * DEBUG: false, // optional, default false\n * })\n * ```\n */\nexport function validateEnv<T extends Record<string, SchemaEntry>>(\n schema: T,\n source?: Record<string, string | undefined>,\n): InferEnvSchema<T> {\n const env = source ?? (typeof process !== 'undefined' ? process.env : {})\n const result: Record<string, unknown> = {}\n const errors: string[] = []\n\n for (const [key, entry] of Object.entries(schema)) {\n const validator = toValidator(entry)\n try {\n result[key] = validator.parse(env[key], key)\n } catch (e) {\n errors.push((e as Error).message)\n }\n }\n\n if (errors.length > 0) {\n const header = `\\n[zero:env] Environment validation failed (${errors.length} error${errors.length > 1 ? 's' : ''}):\\n`\n const body = errors.map((e) => ` ✗ ${e.replace('[zero:env] ', '')}`).join('\\n')\n throw new Error(header + body + '\\n')\n }\n\n return result as InferEnvSchema<T>\n}\n\n// ─── Public env (client-safe) ────────────────────────────────────────────────\n\n/**\n * Extract public environment variables (prefixed with `ZERO_PUBLIC_`).\n *\n * @example\n * ```ts\n * const pub = publicEnv()\n * // → { API_URL: \"https://...\", APP_NAME: \"MyApp\" }\n *\n * const pub = publicEnv({ API_URL: url(), APP_NAME: \"Default\" })\n * // → validated against ZERO_PUBLIC_API_URL, ZERO_PUBLIC_APP_NAME\n * ```\n */\nexport function publicEnv(): Record<string, string>\nexport function publicEnv<T extends Record<string, SchemaEntry>>(schema: T): InferEnvSchema<T>\nexport function publicEnv(schema?: Record<string, SchemaEntry>): Record<string, unknown> {\n const prefix = 'ZERO_PUBLIC_'\n const env = typeof process !== 'undefined' ? process.env : {}\n\n if (!schema) {\n const result: Record<string, string> = {}\n for (const [key, value] of Object.entries(env)) {\n if (key.startsWith(prefix) && value !== undefined) {\n result[key.slice(prefix.length)] = value\n }\n }\n return result\n }\n\n const prefixedSource: Record<string, string | undefined> = {}\n for (const key of Object.keys(schema)) {\n prefixedSource[key] = env[`${prefix}${key}`]\n }\n return validateEnv(schema, prefixedSource)\n}\n\n// ─── Custom validator escape hatch ──────────────────────────────────────────\n\n/**\n * Create an env validator from a custom parse function.\n * Use this to integrate any schema library (Zod, Valibot, ArkType, etc.).\n *\n * @example\n * ```ts\n * import { z } from \"zod\"\n * import { validateEnv, schema } from \"@pyreon/zero/env\"\n *\n * const env = validateEnv({\n * PORT: schema(raw => z.coerce.number().parse(raw)),\n * DATABASE_URL: schema(raw => z.string().url().parse(raw)),\n * HOST: \"localhost\", // plain defaults still work alongside\n * })\n * ```\n */\nexport function schema<T>(parse: (raw: string) => T): EnvValidator<T> {\n return {\n __type: 'env-validator',\n required: true,\n defaultValue: undefined,\n parse(raw: string | undefined, key: string) {\n if (raw === undefined || raw === '') {\n throw new Error(`[zero:env] ${key}: is required but not set`)\n }\n try {\n return parse(raw)\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e)\n throw new Error(`[zero:env] ${key}: ${msg}`)\n }\n },\n }\n}\n"],"mappings":"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"}
package/lib/favicon.js ADDED
@@ -0,0 +1,424 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ //#region src/favicon.ts
6
+ let sharpWarned = false;
7
+ function warnSharpMissing() {
8
+ if (sharpWarned) return;
9
+ sharpWarned = true;
10
+ console.warn("\n[zero:favicon] sharp not installed — favicons will not be generated. Install for full support: bun add -D sharp\n");
11
+ }
12
+ const SIZES = [
13
+ {
14
+ size: 16,
15
+ name: "favicon-16x16.png"
16
+ },
17
+ {
18
+ size: 32,
19
+ name: "favicon-32x32.png"
20
+ },
21
+ {
22
+ size: 180,
23
+ name: "apple-touch-icon.png"
24
+ },
25
+ {
26
+ size: 192,
27
+ name: "icon-192.png"
28
+ },
29
+ {
30
+ size: 512,
31
+ name: "icon-512.png"
32
+ }
33
+ ];
34
+ /**
35
+ * Favicon generation Vite plugin.
36
+ *
37
+ * Generates all required favicon formats at build time from a single source.
38
+ * In dev mode, serves the source directly.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * // vite.config.ts
43
+ * import { faviconPlugin } from "@pyreon/zero"
44
+ *
45
+ * export default {
46
+ * plugins: [faviconPlugin({ source: "./src/assets/icon.svg" })],
47
+ * }
48
+ * ```
49
+ */
50
+ function faviconPlugin(config) {
51
+ const themeColor = config.themeColor ?? "#ffffff";
52
+ const backgroundColor = config.backgroundColor ?? "#ffffff";
53
+ const generateManifest = config.manifest !== false;
54
+ let root = "";
55
+ let isBuild = false;
56
+ return {
57
+ name: "pyreon-zero-favicon",
58
+ enforce: "pre",
59
+ configResolved(resolvedConfig) {
60
+ root = resolvedConfig.root;
61
+ isBuild = resolvedConfig.command === "build";
62
+ },
63
+ configureServer(server) {
64
+ const sourcePath = join(root, config.source);
65
+ const devCache = /* @__PURE__ */ new Map();
66
+ server.middlewares.use(async (req, res, next) => {
67
+ const url = req.url ?? "";
68
+ const localeSource = resolveLocaleSource(url, config, root);
69
+ const svgUrl = localeSource ? localeSource.url : url;
70
+ const svgPath = localeSource ? localeSource.sourcePath : sourcePath;
71
+ const isSvgSource = localeSource ? localeSource.source.endsWith(".svg") : config.source.endsWith(".svg");
72
+ if (svgUrl.endsWith("/favicon.svg") && isSvgSource) try {
73
+ const content = await readFile(svgPath, "utf-8");
74
+ res.setHeader("Content-Type", "image/svg+xml");
75
+ res.end(content);
76
+ return;
77
+ } catch {}
78
+ const baseName = svgUrl.split("/").pop() ?? "";
79
+ const sizeMatch = SIZES.find((s) => s.name === baseName);
80
+ if (sizeMatch) {
81
+ const cacheKey = `${svgPath}:${sizeMatch.size}`;
82
+ let png = devCache.get(cacheKey);
83
+ if (!png) {
84
+ const result = await resizeToPng(svgPath, sizeMatch.size);
85
+ if (result) {
86
+ png = result;
87
+ devCache.set(cacheKey, result);
88
+ }
89
+ }
90
+ if (png) {
91
+ res.setHeader("Content-Type", "image/png");
92
+ res.setHeader("Cache-Control", "no-cache");
93
+ res.end(Buffer.from(png));
94
+ return;
95
+ }
96
+ }
97
+ if (baseName === "favicon.ico") {
98
+ const cacheKey = `ico:${svgPath}`;
99
+ let ico = devCache.get(cacheKey);
100
+ if (!ico) {
101
+ const result = await generateIco(svgPath);
102
+ if (result) {
103
+ ico = result;
104
+ devCache.set(cacheKey, result);
105
+ }
106
+ }
107
+ if (ico) {
108
+ res.setHeader("Content-Type", "image/x-icon");
109
+ res.setHeader("Cache-Control", "no-cache");
110
+ res.end(Buffer.from(ico));
111
+ return;
112
+ }
113
+ }
114
+ if (baseName === "site.webmanifest" && generateManifest) {
115
+ const prefix = localeSource ? `/${localeSource.locale}` : "";
116
+ const manifest = {
117
+ name: config.name ?? "App",
118
+ short_name: config.name ?? "App",
119
+ icons: [{
120
+ src: `${prefix}/icon-192.png`,
121
+ sizes: "192x192",
122
+ type: "image/png"
123
+ }, {
124
+ src: `${prefix}/icon-512.png`,
125
+ sizes: "512x512",
126
+ type: "image/png"
127
+ }],
128
+ theme_color: themeColor,
129
+ background_color: backgroundColor,
130
+ display: "standalone"
131
+ };
132
+ res.setHeader("Content-Type", "application/manifest+json");
133
+ res.end(JSON.stringify(manifest, null, 2));
134
+ return;
135
+ }
136
+ next();
137
+ });
138
+ },
139
+ transformIndexHtml() {
140
+ const isSvg = config.source.endsWith(".svg");
141
+ const tags = [];
142
+ if (isSvg) tags.push({
143
+ tag: "link",
144
+ attrs: {
145
+ rel: "icon",
146
+ type: "image/svg+xml",
147
+ href: "/favicon.svg"
148
+ },
149
+ injectTo: "head"
150
+ });
151
+ tags.push({
152
+ tag: "link",
153
+ attrs: {
154
+ rel: "icon",
155
+ type: "image/png",
156
+ sizes: "32x32",
157
+ href: "/favicon-32x32.png"
158
+ },
159
+ injectTo: "head"
160
+ }, {
161
+ tag: "link",
162
+ attrs: {
163
+ rel: "icon",
164
+ type: "image/png",
165
+ sizes: "16x16",
166
+ href: "/favicon-16x16.png"
167
+ },
168
+ injectTo: "head"
169
+ }, {
170
+ tag: "link",
171
+ attrs: {
172
+ rel: "apple-touch-icon",
173
+ sizes: "180x180",
174
+ href: "/apple-touch-icon.png"
175
+ },
176
+ injectTo: "head"
177
+ });
178
+ if (generateManifest) tags.push({
179
+ tag: "link",
180
+ attrs: {
181
+ rel: "manifest",
182
+ href: "/site.webmanifest"
183
+ },
184
+ injectTo: "head"
185
+ });
186
+ tags.push({
187
+ tag: "meta",
188
+ attrs: {
189
+ name: "theme-color",
190
+ content: themeColor
191
+ },
192
+ injectTo: "head"
193
+ });
194
+ return tags;
195
+ },
196
+ async generateBundle() {
197
+ if (!isBuild) return;
198
+ await generateFaviconSet.call(this, root, config.source, config.darkSource, "", config, themeColor, backgroundColor, generateManifest);
199
+ if (config.locales) for (const [locale, localeConfig] of Object.entries(config.locales)) await generateFaviconSet.call(this, root, localeConfig.source, localeConfig.darkSource, `${locale}/`, config, themeColor, backgroundColor, generateManifest);
200
+ }
201
+ };
202
+ }
203
+ /**
204
+ * Wrap two SVGs into a single SVG that switches based on prefers-color-scheme.
205
+ */
206
+ function wrapSvgWithDarkMode(lightSvg, darkSvg) {
207
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${lightSvg.match(/viewBox="([^"]*)"/)?.[1] ?? "0 0 32 32"}">
208
+ <style>
209
+ :root { color-scheme: light dark; }
210
+ @media (prefers-color-scheme: dark) { .light { display: none; } }
211
+ @media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) { .dark { display: none; } }
212
+ </style>
213
+ <g class="light">${stripSvgWrapper(lightSvg)}</g>
214
+ <g class="dark">${stripSvgWrapper(darkSvg)}</g>
215
+ </svg>`;
216
+ }
217
+ function stripSvgWrapper(svg) {
218
+ return svg.replace(/<svg[^>]*>/, "").replace(/<\/svg>\s*$/, "").trim();
219
+ }
220
+ /**
221
+ * Resolve the source path for a locale-prefixed favicon URL.
222
+ * Returns null if the URL is not locale-prefixed or locale has no override.
223
+ */
224
+ function resolveLocaleSource(url, config, rootDir) {
225
+ if (!config.locales) return null;
226
+ for (const [locale, localeConfig] of Object.entries(config.locales)) {
227
+ const prefix = `/${locale}/`;
228
+ if (url.startsWith(prefix)) return {
229
+ locale,
230
+ url,
231
+ source: localeConfig.source,
232
+ sourcePath: join(rootDir, localeConfig.source)
233
+ };
234
+ }
235
+ return null;
236
+ }
237
+ /**
238
+ * Generate a complete favicon set (SVG, PNGs, ICO, manifest) with a file prefix.
239
+ * Called once for base (prefix = '') and once per locale (prefix = '{locale}/').
240
+ */
241
+ async function generateFaviconSet(rootDir, source, darkSource, prefix, config, themeColor, backgroundColor, generateManifest) {
242
+ const sourcePath = join(rootDir, source);
243
+ if (!existsSync(sourcePath)) {
244
+ console.warn(`[zero:favicon] Source not found: ${sourcePath}`);
245
+ return;
246
+ }
247
+ if (source.endsWith(".svg")) {
248
+ const svgContent = await readFile(sourcePath, "utf-8");
249
+ let finalSvg = svgContent;
250
+ if (darkSource) {
251
+ const darkPath = join(rootDir, darkSource);
252
+ if (existsSync(darkPath)) finalSvg = wrapSvgWithDarkMode(svgContent, await readFile(darkPath, "utf-8"));
253
+ }
254
+ this.emitFile({
255
+ type: "asset",
256
+ fileName: `${prefix}favicon.svg`,
257
+ source: finalSvg
258
+ });
259
+ }
260
+ for (const { size, name } of SIZES) {
261
+ const pngBuffer = await resizeToPng(sourcePath, size);
262
+ if (pngBuffer) this.emitFile({
263
+ type: "asset",
264
+ fileName: `${prefix}${name}`,
265
+ source: pngBuffer
266
+ });
267
+ }
268
+ const ico = await generateIco(sourcePath);
269
+ if (ico) this.emitFile({
270
+ type: "asset",
271
+ fileName: `${prefix}favicon.ico`,
272
+ source: ico
273
+ });
274
+ if (generateManifest) {
275
+ const manifestPrefix = prefix ? `/${prefix.slice(0, -1)}` : "";
276
+ const manifest = {
277
+ name: config.name ?? "App",
278
+ short_name: config.name ?? "App",
279
+ icons: [{
280
+ src: `${manifestPrefix}/icon-192.png`,
281
+ sizes: "192x192",
282
+ type: "image/png"
283
+ }, {
284
+ src: `${manifestPrefix}/icon-512.png`,
285
+ sizes: "512x512",
286
+ type: "image/png"
287
+ }],
288
+ theme_color: themeColor,
289
+ background_color: backgroundColor,
290
+ display: "standalone"
291
+ };
292
+ this.emitFile({
293
+ type: "asset",
294
+ fileName: `${prefix}site.webmanifest`,
295
+ source: JSON.stringify(manifest, null, 2)
296
+ });
297
+ }
298
+ }
299
+ /**
300
+ * Get favicon link tags for a specific locale.
301
+ * Returns link objects suitable for `useHead()` or direct HTML injection.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * const links = faviconLinks("de", { source: "./icon.svg", locales: { de: { source: "./icon-de.svg" } } })
306
+ * // → [{ rel: "icon", type: "image/svg+xml", href: "/de/favicon.svg" }, ...]
307
+ * ```
308
+ */
309
+ function faviconLinks(locale, config) {
310
+ const hasLocaleOverride = locale && config.locales?.[locale];
311
+ const prefix = hasLocaleOverride ? `/${locale}` : "";
312
+ const isSvg = (hasLocaleOverride ? config.locales[locale].source : config.source).endsWith(".svg");
313
+ const links = [];
314
+ if (isSvg) links.push({
315
+ rel: "icon",
316
+ type: "image/svg+xml",
317
+ href: `${prefix}/favicon.svg`
318
+ });
319
+ links.push({
320
+ rel: "icon",
321
+ type: "image/png",
322
+ sizes: "32x32",
323
+ href: `${prefix}/favicon-32x32.png`
324
+ }, {
325
+ rel: "icon",
326
+ type: "image/png",
327
+ sizes: "16x16",
328
+ href: `${prefix}/favicon-16x16.png`
329
+ }, {
330
+ rel: "apple-touch-icon",
331
+ sizes: "180x180",
332
+ href: `${prefix}/apple-touch-icon.png`
333
+ });
334
+ if (config.manifest !== false) links.push({
335
+ rel: "manifest",
336
+ href: `${prefix}/site.webmanifest`
337
+ });
338
+ return links;
339
+ }
340
+ async function resizeToPng(input, size) {
341
+ try {
342
+ return await (await import("sharp").then((m) => m.default ?? m))(input).resize(size, size, {
343
+ fit: "contain",
344
+ background: {
345
+ r: 0,
346
+ g: 0,
347
+ b: 0,
348
+ alpha: 0
349
+ }
350
+ }).png().toBuffer();
351
+ } catch {
352
+ warnSharpMissing();
353
+ return null;
354
+ }
355
+ }
356
+ async function generateIco(input) {
357
+ try {
358
+ const sharp = await import("sharp").then((m) => m.default ?? m);
359
+ const png16 = await sharp(input).resize(16, 16, {
360
+ fit: "contain",
361
+ background: {
362
+ r: 0,
363
+ g: 0,
364
+ b: 0,
365
+ alpha: 0
366
+ }
367
+ }).png().toBuffer();
368
+ const png32 = await sharp(input).resize(32, 32, {
369
+ fit: "contain",
370
+ background: {
371
+ r: 0,
372
+ g: 0,
373
+ b: 0,
374
+ alpha: 0
375
+ }
376
+ }).png().toBuffer();
377
+ return createIcoFromPngs([{
378
+ buffer: png16,
379
+ size: 16
380
+ }, {
381
+ buffer: png32,
382
+ size: 32
383
+ }]);
384
+ } catch {
385
+ warnSharpMissing();
386
+ return null;
387
+ }
388
+ }
389
+ /** @internal Exported for testing */
390
+ function createIcoFromPngs(entries) {
391
+ const headerSize = 6;
392
+ const dirEntrySize = 16;
393
+ const dirSize = dirEntrySize * entries.length;
394
+ let dataOffset = headerSize + dirSize;
395
+ const header = Buffer.alloc(headerSize);
396
+ header.writeUInt16LE(0, 0);
397
+ header.writeUInt16LE(1, 2);
398
+ header.writeUInt16LE(entries.length, 4);
399
+ const dirEntries = Buffer.alloc(dirSize);
400
+ const dataBuffers = [];
401
+ for (let i = 0; i < entries.length; i++) {
402
+ const entry = entries[i];
403
+ const offset = i * dirEntrySize;
404
+ dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset);
405
+ dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset + 1);
406
+ dirEntries.writeUInt8(0, offset + 2);
407
+ dirEntries.writeUInt8(0, offset + 3);
408
+ dirEntries.writeUInt16LE(1, offset + 4);
409
+ dirEntries.writeUInt16LE(32, offset + 6);
410
+ dirEntries.writeUInt32LE(entry.buffer.length, offset + 8);
411
+ dirEntries.writeUInt32LE(dataOffset, offset + 12);
412
+ dataOffset += entry.buffer.length;
413
+ dataBuffers.push(entry.buffer);
414
+ }
415
+ return Buffer.concat([
416
+ header,
417
+ dirEntries,
418
+ ...dataBuffers
419
+ ]);
420
+ }
421
+
422
+ //#endregion
423
+ export { createIcoFromPngs, faviconLinks, faviconPlugin };
424
+ //# sourceMappingURL=favicon.js.map