@sveltesentio/core 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0](https://github.com/golusoris/sveltesentio/compare/core-v0.0.1...core-v0.1.0) (2026-06-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * land foundation packages and repair CI gate ([#41](https://github.com/golusoris/sveltesentio/issues/41)) ([7557620](https://github.com/golusoris/sveltesentio/commit/75576200e324cd4c55f48571a6532540c1f6eb16))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lusoris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @sveltesentio/core
2
+
3
+ > Core utilities — rune helpers, type-safe fetch, CSP hooks, error boundaries
4
+
5
+ Part of the [sveltesentio](https://github.com/golusoris/sveltesentio) composable SvelteKit framework.
6
+
7
+ ## Status
8
+
9
+ ✅ v0.1.0 — `./env`, `./problem` (RFC 9457), `./http`, `./id` (UUIDv7), `./csp`,
10
+ `./vite`, and clock injection have shipped.
11
+
12
+ ## Requirements
13
+
14
+ **Zod v4 only.** `@sveltesentio/core` schemas require `zod@^4`
15
+ ([ADR-0001](../../docs/adr/0001-zod-v4-floor.md)); **v3 is unsupported** — a v3 schema
16
+ breaks `createEnv` error reporting (`z.treeifyError`) and the `@sveltesentio/forms`
17
+ `zod4` adapter. Downstream apps on `zod@^3` must upgrade first; follow the
18
+ [Zod v3 → v4 migration guide](../../docs/migrations/zod-v3-to-v4.md).
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pnpm add @sveltesentio/core
24
+ ```
25
+
26
+ See the [monorepo README](../../README.md) and [`docs/`](../../docs/) for design principles and usage.
27
+
28
+ ## License
29
+
30
+ MIT © lusoris
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@sveltesentio/core",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin, env schema, error types, id/clock utils — sveltesentio core",
5
+ "type": "module",
6
+ "private": false,
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.ts",
11
+ "types": "./src/index.ts"
12
+ },
13
+ "./clock": {
14
+ "import": "./src/clock.ts",
15
+ "types": "./src/clock.ts"
16
+ },
17
+ "./env": {
18
+ "import": "./src/env.ts",
19
+ "types": "./src/env.ts"
20
+ },
21
+ "./id": {
22
+ "import": "./src/id.ts",
23
+ "types": "./src/id.ts"
24
+ },
25
+ "./problem": {
26
+ "import": "./src/problem.ts",
27
+ "types": "./src/problem.ts"
28
+ },
29
+ "./http": {
30
+ "import": "./src/http.ts",
31
+ "types": "./src/http.ts"
32
+ },
33
+ "./csp": {
34
+ "import": "./src/csp.ts",
35
+ "types": "./src/csp.ts"
36
+ },
37
+ "./vite": {
38
+ "import": "./src/vite.ts",
39
+ "types": "./src/vite.ts"
40
+ }
41
+ },
42
+ "peerDependencies": {
43
+ "@sveltejs/kit": ">=2.0.0",
44
+ "esm-env": ">=1.0.0",
45
+ "openapi-fetch": ">=0.17.0",
46
+ "svelte": ">=5.0.0",
47
+ "uuid": ">=13.0.0",
48
+ "vite": ">=8.0.0",
49
+ "zod": ">=4.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "openapi-fetch": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@sveltejs/kit": "^2.0.0",
58
+ "@types/node": "^24.0.0",
59
+ "esm-env": "^1.2.2",
60
+ "openapi-fetch": "^0.17.0",
61
+ "svelte": "^5.55.4",
62
+ "typescript": "^6.0.3",
63
+ "uuid": "^13.0.0",
64
+ "vitest": "^4.1.4",
65
+ "zod": "^4.3.6"
66
+ },
67
+ "keywords": [
68
+ "sveltesentio",
69
+ "sveltekit",
70
+ "vite-plugin"
71
+ ],
72
+ "publishConfig": {
73
+ "access": "public"
74
+ },
75
+ "files": [
76
+ "src",
77
+ "CHANGELOG.md"
78
+ ],
79
+ "scripts": {
80
+ "build": "tsc",
81
+ "lint": "eslint src/",
82
+ "typecheck": "tsc --noEmit",
83
+ "test": "vitest run"
84
+ }
85
+ }
package/src/clock.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { getContext, hasContext, setContext } from 'svelte';
3
+ import { BROWSER } from 'esm-env';
4
+ import type { Handle } from '@sveltejs/kit';
5
+
6
+ export interface Clock {
7
+ now(): Date;
8
+ monotonic(): number;
9
+ }
10
+
11
+ export const systemClock: Clock = {
12
+ now: () => new Date(),
13
+ monotonic: () =>
14
+ BROWSER ? performance.now() : Number(process.hrtime.bigint()) / 1e6,
15
+ };
16
+
17
+ const CLOCK_KEY = Symbol.for('sveltesentio.clock');
18
+
19
+ const als: AsyncLocalStorage<Clock> | null = BROWSER
20
+ ? null
21
+ : new AsyncLocalStorage<Clock>({ name: 'clock', defaultValue: systemClock });
22
+
23
+ let clientClock: Clock = systemClock;
24
+
25
+ export function setClock(clock: Clock): void {
26
+ setContext(CLOCK_KEY, clock);
27
+ if (BROWSER) clientClock = clock;
28
+ }
29
+
30
+ export function useClock(): Clock {
31
+ if (hasContext(CLOCK_KEY)) return getContext<Clock>(CLOCK_KEY);
32
+ if (BROWSER) return clientClock;
33
+ return als?.getStore() ?? systemClock;
34
+ }
35
+
36
+ export function getClock(): Clock {
37
+ if (BROWSER) return clientClock;
38
+ return als?.getStore() ?? systemClock;
39
+ }
40
+
41
+ export function withClock(clock: Clock): Handle {
42
+ return ({ event, resolve }) => {
43
+ (event.locals as { clock?: Clock }).clock = clock;
44
+ if (!als) return resolve(event);
45
+ return als.run(clock, () => resolve(event));
46
+ };
47
+ }
48
+
49
+ export function createHydrationClock(serverNow: Date): Clock {
50
+ const serverMs = serverNow.getTime();
51
+ const monotonicAtHydration = BROWSER ? performance.now() : 0;
52
+ let firstRead = true;
53
+ return {
54
+ now: () => {
55
+ if (firstRead) {
56
+ firstRead = false;
57
+ return new Date(serverMs);
58
+ }
59
+ const delta = BROWSER ? performance.now() - monotonicAtHydration : 0;
60
+ return new Date(serverMs + delta);
61
+ },
62
+ monotonic: () => (BROWSER ? performance.now() : 0),
63
+ };
64
+ }
package/src/csp.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { newIdV4 } from './id.js';
2
+
3
+ export type CspSource = string;
4
+
5
+ export interface CspDirectives {
6
+ 'default-src'?: readonly CspSource[];
7
+ 'script-src'?: readonly CspSource[];
8
+ 'script-src-elem'?: readonly CspSource[];
9
+ 'style-src'?: readonly CspSource[];
10
+ 'style-src-elem'?: readonly CspSource[];
11
+ 'img-src'?: readonly CspSource[];
12
+ 'font-src'?: readonly CspSource[];
13
+ 'connect-src'?: readonly CspSource[];
14
+ 'media-src'?: readonly CspSource[];
15
+ 'frame-src'?: readonly CspSource[];
16
+ 'worker-src'?: readonly CspSource[];
17
+ 'manifest-src'?: readonly CspSource[];
18
+ 'object-src'?: readonly CspSource[];
19
+ 'base-uri'?: readonly CspSource[];
20
+ 'form-action'?: readonly CspSource[];
21
+ 'frame-ancestors'?: readonly CspSource[];
22
+ 'report-uri'?: readonly string[];
23
+ 'report-to'?: string;
24
+ 'upgrade-insecure-requests'?: boolean;
25
+ }
26
+
27
+ export function createNonce(): string {
28
+ return newIdV4();
29
+ }
30
+
31
+ export function nonceSource(nonce: string): CspSource {
32
+ return `'nonce-${nonce}'`;
33
+ }
34
+
35
+ export function hashSource(algo: 'sha256' | 'sha384' | 'sha512', base64: string): CspSource {
36
+ return `'${algo}-${base64}'`;
37
+ }
38
+
39
+ export const STRICT_DYNAMIC: CspSource = "'strict-dynamic'";
40
+ export const SELF: CspSource = "'self'";
41
+ export const NONE: CspSource = "'none'";
42
+
43
+ export interface StrictCspOptions {
44
+ nonce: string;
45
+ reportUri?: string;
46
+ connectSrc?: readonly CspSource[];
47
+ imgSrc?: readonly CspSource[];
48
+ fontSrc?: readonly CspSource[];
49
+ styleSrc?: readonly CspSource[];
50
+ mediaSrc?: readonly CspSource[];
51
+ extra?: CspDirectives;
52
+ }
53
+
54
+ export function strictCsp(options: StrictCspOptions): CspDirectives {
55
+ const { nonce, reportUri, connectSrc, imgSrc, fontSrc, styleSrc, mediaSrc, extra } = options;
56
+ return {
57
+ 'default-src': [SELF],
58
+ 'script-src': [STRICT_DYNAMIC, nonceSource(nonce)],
59
+ 'style-src': styleSrc ?? [SELF, nonceSource(nonce)],
60
+ 'img-src': imgSrc ?? [SELF, 'data:'],
61
+ 'font-src': fontSrc ?? [SELF],
62
+ 'connect-src': connectSrc ?? [SELF],
63
+ 'media-src': mediaSrc ?? [SELF],
64
+ 'object-src': [NONE],
65
+ 'base-uri': [NONE],
66
+ 'frame-ancestors': [NONE],
67
+ 'form-action': [SELF],
68
+ 'upgrade-insecure-requests': true,
69
+ ...(reportUri ? { 'report-uri': [reportUri] } : {}),
70
+ ...extra,
71
+ };
72
+ }
73
+
74
+ export function serialiseCsp(directives: CspDirectives): string {
75
+ const parts: string[] = [];
76
+ for (const [name, value] of Object.entries(directives) as Array<[
77
+ keyof CspDirectives,
78
+ CspDirectives[keyof CspDirectives],
79
+ ]>) {
80
+ if (value === undefined || value === false) continue;
81
+ if (value === true) {
82
+ parts.push(name);
83
+ continue;
84
+ }
85
+ if (typeof value === 'string') {
86
+ parts.push(`${name} ${value}`);
87
+ continue;
88
+ }
89
+ if (value.length === 0) continue;
90
+ parts.push(`${name} ${value.join(' ')}`);
91
+ }
92
+ return parts.join('; ');
93
+ }
package/src/env.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { ZodObject, ZodRawShape, infer as ZodInfer } from 'zod';
2
+ import { z } from 'zod';
3
+
4
+ export class EnvValidationError extends Error {
5
+ readonly tree: unknown;
6
+ constructor(kind: 'server' | 'public', tree: unknown) {
7
+ super(`Invalid ${kind} environment. ${summarise(tree)}`);
8
+ this.name = 'EnvValidationError';
9
+ this.tree = tree;
10
+ }
11
+ }
12
+
13
+ export interface EnvOptions<
14
+ TServer extends ZodRawShape,
15
+ TPublic extends ZodRawShape,
16
+ > {
17
+ server: ZodObject<TServer>;
18
+ publicEnv: ZodObject<TPublic>;
19
+ runtimeEnv: Record<string, string | undefined>;
20
+ skipValidation?: boolean;
21
+ }
22
+
23
+ export type Env<
24
+ TServer extends ZodRawShape,
25
+ TPublic extends ZodRawShape,
26
+ > = Readonly<ZodInfer<ZodObject<TServer>> & ZodInfer<ZodObject<TPublic>>>;
27
+
28
+ export function createEnv<
29
+ TServer extends ZodRawShape,
30
+ TPublic extends ZodRawShape,
31
+ >(options: EnvOptions<TServer, TPublic>): Env<TServer, TPublic> {
32
+ const { server, publicEnv, runtimeEnv, skipValidation = false } = options;
33
+
34
+ if (skipValidation) {
35
+ return runtimeEnv as unknown as Env<TServer, TPublic>;
36
+ }
37
+
38
+ const serverResult = server.safeParse(runtimeEnv);
39
+ if (!serverResult.success) {
40
+ throw new EnvValidationError('server', z.treeifyError(serverResult.error));
41
+ }
42
+
43
+ const publicResult = publicEnv.safeParse(runtimeEnv);
44
+ if (!publicResult.success) {
45
+ throw new EnvValidationError('public', z.treeifyError(publicResult.error));
46
+ }
47
+
48
+ return Object.freeze({
49
+ ...serverResult.data,
50
+ ...publicResult.data,
51
+ });
52
+ }
53
+
54
+ export function requireEnv(name: string, value: string | undefined): string {
55
+ if (value === undefined || value === '') {
56
+ throw new EnvValidationError('server', {
57
+ errors: [`Required environment variable "${name}" is not set`],
58
+ });
59
+ }
60
+ return value;
61
+ }
62
+
63
+ function summarise(tree: unknown): string {
64
+ try {
65
+ return JSON.stringify(tree, null, 2);
66
+ } catch {
67
+ return '[unserialisable tree]';
68
+ }
69
+ }
package/src/http.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { Middleware } from 'openapi-fetch';
2
+ import {
3
+ ProblemError,
4
+ isProblemResponse,
5
+ parseProblem,
6
+ problemFromDocument,
7
+ } from './problem.js';
8
+
9
+ export { ProblemError } from './problem.js';
10
+ export type { ProblemDocument, ProblemErrorInit, InvalidParam } from './problem.js';
11
+
12
+ export interface ProblemMiddlewareOptions {
13
+ onProblem?: (error: ProblemError) => void;
14
+ }
15
+
16
+ export function problemMiddleware(options: ProblemMiddlewareOptions = {}): Middleware {
17
+ return {
18
+ onResponse: async ({ response }) => {
19
+ if (response.ok) return undefined;
20
+ if (!isProblemResponse(response)) return undefined;
21
+
22
+ const clone = response.clone();
23
+ let body: unknown;
24
+ try {
25
+ body = await clone.json();
26
+ } catch (cause) {
27
+ throw new ProblemError({
28
+ type: 'about:blank',
29
+ title: response.statusText || 'Problem parse failure',
30
+ status: response.status,
31
+ cause,
32
+ });
33
+ }
34
+
35
+ const parsed = parseProblem(body);
36
+ const error = parsed
37
+ ? problemFromDocument(parsed)
38
+ : new ProblemError({
39
+ type: 'about:blank',
40
+ title: response.statusText || 'Unknown problem',
41
+ status: response.status,
42
+ detail: typeof body === 'string' ? body : undefined,
43
+ });
44
+
45
+ options.onProblem?.(error);
46
+ throw error;
47
+ },
48
+ };
49
+ }
package/src/id.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { v4 as uuidv4, v7 as uuidv7, validate, version } from 'uuid';
2
+
3
+ export type Id<Brand extends string = string> = string & { readonly __brand: Brand };
4
+
5
+ export function newId(): string {
6
+ return uuidv7();
7
+ }
8
+
9
+ export function newIdV4(): string {
10
+ return uuidv4();
11
+ }
12
+
13
+ export function isId(value: unknown): value is string {
14
+ return typeof value === 'string' && validate(value) && version(value) === 7;
15
+ }
16
+
17
+ export function isIdV4(value: unknown): value is string {
18
+ return typeof value === 'string' && validate(value) && version(value) === 4;
19
+ }
20
+
21
+ export function brandId<Brand extends string>(value: string): Id<Brand> {
22
+ if (!isId(value)) throw new TypeError(`Not a valid UUIDv7: ${value}`);
23
+ return value as Id<Brand>;
24
+ }
25
+
26
+ export function idToTimestamp(id: string): number {
27
+ if (!isId(id)) throw new TypeError(`Not a valid UUIDv7: ${id}`);
28
+ const hex = id.replace(/-/g, '').slice(0, 12);
29
+ return Number.parseInt(hex, 16);
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ export type { Clock } from './clock.js';
2
+ export {
3
+ createHydrationClock,
4
+ getClock,
5
+ setClock,
6
+ systemClock,
7
+ useClock,
8
+ withClock,
9
+ } from './clock.js';
10
+
11
+ export type { Env, EnvOptions } from './env.js';
12
+ export { EnvValidationError, createEnv, requireEnv } from './env.js';
13
+
14
+ export type { Id } from './id.js';
15
+ export { brandId, idToTimestamp, isId, isIdV4, newId, newIdV4 } from './id.js';
16
+
17
+ export type {
18
+ InvalidParam,
19
+ ProblemDocument,
20
+ ProblemErrorInit,
21
+ } from './problem.js';
22
+ export {
23
+ ProblemError,
24
+ isProblemResponse,
25
+ parseProblem,
26
+ problemFromDocument,
27
+ problemFromResponse,
28
+ } from './problem.js';
29
+
30
+ export type { CspDirectives, CspSource, StrictCspOptions } from './csp.js';
31
+ export {
32
+ NONE,
33
+ SELF,
34
+ STRICT_DYNAMIC,
35
+ createNonce,
36
+ hashSource,
37
+ nonceSource,
38
+ serialiseCsp,
39
+ strictCsp,
40
+ } from './csp.js';
41
+
42
+ export type { SentioPluginOptions } from './vite.js';
43
+ export { sentioPlugin } from './vite.js';
package/src/problem.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { z } from 'zod';
2
+
3
+ const invalidParamSchema = z.object({
4
+ name: z.string(),
5
+ reason: z.string(),
6
+ });
7
+
8
+ const problemSchema = z
9
+ .object({
10
+ type: z.string().default('about:blank'),
11
+ title: z.string().optional(),
12
+ status: z.number().int().optional(),
13
+ detail: z.string().optional(),
14
+ instance: z.string().optional(),
15
+ 'invalid-params': z.array(invalidParamSchema).optional(),
16
+ })
17
+ .passthrough();
18
+
19
+ export type InvalidParam = z.infer<typeof invalidParamSchema>;
20
+ export type ProblemDocument = z.infer<typeof problemSchema>;
21
+
22
+ export interface ProblemErrorInit {
23
+ type: string;
24
+ title?: string | undefined;
25
+ status?: number | undefined;
26
+ detail?: string | undefined;
27
+ instance?: string | undefined;
28
+ invalidParams?: readonly InvalidParam[] | undefined;
29
+ extensions?: Readonly<Record<string, unknown>> | undefined;
30
+ cause?: unknown;
31
+ }
32
+
33
+ export class ProblemError extends Error {
34
+ readonly type: string;
35
+ readonly title: string | undefined;
36
+ readonly status: number | undefined;
37
+ readonly detail: string | undefined;
38
+ readonly instance: string | undefined;
39
+ readonly invalidParams: readonly InvalidParam[] | undefined;
40
+ readonly extensions: Readonly<Record<string, unknown>>;
41
+
42
+ constructor(init: ProblemErrorInit) {
43
+ const message =
44
+ init.detail ?? init.title ?? `Problem: ${init.type} (${init.status ?? '?'})`;
45
+ super(message, init.cause === undefined ? undefined : { cause: init.cause });
46
+ this.name = 'ProblemError';
47
+ this.type = init.type;
48
+ this.title = init.title;
49
+ this.status = init.status;
50
+ this.detail = init.detail;
51
+ this.instance = init.instance;
52
+ this.invalidParams = init.invalidParams;
53
+ this.extensions = init.extensions ?? {};
54
+ }
55
+
56
+ toJSON(): ProblemDocument {
57
+ const base: Record<string, unknown> = {
58
+ type: this.type,
59
+ ...this.extensions,
60
+ };
61
+ if (this.title !== undefined) base.title = this.title;
62
+ if (this.status !== undefined) base.status = this.status;
63
+ if (this.detail !== undefined) base.detail = this.detail;
64
+ if (this.instance !== undefined) base.instance = this.instance;
65
+ if (this.invalidParams !== undefined) base['invalid-params'] = this.invalidParams;
66
+ return base as ProblemDocument;
67
+ }
68
+ }
69
+
70
+ export function parseProblem(input: unknown): ProblemDocument | undefined {
71
+ const result = problemSchema.safeParse(input);
72
+ return result.success ? result.data : undefined;
73
+ }
74
+
75
+ export function problemFromDocument(doc: ProblemDocument, cause?: unknown): ProblemError {
76
+ const {
77
+ type,
78
+ title,
79
+ status,
80
+ detail,
81
+ instance,
82
+ ['invalid-params']: invalidParams,
83
+ ...rest
84
+ } = doc;
85
+ return new ProblemError({
86
+ type,
87
+ title,
88
+ status,
89
+ detail,
90
+ instance,
91
+ invalidParams,
92
+ extensions: rest,
93
+ cause,
94
+ });
95
+ }
96
+
97
+ export function problemFromResponse(
98
+ response: Response,
99
+ body: unknown,
100
+ cause?: unknown,
101
+ ): ProblemError {
102
+ const parsed = parseProblem(body);
103
+ if (parsed) return problemFromDocument(parsed, cause);
104
+ return new ProblemError({
105
+ type: 'about:blank',
106
+ title: response.statusText || 'HTTP error',
107
+ status: response.status,
108
+ detail: typeof body === 'string' ? body : undefined,
109
+ cause,
110
+ });
111
+ }
112
+
113
+ export function isProblemResponse(response: Response): boolean {
114
+ const ct = response.headers.get('content-type') ?? '';
115
+ return ct.toLowerCase().includes('application/problem+json');
116
+ }
package/src/vite.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { Plugin } from 'vite';
2
+
3
+ export interface SentioPluginOptions {
4
+ requiredEnv?: readonly string[];
5
+ verbose?: boolean;
6
+ virtualModule?: Readonly<Record<string, unknown>>;
7
+ }
8
+
9
+ const VIRTUAL_ID = '$sentio';
10
+ const RESOLVED_ID = '\0$sentio';
11
+
12
+ export function sentioPlugin(options: SentioPluginOptions = {}): Plugin {
13
+ const { requiredEnv = [], verbose = false, virtualModule = {} } = options;
14
+
15
+ return {
16
+ name: 'vite-plugin-sentio',
17
+ enforce: 'pre',
18
+
19
+ resolveId(id) {
20
+ if (id === VIRTUAL_ID) return RESOLVED_ID;
21
+ return undefined;
22
+ },
23
+
24
+ load(id) {
25
+ if (id !== RESOLVED_ID) return undefined;
26
+ const entries = Object.entries(virtualModule);
27
+ const exports = entries
28
+ .map(([key, value]) => `export const ${key} = ${JSON.stringify(value)};`)
29
+ .join('\n');
30
+ return `${exports}\nexport default Object.freeze(${JSON.stringify(virtualModule)});\n`;
31
+ },
32
+
33
+ configResolved(config) {
34
+ if (verbose) {
35
+ console.warn('[sentio] Resolved Vite config:', {
36
+ mode: config.mode,
37
+ root: config.root,
38
+ build: { outDir: config.build.outDir, ssr: config.build.ssr },
39
+ });
40
+ }
41
+ },
42
+
43
+ buildStart() {
44
+ const missing = requiredEnv.filter(
45
+ (key) => !process.env[key] || process.env[key] === '',
46
+ );
47
+ if (missing.length > 0) {
48
+ throw new Error(
49
+ `[sentio] Missing required environment variables:\n${missing
50
+ .map((k) => ` - ${k}`)
51
+ .join('\n')}\nCheck your .env file or deployment environment.`,
52
+ );
53
+ }
54
+ },
55
+ };
56
+ }