@rx-ted/packages-core 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @rx-ted/packages-core
2
+
3
+ Unified runtime context, environment config, and logging for multi-platform TypeScript (Node, Bun, Deno, Cloudflare Workers, Vercel Edge).
4
+
5
+ ```
6
+ packages/core/src/
7
+ ├── config/ — Runtime detection, env vars, dotenv loading
8
+ │ ├── runtime.ts Runtime detection (node/bun/deno/cf/edge)
9
+ │ ├── env.ts Env class (get / var / require / setLogger)
10
+ │ ├── dotenv.ts parseDotenv + async loadEnv with directory traversal
11
+ │ ├── prefixes.ts resolveKey / filterKeys for prefixed env vars
12
+ │ ├── create-env.ts Zod-schema-driven typed env validation
13
+ │ └── runtime-helpers.ts createNodeContext / createCloudflareContext
14
+ ├── logger/ — Console and Pino logging
15
+ │ ├── types.ts ILogger / NOOP_LOGGER / LoggerOptions
16
+ │ ├── logger.ts Logger class (Console fallback → Pino upgrade)
17
+ │ └── console-logger.ts Zero-dependency console implementation
18
+ └── utils/
19
+ └── shared.ts ConfigError / assertString / assertNumber / assertBoolean / assertUrl
20
+ ```
21
+
22
+ All tests are co-located with their source files (`*.test.ts` next to each module).
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { env, Runtime, Logger } from '@rx-ted/packages-core';
28
+ ```
29
+
30
+ ### Runtime
31
+
32
+ Detect platform and access per-request context (ALS-based):
33
+
34
+ ```ts
35
+ Runtime.platform() // 'node' | 'bun' | 'deno' | 'cloudflare' | 'vercel-edge'
36
+ Runtime.env() // platform-aware env source
37
+ Runtime.request() // current Request (inside run())
38
+ Runtime.run({ platform, env, request }, () => { /* per-request scope */ })
39
+ ```
40
+
41
+ ### Env
42
+
43
+ ```ts
44
+ env.get('PORT') // string | undefined
45
+ env.get('PORT', 'number') // number | undefined
46
+ env.var('DB_PATH', './db') // string — falls back to './db' if unset
47
+ env.require('JWT_SECRET') // string — throws if missing
48
+ env.has('SOME_KEY') // boolean
49
+ env.setLogger(logger) // inject ILogger for debug traces
50
+ ```
51
+
52
+ ### Logger
53
+
54
+ ```ts
55
+ const log = new Logger({ name: 'app', level: 'debug' });
56
+ // or
57
+ const log = createLogger({ name: 'app', level: 'info', destination: 'logs/app.log' });
58
+
59
+ log.debug('connecting to %s', host);
60
+ log.info({ userId }, 'user logged in');
61
+ log.error(err, 'request failed');
62
+ log.child({ module: 'auth' }).warn('rate limit hit');
63
+ ```
64
+
65
+ - **Node/Bun**: upgrades from `ConsoleLogger` → `pino` (async, best-effort)
66
+ - **Edge/CF**: uses `ConsoleLogger` directly (no dynamic imports)
67
+ - **File output**: set `destination` — adds `pino/file` transport automatically
68
+ - **NOOP_LOGGER**: import for tests or when logging should be silenced
69
+
70
+ ### Dotenv
71
+
72
+ ```ts
73
+ await loadEnv(); // searches .env up to 3 parent dirs from CWD
74
+ await loadEnv('/path'); // start search from /path
75
+ ```
76
+
77
+ Strategy: `.env` (base) → merges `.env.dev` if `DEBUG=true` or `.env.prod` if not.
78
+ Safe in Cloudflare Workers (returns `{}` when filesystem unavailable).
79
+
80
+ ### Zod-schema env validation
81
+
82
+ ```ts
83
+ import { createEnv, z } from '@rx-ted/packages-core';
84
+
85
+ const cfg = createEnv({
86
+ schema: { PORT: z.coerce.number().default(3000) },
87
+ prefixes: ['MY_APP'],
88
+ });
89
+ // reads MY_APP_PORT → falls back to PORT → defaults to 3000
90
+ ```
91
+
92
+ ### getSchema
93
+
94
+ ```ts
95
+ import { getSchema, z } from '@rx-ted/packages-core';
96
+
97
+ const db = getSchema('DB', {
98
+ host: z.string().default('127.0.0.1'),
99
+ port: z.coerce.number().default(3306),
100
+ user: z.string(),
101
+ password: z.string(),
102
+ });
103
+ // Reads DB_HOST, DB_PORT, DB_USER, DB_PASSWORD
104
+ ```
105
+
106
+ ## Platform compatibility
107
+
108
+ | Feature | Node | Bun | Deno | Cloudflare | Vercel Edge |
109
+ |----------------|------|-----|------|------------|-------------|
110
+ | Runtime | ✅ | ✅ | ✅ | ✅ | ✅ |
111
+ | Env (get/var) | ✅ | ✅ | ✅ | ✅ | ✅ |
112
+ | Env (require) | ✅ | ✅ | ✅ | ✅ | ✅ |
113
+ | loadEnv | ✅ | ✅ | ❌ | ❌ (safe) | ❌ (safe) |
114
+ | Logger (console)| ✅ | ✅ | ✅ | ✅ | ✅ |
115
+ | Logger (pino) | ✅ | ✅ | ❌ | ❌ | ❌ |
116
+ | Logger (file) | ✅ | ✅ | ❌ | ❌ | ❌ |
117
+
118
+ The package exports a single barrel (`index.ts`) — all platforms import from `@rx-ted/packages-core`.
@@ -0,0 +1,18 @@
1
+ import type { z } from 'zod';
2
+ import { Platform } from './platform';
3
+ type EnvSource = Record<string, string | undefined>;
4
+ type ZodSchemaMap = Record<string, z.ZodTypeAny>;
5
+ type InferEnv<T extends ZodSchemaMap> = {
6
+ [K in keyof T]: z.infer<T[K]>;
7
+ } & {
8
+ toJSON(): Record<string, unknown>;
9
+ };
10
+ export interface CreateEnvOptions<T extends ZodSchemaMap> {
11
+ schema: T;
12
+ prefixes?: string[];
13
+ runtimeEnv?: EnvSource;
14
+ runtime?: Platform;
15
+ skipValidation?: boolean;
16
+ }
17
+ export declare function createEnv<T extends ZodSchemaMap>(options: CreateEnvOptions<T>): InferEnv<T>;
18
+ export {};
@@ -0,0 +1,43 @@
1
+ import { Platform } from './platform';
2
+ import { resolveKey } from './prefixes';
3
+ import { ConfigError } from '../utils/shared';
4
+ export function createEnv(options) {
5
+ const { schema, prefixes = [] } = options;
6
+ const source = options.runtimeEnv ?? Platform.env();
7
+ const validated = {};
8
+ for (const key of Object.keys(schema)) {
9
+ const raw = resolveKey(key, source, prefixes);
10
+ const zodSchema = schema[key];
11
+ const result = zodSchema.safeParse(raw);
12
+ if (result.success) {
13
+ validated[key] = result.data;
14
+ }
15
+ else if (raw === undefined) {
16
+ const defResult = zodSchema.safeParse(undefined);
17
+ if (defResult.success) {
18
+ validated[key] = defResult.data;
19
+ }
20
+ else if (!options.skipValidation) {
21
+ throw new ConfigError(`Validation failed for "${key}": ${result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`);
22
+ }
23
+ }
24
+ else if (!options.skipValidation) {
25
+ throw new ConfigError(`Validation failed for "${key}": ${result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`);
26
+ }
27
+ }
28
+ return new Proxy(validated, {
29
+ get(target, prop) {
30
+ if (prop === 'toJSON')
31
+ return () => ({ ...target });
32
+ if (typeof prop === 'string' && prop in target)
33
+ return target[prop];
34
+ return undefined;
35
+ },
36
+ ownKeys(target) {
37
+ return Reflect.ownKeys(target);
38
+ },
39
+ getOwnPropertyDescriptor(target, prop) {
40
+ return Reflect.getOwnPropertyDescriptor(target, prop);
41
+ },
42
+ });
43
+ }
@@ -0,0 +1,20 @@
1
+ export type DotenvOptions = never;
2
+ declare function loadEnvImpl(cwd?: string): Promise<Record<string, string>>;
3
+ /**
4
+ * Load .env file chain — safe for drizzle configs and other Node-only CLI tools.
5
+ * Returns {} when filesystem is unavailable.
6
+ */
7
+ export declare const loadEnvSync: typeof loadEnvImpl;
8
+ /**
9
+ * Load .env file chain by searching up to 3 parent directories from `cwd`.
10
+ *
11
+ * Strategy (from lowest to highest priority):
12
+ * 1. `.env` — always loaded as base
13
+ * 2. If `DEBUG=true` → `.env.dev` overrides base
14
+ * 3. If `DEBUG=false` → `.env.prod` overrides base
15
+ *
16
+ * Safe in Cloudflare Workers — returns `{}` when filesystem is unavailable.
17
+ */
18
+ export declare const loadEnv: typeof loadEnvImpl;
19
+ export declare function parseDotenv(content: string): Record<string, string>;
20
+ export {};
@@ -0,0 +1,76 @@
1
+ import { getNodeModules } from './node-modules';
2
+ async function loadEnvImpl(cwd) {
3
+ const modules = await getNodeModules();
4
+ if (!modules)
5
+ return {};
6
+ const { readFileSync, existsSync, resolve } = modules;
7
+ const startDir = cwd ?? (typeof process !== 'undefined' ? process.cwd() : '');
8
+ if (!startDir)
9
+ return {};
10
+ function findUp(filename) {
11
+ let dir = startDir;
12
+ for (let i = 0; i < 4; i++) {
13
+ const filePath = resolve(dir, filename);
14
+ if (existsSync(filePath))
15
+ return filePath;
16
+ const parent = resolve(dir, '..');
17
+ if (parent === dir)
18
+ break;
19
+ dir = parent;
20
+ }
21
+ return null;
22
+ }
23
+ const env = {};
24
+ const envPath = findUp('.env');
25
+ if (envPath) {
26
+ Object.assign(env, parseDotenv(readFileSync(envPath, 'utf-8')));
27
+ }
28
+ else {
29
+ return env;
30
+ }
31
+ const isDebug = process.env.DEBUG === 'true';
32
+ const overrideName = isDebug ? '.env.dev' : '.env.prod';
33
+ const overridePath = findUp(overrideName);
34
+ if (overridePath) {
35
+ Object.assign(env, parseDotenv(readFileSync(overridePath, 'utf-8')));
36
+ }
37
+ for (const [key, value] of Object.entries(env)) {
38
+ if (!(key in process.env)) {
39
+ process.env[key] = value;
40
+ }
41
+ }
42
+ return env;
43
+ }
44
+ /**
45
+ * Load .env file chain — safe for drizzle configs and other Node-only CLI tools.
46
+ * Returns {} when filesystem is unavailable.
47
+ */
48
+ export const loadEnvSync = loadEnvImpl;
49
+ /**
50
+ * Load .env file chain by searching up to 3 parent directories from `cwd`.
51
+ *
52
+ * Strategy (from lowest to highest priority):
53
+ * 1. `.env` — always loaded as base
54
+ * 2. If `DEBUG=true` → `.env.dev` overrides base
55
+ * 3. If `DEBUG=false` → `.env.prod` overrides base
56
+ *
57
+ * Safe in Cloudflare Workers — returns `{}` when filesystem is unavailable.
58
+ */
59
+ export const loadEnv = loadEnvImpl;
60
+ export function parseDotenv(content) {
61
+ const result = {};
62
+ for (const line of content.split('\n')) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('='))
65
+ continue;
66
+ const eqIndex = trimmed.indexOf('=');
67
+ const key = trimmed.slice(0, eqIndex).trim();
68
+ let raw = trimmed.slice(eqIndex + 1).trim();
69
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
70
+ raw = raw.slice(1, -1);
71
+ }
72
+ if (key)
73
+ result[key] = raw;
74
+ }
75
+ return result;
76
+ }
@@ -0,0 +1,39 @@
1
+ import type { Platform as PlatformType } from './platform';
2
+ import type { DotenvOptions } from './dotenv';
3
+ import type { z } from 'zod';
4
+ export type EnvMode = 'prod' | 'dev';
5
+ export declare const ENV_SYMBOL: unique symbol;
6
+ export declare class Env {
7
+ private _env;
8
+ private _ctx;
9
+ constructor(env?: Record<string, any>, ctx?: Record<string, any>);
10
+ get platform(): PlatformType;
11
+ get mode(): EnvMode;
12
+ get DEBUG(): boolean;
13
+ /** Read from merged source: ctx overrides env. Supports all value types. */
14
+ get(key: string): any;
15
+ get(key: string, type: 'string'): string;
16
+ get(key: string, type: 'number'): number;
17
+ get(key: string, type: 'boolean'): boolean;
18
+ get(key: string, type: 'url'): URL;
19
+ /** Read from pre-boot env only (process.env style). */
20
+ var(key: string): any;
21
+ var(key: string, defaultValue: string): string;
22
+ /** Read from runtime bindings only (CF D1/KV/R2 etc.). */
23
+ ctx(key: string): any;
24
+ /** Merged read, throws if missing. */
25
+ require(key: string): string;
26
+ has(key: string): boolean;
27
+ /** Write to pre-boot env source. Returns this for chaining. */
28
+ set(key: string, value: any): this;
29
+ toObject(): Record<string, any>;
30
+ /** Parse a group of env vars by Zod schema, with optional prefix fallback. */
31
+ schema<T extends Record<string, z.ZodTypeAny>>(schema: T, opts?: {
32
+ prefix?: string;
33
+ prefixes?: string[];
34
+ }): {
35
+ [K in keyof T]: z.output<T[K]>;
36
+ };
37
+ loadDotenv(_options?: DotenvOptions): Promise<this>;
38
+ }
39
+ export declare const env: Env;
@@ -0,0 +1,99 @@
1
+ import { detectPlatform } from './platform';
2
+ import { assertString, assertNumber, assertBoolean, assertUrl } from '../utils/shared';
3
+ import { resolveKey } from './prefixes';
4
+ export const ENV_SYMBOL = Symbol('app:env');
5
+ export class Env {
6
+ _env;
7
+ _ctx;
8
+ constructor(env, ctx) {
9
+ this._env = env ?? {};
10
+ this._ctx = ctx ?? {};
11
+ }
12
+ get platform() {
13
+ return detectPlatform();
14
+ }
15
+ get mode() {
16
+ const debugRaw = this._env.DEBUG ?? this._ctx.DEBUG;
17
+ const raw = this._env.NODE_ENV ?? this._env.APP_ENV ?? this._ctx.NODE_ENV ?? 'dev';
18
+ const intended = raw === 'production' || raw === 'prod' ? 'prod' : 'dev';
19
+ if (debugRaw === 'false') {
20
+ if (intended === 'dev') {
21
+ console.warn('[packages-core] DEBUG=false overrides mode="dev" → using mode="prod". ' +
22
+ 'Set NODE_ENV=production or APP_ENV=prod to suppress this warning.');
23
+ }
24
+ return 'prod';
25
+ }
26
+ if (debugRaw === 'true') {
27
+ if (intended === 'prod') {
28
+ console.warn('[packages-core] DEBUG=true overrides mode="prod" → using mode="dev". ' +
29
+ 'Set NODE_ENV=development or remove DEBUG=true to suppress this warning.');
30
+ }
31
+ return 'dev';
32
+ }
33
+ return intended;
34
+ }
35
+ get DEBUG() {
36
+ const explicit = this._env.DEBUG ?? this._ctx.DEBUG;
37
+ if (explicit === 'true')
38
+ return true;
39
+ if (explicit === 'false')
40
+ return false;
41
+ return this.mode !== 'prod';
42
+ }
43
+ get(key, type) {
44
+ const raw = key in this._ctx ? this._ctx[key] : this._env[key];
45
+ if (type === 'string')
46
+ return assertString(key, raw);
47
+ if (type === 'number')
48
+ return assertNumber(key, raw);
49
+ if (type === 'boolean')
50
+ return assertBoolean(key, raw);
51
+ if (type === 'url')
52
+ return assertUrl(key, raw);
53
+ return raw;
54
+ }
55
+ var(key, defaultValue) {
56
+ const raw = this._env[key];
57
+ return raw ?? defaultValue;
58
+ }
59
+ /** Read from runtime bindings only (CF D1/KV/R2 etc.). */
60
+ ctx(key) {
61
+ return this._ctx[key];
62
+ }
63
+ /** Merged read, throws if missing. */
64
+ require(key) {
65
+ const raw = key in this._ctx ? this._ctx[key] : this._env[key];
66
+ return assertString(key, raw);
67
+ }
68
+ has(key) {
69
+ return key in this._env || key in this._ctx;
70
+ }
71
+ /** Write to pre-boot env source. Returns this for chaining. */
72
+ set(key, value) {
73
+ this._env[key] = value;
74
+ return this;
75
+ }
76
+ toObject() {
77
+ return { ...this._env, ...this._ctx };
78
+ }
79
+ /** Parse a group of env vars by Zod schema, with optional prefix fallback. */
80
+ schema(schema, opts) {
81
+ const source = { ...this._env, ...this._ctx };
82
+ const allPrefixes = opts?.prefix
83
+ ? [opts.prefix, ...(opts.prefixes ?? [])]
84
+ : (opts?.prefixes ?? []);
85
+ const result = {};
86
+ for (const [key, zodType] of Object.entries(schema)) {
87
+ const upperKey = key.toUpperCase();
88
+ const prefixedKey = allPrefixes.length > 0
89
+ ? resolveKey(upperKey, source, allPrefixes.map((p) => `${p}_`))
90
+ : source[upperKey];
91
+ result[key] = zodType.parse(prefixedKey ?? undefined);
92
+ }
93
+ return result;
94
+ }
95
+ loadDotenv(_options) {
96
+ throw new Error('[packages-core] env.loadDotenv() is removed. Use Platform.setAppContext() to provide env data at startup.');
97
+ }
98
+ }
99
+ export const env = typeof process !== 'undefined' ? new Env(process.env, {}) : new Env({}, {});
@@ -0,0 +1,6 @@
1
+ export interface NodeModules {
2
+ readFileSync: (path: string, encoding: string) => string;
3
+ existsSync: (path: string) => boolean;
4
+ resolve: (...parts: string[]) => string;
5
+ }
6
+ export declare function getNodeModules(): Promise<NodeModules | null>;
@@ -0,0 +1,17 @@
1
+ let cached = null;
2
+ export async function getNodeModules() {
3
+ if (cached)
4
+ return cached;
5
+ try {
6
+ const [fs, path] = await Promise.all([import('node:fs'), import('node:path')]);
7
+ cached = {
8
+ readFileSync: fs.readFileSync,
9
+ existsSync: fs.existsSync,
10
+ resolve: path.resolve,
11
+ };
12
+ return cached;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
@@ -0,0 +1,20 @@
1
+ export type Platform = 'node' | 'bun' | 'deno' | 'cloudflare' | 'vercel-edge';
2
+ export interface RuntimeContext {
3
+ platform: Platform;
4
+ env: Record<string, any>;
5
+ request?: Request;
6
+ executionContext?: unknown;
7
+ waitUntil?: (promise: Promise<unknown>) => void;
8
+ }
9
+ declare function detectPlatform(): Platform;
10
+ declare function resetDetection(): void;
11
+ export { detectPlatform, resetDetection };
12
+ export declare const Platform: {
13
+ run<T>(context: RuntimeContext, fn: () => T): T;
14
+ context(): RuntimeContext;
15
+ env(): Record<string, string | undefined>;
16
+ request(): Request | undefined;
17
+ platform(): Platform;
18
+ setAppContext(context: RuntimeContext): void;
19
+ clearAppContext(): void;
20
+ };
@@ -0,0 +1,75 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const als = new AsyncLocalStorage();
3
+ let appContext = null;
4
+ let cached = null;
5
+ function detectPlatform() {
6
+ if (cached)
7
+ return cached;
8
+ const g = globalThis;
9
+ if (typeof g.Deno !== 'undefined') {
10
+ cached = 'deno';
11
+ return cached;
12
+ }
13
+ if (typeof g.Bun !== 'undefined') {
14
+ cached = 'bun';
15
+ return cached;
16
+ }
17
+ if (typeof g.navigator !== 'undefined' && g.navigator?.userAgent === 'Cloudflare-Workers') {
18
+ cached = 'cloudflare';
19
+ return cached;
20
+ }
21
+ if (typeof process !== 'undefined' && process.versions?.node) {
22
+ cached = 'node';
23
+ return cached;
24
+ }
25
+ if (typeof process !== 'undefined' && process.env) {
26
+ cached = 'vercel-edge';
27
+ return cached;
28
+ }
29
+ cached = 'node';
30
+ return cached;
31
+ }
32
+ function resetDetection() {
33
+ cached = null;
34
+ }
35
+ export { detectPlatform, resetDetection };
36
+ function defaultEnv() {
37
+ if (typeof process !== 'undefined' && process.env)
38
+ return process.env;
39
+ return {};
40
+ }
41
+ function union(a, b) {
42
+ return { ...a, ...b };
43
+ }
44
+ export const Platform = {
45
+ run(context, fn) {
46
+ const hasEnv = Object.keys(context.env).length > 0;
47
+ if (!hasEnv) {
48
+ console.warn('[Platform.run] env is empty — D1 bindings and other platform resources will not be available. ' +
49
+ 'Ensure Platform.setAppContext() is called before run(), or pass env in the context.');
50
+ }
51
+ return als.run(context, fn);
52
+ },
53
+ context() {
54
+ return als.getStore() ?? appContext ?? { platform: detectPlatform(), env: defaultEnv() };
55
+ },
56
+ env() {
57
+ const ctx = Platform.context();
58
+ if (als.getStore() && appContext) {
59
+ return union(appContext.env, ctx.env);
60
+ }
61
+ return ctx.env;
62
+ },
63
+ request() {
64
+ return Platform.context().request;
65
+ },
66
+ platform() {
67
+ return Platform.context().platform;
68
+ },
69
+ setAppContext(context) {
70
+ appContext = context;
71
+ },
72
+ clearAppContext() {
73
+ appContext = null;
74
+ },
75
+ };
@@ -0,0 +1,2 @@
1
+ export declare function resolveKey(key: string, source: Record<string, string | undefined>, prefixes?: string[]): string | undefined;
2
+ export declare function filterKeys(source: Record<string, string | undefined>, prefixes: string[]): Record<string, string>;
@@ -0,0 +1,22 @@
1
+ export function resolveKey(key, source, prefixes = []) {
2
+ if (key in source)
3
+ return source[key];
4
+ for (const prefix of prefixes) {
5
+ const prefixed = `${prefix}${key}`;
6
+ if (prefixed in source)
7
+ return source[prefixed];
8
+ }
9
+ return undefined;
10
+ }
11
+ export function filterKeys(source, prefixes) {
12
+ const result = {};
13
+ for (const key of Object.keys(source)) {
14
+ for (const prefix of prefixes) {
15
+ if (key.startsWith(prefix)) {
16
+ result[key] = source[key] ?? '';
17
+ break;
18
+ }
19
+ }
20
+ }
21
+ return result;
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { RuntimeContext } from './platform';
2
+ export declare function createNodeContext(env?: Record<string, string | undefined>, request?: Request): RuntimeContext;
3
+ export declare function createCloudflareContext(env: Record<string, string | undefined>, request?: Request, executionContext?: unknown): RuntimeContext;
@@ -0,0 +1,15 @@
1
+ export function createNodeContext(env, request) {
2
+ return {
3
+ platform: 'node',
4
+ env: env ?? (typeof process !== 'undefined' ? process.env : {}),
5
+ request,
6
+ };
7
+ }
8
+ export function createCloudflareContext(env, request, executionContext) {
9
+ return {
10
+ platform: 'cloudflare',
11
+ env,
12
+ request,
13
+ executionContext,
14
+ };
15
+ }
@@ -0,0 +1,14 @@
1
+ export { Platform, detectPlatform, resetDetection } from './config/platform';
2
+ export type { RuntimeContext } from './config/platform';
3
+ export { createNodeContext, createCloudflareContext } from './config/runtime-helpers';
4
+ export { ConfigError, ConfigTypeError, resolveBinding } from './utils/shared';
5
+ export { resolveKey, filterKeys } from './config/prefixes';
6
+ export { parseDotenv, loadEnv, loadEnvSync } from './config/dotenv';
7
+ export { Env, env, ENV_SYMBOL } from './config/env';
8
+ export type { EnvMode } from './config/env';
9
+ export { createEnv } from './config/create-env';
10
+ export type { CreateEnvOptions } from './config/create-env';
11
+ export { createLogger, Logger, createFileTransport } from './logger/logger';
12
+ export { LOGGER_SYMBOL, NOOP_LOGGER } from './logger/types';
13
+ export { ConsoleLogger } from './logger/console-logger';
14
+ export type { ILogger, LoggerOptions, LogLevel, PinoTransportTarget, FileTransportOptions, } from './logger/types';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // ── Config (env, platform, dotenv) ──
2
+ export { Platform, detectPlatform, resetDetection } from './config/platform';
3
+ export { createNodeContext, createCloudflareContext } from './config/runtime-helpers';
4
+ export { ConfigError, ConfigTypeError, resolveBinding } from './utils/shared';
5
+ export { resolveKey, filterKeys } from './config/prefixes';
6
+ export { parseDotenv, loadEnv, loadEnvSync } from './config/dotenv';
7
+ export { Env, env, ENV_SYMBOL } from './config/env';
8
+ export { createEnv } from './config/create-env';
9
+ // ── Logger ──
10
+ export { createLogger, Logger, createFileTransport } from './logger/logger';
11
+ export { LOGGER_SYMBOL, NOOP_LOGGER } from './logger/types';
12
+ export { ConsoleLogger } from './logger/console-logger';
@@ -0,0 +1,27 @@
1
+ import type { ILogger, LogLevel } from './types';
2
+ export declare class ConsoleLogger implements ILogger {
3
+ private name;
4
+ private level;
5
+ constructor(options?: {
6
+ name?: string;
7
+ level?: LogLevel;
8
+ });
9
+ private shouldLog;
10
+ private consoleMethod;
11
+ private formatMsg;
12
+ trace(msg: string, ...args: unknown[]): void;
13
+ trace(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
14
+ debug(msg: string, ...args: unknown[]): void;
15
+ debug(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
16
+ info(msg: string, ...args: unknown[]): void;
17
+ info(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
18
+ warn(msg: string, ...args: unknown[]): void;
19
+ warn(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
20
+ error(msg: string, ...args: unknown[]): void;
21
+ error(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
22
+ fatal(msg: string, ...args: unknown[]): void;
23
+ fatal(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
24
+ child(bindings: Record<string, unknown>): ILogger;
25
+ setLevel(level: LogLevel): void;
26
+ close(): Promise<void>;
27
+ }
@@ -0,0 +1,119 @@
1
+ const LOG_LEVELS = {
2
+ trace: 0,
3
+ debug: 1,
4
+ info: 2,
5
+ warn: 3,
6
+ error: 4,
7
+ fatal: 5,
8
+ };
9
+ export class ConsoleLogger {
10
+ name;
11
+ level = 'info';
12
+ constructor(options = {}) {
13
+ this.name = options.name ?? 'app';
14
+ if (options.level && LOG_LEVELS[options.level] !== undefined) {
15
+ this.level = options.level;
16
+ }
17
+ }
18
+ shouldLog(level) {
19
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
20
+ }
21
+ consoleMethod(level) {
22
+ if (level === 'fatal')
23
+ return 'error';
24
+ if (level === 'trace')
25
+ return 'debug';
26
+ return level;
27
+ }
28
+ formatMsg(level, msgOrObj, ...rest) {
29
+ const prefix = `[${this.name}] ${level}:`;
30
+ const method = this.consoleMethod(level);
31
+ const flatten = (v) => {
32
+ if (v instanceof Error)
33
+ return { message: v.message, stack: v.stack, cause: v.cause };
34
+ if (v && typeof v === 'object')
35
+ return v;
36
+ return v;
37
+ };
38
+ if (typeof msgOrObj === 'string') {
39
+ console[method](prefix, msgOrObj, ...rest.map(flatten));
40
+ }
41
+ else {
42
+ const [msg, ...args] = rest;
43
+ console[method](prefix, msg ?? '', flatten(msgOrObj), ...args.map(flatten));
44
+ }
45
+ }
46
+ trace(msgOrObj, msg, ...args) {
47
+ if (!this.shouldLog('trace'))
48
+ return;
49
+ if (typeof msgOrObj === 'string') {
50
+ this.formatMsg('trace', msgOrObj, ...args);
51
+ }
52
+ else {
53
+ this.formatMsg('trace', msgOrObj, msg, ...args);
54
+ }
55
+ }
56
+ debug(msgOrObj, msg, ...args) {
57
+ if (!this.shouldLog('debug'))
58
+ return;
59
+ if (typeof msgOrObj === 'string') {
60
+ this.formatMsg('debug', msgOrObj, ...args);
61
+ }
62
+ else {
63
+ this.formatMsg('debug', msgOrObj, msg, ...args);
64
+ }
65
+ }
66
+ info(msgOrObj, msg, ...args) {
67
+ if (!this.shouldLog('info'))
68
+ return;
69
+ if (typeof msgOrObj === 'string') {
70
+ this.formatMsg('info', msgOrObj, ...args);
71
+ }
72
+ else {
73
+ this.formatMsg('info', msgOrObj, msg, ...args);
74
+ }
75
+ }
76
+ warn(msgOrObj, msg, ...args) {
77
+ if (!this.shouldLog('warn'))
78
+ return;
79
+ if (typeof msgOrObj === 'string') {
80
+ this.formatMsg('warn', msgOrObj, ...args);
81
+ }
82
+ else {
83
+ this.formatMsg('warn', msgOrObj, msg, ...args);
84
+ }
85
+ }
86
+ error(msgOrObj, msg, ...args) {
87
+ if (!this.shouldLog('error'))
88
+ return;
89
+ if (typeof msgOrObj === 'string') {
90
+ this.formatMsg('error', msgOrObj, ...args);
91
+ }
92
+ else {
93
+ this.formatMsg('error', msgOrObj, msg, ...args);
94
+ }
95
+ }
96
+ fatal(msgOrObj, msg, ...args) {
97
+ if (!this.shouldLog('fatal'))
98
+ return;
99
+ if (typeof msgOrObj === 'string') {
100
+ this.formatMsg('fatal', msgOrObj, ...args);
101
+ }
102
+ else {
103
+ this.formatMsg('fatal', msgOrObj, msg, ...args);
104
+ }
105
+ }
106
+ child(bindings) {
107
+ const module = bindings.module;
108
+ return new ConsoleLogger({
109
+ name: module ? `${this.name}:${module}` : this.name,
110
+ level: this.level,
111
+ });
112
+ }
113
+ setLevel(level) {
114
+ this.level = level;
115
+ }
116
+ close() {
117
+ return Promise.resolve();
118
+ }
119
+ }
@@ -0,0 +1,22 @@
1
+ import type { ILogger, LoggerOptions, LogLevel, FileTransportOptions, PinoTransportTarget } from './types';
2
+ export declare class Logger implements ILogger {
3
+ private impl;
4
+ constructor(options?: LoggerOptions);
5
+ trace(msg: string, ...args: unknown[]): void;
6
+ trace(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
7
+ debug(msg: string, ...args: unknown[]): void;
8
+ debug(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
9
+ info(msg: string, ...args: unknown[]): void;
10
+ info(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
11
+ warn(msg: string, ...args: unknown[]): void;
12
+ warn(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
13
+ error(msg: string, ...args: unknown[]): void;
14
+ error(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
15
+ fatal(msg: string, ...args: unknown[]): void;
16
+ fatal(obj: Record<string, unknown>, msg?: string, ...args: unknown[]): void;
17
+ child(bindings: Record<string, unknown>): ILogger;
18
+ setLevel(level: LogLevel): void;
19
+ close(): Promise<void>;
20
+ }
21
+ export declare function createFileTransport(options: FileTransportOptions): PinoTransportTarget;
22
+ export declare function createLogger(options?: LoggerOptions): Logger;
@@ -0,0 +1,148 @@
1
+ import { ConsoleLogger } from './console-logger';
2
+ const isEdgeRuntime = typeof process === 'undefined' || typeof process.stdout === 'undefined';
3
+ export class Logger {
4
+ impl;
5
+ constructor(options = {}) {
6
+ if (isEdgeRuntime) {
7
+ this.impl = new ConsoleLogger({ name: options.name, level: options.level });
8
+ }
9
+ else {
10
+ this.impl = new LazyPinoLogger(options);
11
+ }
12
+ }
13
+ trace(msgOrObj, msg, ...args) {
14
+ this.impl.trace(msgOrObj, msg, ...args);
15
+ }
16
+ debug(msgOrObj, msg, ...args) {
17
+ this.impl.debug(msgOrObj, msg, ...args);
18
+ }
19
+ info(msgOrObj, msg, ...args) {
20
+ this.impl.info(msgOrObj, msg, ...args);
21
+ }
22
+ warn(msgOrObj, msg, ...args) {
23
+ this.impl.warn(msgOrObj, msg, ...args);
24
+ }
25
+ error(msgOrObj, msg, ...args) {
26
+ this.impl.error(msgOrObj, msg, ...args);
27
+ }
28
+ fatal(msgOrObj, msg, ...args) {
29
+ this.impl.fatal(msgOrObj, msg, ...args);
30
+ }
31
+ child(bindings) {
32
+ return this.impl.child(bindings);
33
+ }
34
+ setLevel(level) {
35
+ this.impl.setLevel(level);
36
+ }
37
+ close() {
38
+ return this.impl.close();
39
+ }
40
+ }
41
+ class LazyPinoLogger {
42
+ impl;
43
+ options;
44
+ constructor(options = {}) {
45
+ this.options = options;
46
+ this.impl = new ConsoleLogger({ name: options.name, level: options.level });
47
+ this.tryUpgrade();
48
+ }
49
+ async tryUpgrade() {
50
+ try {
51
+ const name = 'pino';
52
+ const pino = await import(name);
53
+ const transportOpts = this.options.transport;
54
+ const targets = [];
55
+ if (transportOpts) {
56
+ const list = Array.isArray(transportOpts) ? transportOpts : [transportOpts];
57
+ targets.push(...list);
58
+ }
59
+ if (this.options.destination) {
60
+ targets.push({
61
+ target: 'pino/file',
62
+ options: { destination: this.options.destination },
63
+ });
64
+ }
65
+ let stream;
66
+ if (targets.length > 0) {
67
+ stream = pino.transport({ targets });
68
+ }
69
+ const instance = pino({
70
+ name: this.options.name ?? 'app',
71
+ level: this.options.level ?? 'info',
72
+ ...this.options.pino,
73
+ }, stream);
74
+ this.impl = pinoInstanceToLogger(instance);
75
+ }
76
+ catch {
77
+ // Keep ConsoleLogger as fallback
78
+ }
79
+ }
80
+ trace(msgOrObj, msg, ...args) {
81
+ this.impl.trace(msgOrObj, msg, ...args);
82
+ }
83
+ debug(msgOrObj, msg, ...args) {
84
+ this.impl.debug(msgOrObj, msg, ...args);
85
+ }
86
+ info(msgOrObj, msg, ...args) {
87
+ this.impl.info(msgOrObj, msg, ...args);
88
+ }
89
+ warn(msgOrObj, msg, ...args) {
90
+ this.impl.warn(msgOrObj, msg, ...args);
91
+ }
92
+ error(msgOrObj, msg, ...args) {
93
+ this.impl.error(msgOrObj, msg, ...args);
94
+ }
95
+ fatal(msgOrObj, msg, ...args) {
96
+ this.impl.fatal(msgOrObj, msg, ...args);
97
+ }
98
+ child(bindings) {
99
+ return this.impl.child(bindings);
100
+ }
101
+ setLevel(level) {
102
+ this.impl.setLevel(level);
103
+ }
104
+ close() {
105
+ return this.impl.close();
106
+ }
107
+ }
108
+ function pinoInstanceToLogger(instance) {
109
+ const wrap = (method) => {
110
+ return (msgOrObj, msg, ...args) => {
111
+ method.call(instance, msgOrObj, msg, ...args);
112
+ };
113
+ };
114
+ return {
115
+ trace: wrap(instance.trace),
116
+ debug: wrap(instance.debug),
117
+ info: wrap(instance.info),
118
+ warn: wrap(instance.warn),
119
+ error: wrap(instance.error),
120
+ fatal: wrap(instance.fatal),
121
+ child(bindings) {
122
+ return pinoInstanceToLogger(instance.child(bindings));
123
+ },
124
+ setLevel(level) {
125
+ instance.level = level;
126
+ },
127
+ async close() {
128
+ await instance.flush();
129
+ },
130
+ };
131
+ }
132
+ export function createFileTransport(options) {
133
+ return {
134
+ target: 'pino-roll',
135
+ options: {
136
+ file: options.file,
137
+ ...(options.size && { size: options.size }),
138
+ ...(options.frequency && { frequency: options.frequency }),
139
+ ...(options.limit && { limit: { count: options.limit } }),
140
+ ...(options.mkdir !== undefined && { mkdir: options.mkdir }),
141
+ ...(options.dateFormat && { dateFormat: options.dateFormat }),
142
+ ...(options.symlink !== undefined && { symlink: options.symlink }),
143
+ },
144
+ };
145
+ }
146
+ export function createLogger(options) {
147
+ return new Logger(options);
148
+ }
@@ -0,0 +1,49 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'trace';
2
+ export interface ILogger {
3
+ trace(msg: string, ...args: any[]): void;
4
+ trace(obj: Record<string, unknown>, msg?: string, ...args: any[]): void;
5
+ debug(msg: string, ...args: any[]): void;
6
+ debug(obj: Record<string, unknown>, msg?: string, ...args: any[]): void;
7
+ info(msg: string, ...args: any[]): void;
8
+ info(obj: Record<string, unknown>, msg?: string, ...args: any[]): void;
9
+ warn(msg: string, ...args: any[]): void;
10
+ warn(obj: Record<string, unknown>, msg?: string, ...args: any[]): void;
11
+ error(msg: string, ...args: any[]): void;
12
+ error(obj: Record<string, unknown>, msg?: string, ...args: any[]): void;
13
+ fatal(msg: string, ...args: any[]): void;
14
+ fatal(obj: Record<string, unknown>, msg?: string, ...args: any[]): void;
15
+ child(bindings: Record<string, unknown>): ILogger;
16
+ setLevel(level: LogLevel): void;
17
+ close(): Promise<void>;
18
+ }
19
+ export interface PinoTransportTarget {
20
+ target: string;
21
+ options?: Record<string, unknown>;
22
+ level?: LogLevel;
23
+ }
24
+ export declare const LOGGER_SYMBOL: unique symbol;
25
+ export declare const NOOP_LOGGER: ILogger;
26
+ export interface LoggerOptions {
27
+ name?: string;
28
+ level?: LogLevel;
29
+ transport?: PinoTransportTarget | PinoTransportTarget[];
30
+ pino?: Record<string, unknown>;
31
+ /** File path to write logs. Only works on Node/Bun (uses pino/file transport). */
32
+ destination?: string;
33
+ }
34
+ export interface FileTransportOptions {
35
+ /** Base log file path (e.g. 'logs/app'). pino-roll appends rotation numbers. */
36
+ file: string;
37
+ /** Max file size before rotation. Accepts '10m', '1g', etc. */
38
+ size?: string;
39
+ /** Time-based rotation: 'daily', 'hourly', or ms number. */
40
+ frequency?: string | number;
41
+ /** Max rotated files to keep (in addition to active file). */
42
+ limit?: number;
43
+ /** Create parent directories if they don't exist. */
44
+ mkdir?: boolean;
45
+ /** Date format string for rotated filenames (date-fns format). */
46
+ dateFormat?: string;
47
+ /** Maintain a 'current.log' symlink. */
48
+ symlink?: boolean;
49
+ }
@@ -0,0 +1,16 @@
1
+ export const LOGGER_SYMBOL = Symbol('app:logger');
2
+ export const NOOP_LOGGER = {
3
+ trace() { },
4
+ debug() { },
5
+ info() { },
6
+ warn() { },
7
+ error() { },
8
+ fatal() { },
9
+ child() {
10
+ return NOOP_LOGGER;
11
+ },
12
+ setLevel() { },
13
+ close() {
14
+ return Promise.resolve();
15
+ },
16
+ };
@@ -0,0 +1,12 @@
1
+ import type { Env } from '../config/env';
2
+ export declare class ConfigError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare class ConfigTypeError extends ConfigError {
6
+ constructor(key: string, expected: string, value: string);
7
+ }
8
+ export declare function assertString(key: string, value: string | undefined): string;
9
+ export declare function assertNumber(key: string, value: string | undefined): number;
10
+ export declare function assertBoolean(key: string, value: string | undefined): boolean;
11
+ export declare function assertUrl(key: string, value: string | undefined): URL;
12
+ export declare function resolveBinding(name: string, env?: Env): any;
@@ -0,0 +1,52 @@
1
+ import { Platform } from '../config/platform';
2
+ export class ConfigError extends Error {
3
+ constructor(message) {
4
+ super(`[packages-core] ${message}`);
5
+ this.name = 'ConfigError';
6
+ }
7
+ }
8
+ export class ConfigTypeError extends ConfigError {
9
+ constructor(key, expected, value) {
10
+ super(`Invalid type for "${key}": expected ${expected}, got "${value}"`);
11
+ this.name = 'ConfigTypeError';
12
+ }
13
+ }
14
+ export function assertString(key, value) {
15
+ if (value === undefined)
16
+ throw new ConfigError(`Missing required config: ${key}`);
17
+ return value;
18
+ }
19
+ export function assertNumber(key, value) {
20
+ if (value === undefined)
21
+ throw new ConfigError(`Missing required config: ${key}`);
22
+ const n = Number(value);
23
+ if (Number.isNaN(n))
24
+ throw new ConfigTypeError(key, 'number', value);
25
+ return n;
26
+ }
27
+ export function assertBoolean(key, value) {
28
+ if (value === undefined)
29
+ throw new ConfigError(`Missing required config: ${key}`);
30
+ if (value === 'true' || value === '1')
31
+ return true;
32
+ if (value === 'false' || value === '0')
33
+ return false;
34
+ throw new ConfigTypeError(key, 'boolean', value);
35
+ }
36
+ export function assertUrl(key, value) {
37
+ if (value === undefined)
38
+ throw new ConfigError(`Missing required config: ${key}`);
39
+ try {
40
+ return new URL(value);
41
+ }
42
+ catch {
43
+ throw new ConfigTypeError(key, 'URL', value);
44
+ }
45
+ }
46
+ export function resolveBinding(name, env) {
47
+ return (globalThis[name] ??
48
+ globalThis.env?.[name] ??
49
+ (typeof process !== 'undefined' ? process.env?.[name] : undefined) ??
50
+ Platform.env()?.[name] ??
51
+ env?.get(name));
52
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@rx-ted/packages-core",
3
+ "version": "1.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Unified runtime context, environment config, and logging for multi-platform TypeScript.",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "author": "rx-ted",
14
+ "license": "MIT",
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "test:coverage": "vitest run --coverage",
20
+ "typecheck": "tsc -p tsconfig.json --noEmit",
21
+ "demo": "tsx demo/demo.ts"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "keywords": [
27
+ "environment",
28
+ "runtime",
29
+ "als",
30
+ "bun",
31
+ "node",
32
+ "deno",
33
+ "cloudflare",
34
+ "vercel-edge",
35
+ "logging",
36
+ "typescript",
37
+ "zod"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@types/node": "^25.9.1",
44
+ "pino": "^9.6.0"
45
+ },
46
+ "optionalDependencies": {
47
+ "pino-pretty": "^13.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "zod": "^3.0.0 || ^4.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@vitest/coverage-v8": "^4.1.7",
54
+ "typescript": "^6.0.3",
55
+ "vitest": "^4.1.7",
56
+ "zod": "^4.0.0"
57
+ }
58
+ }