@ressjs/config 0.6.0-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * La configuración del proyecto, resuelta.
3
+ *
4
+ * Un archivo por modo en `config/`, cada uno exportando por defecto un objeto:
5
+ * las claves que el framework reconoce —`basePath`, `redirects`, `dev`— y las
6
+ * del proyecto —una URL de API, una clave— en el mismo lugar. Se fusionan
7
+ * `default` → el modo activo → `local`.
8
+ *
9
+ * De sólo servidor. Importarlo desde código que termina en el bundle del cliente
10
+ * falla el build, no la petición.
11
+ */
12
+ import { type ProjectConfig } from './load';
13
+ /**
14
+ * La configuración resuelta.
15
+ *
16
+ * Es un `Proxy` y no una constante porque una constante no se puede reasignar:
17
+ * quien capturara el binding antes de la resolución se quedaría con `undefined`
18
+ * para siempre, y `cdnPrefix: config.CDN_URL` dentro de `ress.config.ts`
19
+ * fallaría en silencio. Cada lectura consulta lo ya resuelto.
20
+ */
21
+ export declare const config: ProjectConfig;
22
+ export { invalidateConfig, resolveConfig, resolved } from './load';
23
+ export type { ProjectConfig } from './load';
24
+ export { resolveLayers } from './layers';
25
+ export { deepFreeze, deepMerge } from './merge';
26
+ export { CONFIG_EXTENSIONS, layerNamesFor, RESERVED_LAYERS, reservedKeysFor } from './types';
27
+ export type { LayerSource } from './types';
28
+ export { configServerOnlyGuard, isConfigModule } from './vite-guard';
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * La configuración del proyecto, resuelta.
4
+ *
5
+ * Un archivo por modo en `config/`, cada uno exportando por defecto un objeto:
6
+ * las claves que el framework reconoce —`basePath`, `redirects`, `dev`— y las
7
+ * del proyecto —una URL de API, una clave— en el mismo lugar. Se fusionan
8
+ * `default` → el modo activo → `local`.
9
+ *
10
+ * De sólo servidor. Importarlo desde código que termina en el bundle del cliente
11
+ * falla el build, no la petición.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.isConfigModule = exports.configServerOnlyGuard = exports.reservedKeysFor = exports.RESERVED_LAYERS = exports.layerNamesFor = exports.CONFIG_EXTENSIONS = exports.deepMerge = exports.deepFreeze = exports.resolveLayers = exports.resolved = exports.resolveConfig = exports.invalidateConfig = exports.config = void 0;
15
+ const load_1 = require("./load");
16
+ /**
17
+ * La configuración resuelta.
18
+ *
19
+ * Es un `Proxy` y no una constante porque una constante no se puede reasignar:
20
+ * quien capturara el binding antes de la resolución se quedaría con `undefined`
21
+ * para siempre, y `cdnPrefix: config.CDN_URL` dentro de `ress.config.ts`
22
+ * fallaría en silencio. Cada lectura consulta lo ya resuelto.
23
+ */
24
+ exports.config = new Proxy({}, {
25
+ get: (_target, key) => (0, load_1.resolved)()[key],
26
+ has: (_target, key) => key in (0, load_1.resolved)(),
27
+ ownKeys: () => Reflect.ownKeys((0, load_1.resolved)()),
28
+ getOwnPropertyDescriptor: (_target, key) => {
29
+ const descriptor = Reflect.getOwnPropertyDescriptor((0, load_1.resolved)(), key);
30
+ // Un proxy tiene que declarar configurable lo que su objetivo no tiene.
31
+ return descriptor && { ...descriptor, configurable: true };
32
+ },
33
+ });
34
+ var load_2 = require("./load");
35
+ Object.defineProperty(exports, "invalidateConfig", { enumerable: true, get: function () { return load_2.invalidateConfig; } });
36
+ Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return load_2.resolveConfig; } });
37
+ Object.defineProperty(exports, "resolved", { enumerable: true, get: function () { return load_2.resolved; } });
38
+ var layers_1 = require("./layers");
39
+ Object.defineProperty(exports, "resolveLayers", { enumerable: true, get: function () { return layers_1.resolveLayers; } });
40
+ var merge_1 = require("./merge");
41
+ Object.defineProperty(exports, "deepFreeze", { enumerable: true, get: function () { return merge_1.deepFreeze; } });
42
+ Object.defineProperty(exports, "deepMerge", { enumerable: true, get: function () { return merge_1.deepMerge; } });
43
+ var types_1 = require("./types");
44
+ Object.defineProperty(exports, "CONFIG_EXTENSIONS", { enumerable: true, get: function () { return types_1.CONFIG_EXTENSIONS; } });
45
+ Object.defineProperty(exports, "layerNamesFor", { enumerable: true, get: function () { return types_1.layerNamesFor; } });
46
+ Object.defineProperty(exports, "RESERVED_LAYERS", { enumerable: true, get: function () { return types_1.RESERVED_LAYERS; } });
47
+ Object.defineProperty(exports, "reservedKeysFor", { enumerable: true, get: function () { return types_1.reservedKeysFor; } });
48
+ var vite_guard_1 = require("./vite-guard");
49
+ Object.defineProperty(exports, "configServerOnlyGuard", { enumerable: true, get: function () { return vite_guard_1.configServerOnlyGuard; } });
50
+ Object.defineProperty(exports, "isConfigModule", { enumerable: true, get: function () { return vite_guard_1.isConfigModule; } });
@@ -0,0 +1,20 @@
1
+ import { type LayerSource } from './types';
2
+ /**
3
+ * Resuelve una superficie de configuración a partir de sus capas.
4
+ *
5
+ * Un solo motor para las dos superficies. Tener dos implementaciones —una para
6
+ * el comportamiento del framework y otra para los valores por entorno— habría
7
+ * significado dos reglas de fusión que divergen en cuanto alguien toque una.
8
+ *
9
+ * `object` y no `Record<string, unknown>`: una interfaz sin firma de índice no
10
+ * satisface esa restricción, y `resolveLayers<RessConfig>` no compilaría.
11
+ */
12
+ export declare function resolveLayers<T extends object>(source: LayerSource): Promise<T>;
13
+ /**
14
+ * El archivo de una capa, si existe.
15
+ *
16
+ * Dos archivos que compiten por la misma capa son un error: cuál gana dependería
17
+ * del orden en que se busque, y ese orden es una decisión interna que nadie
18
+ * puede ver desde afuera.
19
+ */
20
+ export declare function findConfigFile(dir: string, name: string): Promise<string | undefined>;
package/dist/layers.js ADDED
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveLayers = resolveLayers;
7
+ exports.findConfigFile = findConfigFile;
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_url_1 = require("node:url");
11
+ const merge_1 = require("./merge");
12
+ const types_1 = require("./types");
13
+ /**
14
+ * Resuelve una superficie de configuración a partir de sus capas.
15
+ *
16
+ * Un solo motor para las dos superficies. Tener dos implementaciones —una para
17
+ * el comportamiento del framework y otra para los valores por entorno— habría
18
+ * significado dos reglas de fusión que divergen en cuanto alguien toque una.
19
+ *
20
+ * `object` y no `Record<string, unknown>`: una interfaz sin firma de índice no
21
+ * satisface esa restricción, y `resolveLayers<RessConfig>` no compilaría.
22
+ */
23
+ async function resolveLayers(source) {
24
+ const dir = await findDirectory(source.root, source.base);
25
+ const file = await findConfigFile(source.root, source.base);
26
+ if (dir && file) {
27
+ throw new Error(`[ress] "${source.base}.*" y "${source.base}/" no pueden coexistir: ` +
28
+ 'no hay forma de saber cuál gana. Dejá una sola.');
29
+ }
30
+ if (dir)
31
+ return (0, merge_1.deepFreeze)(await resolveFolder(dir, source));
32
+ if (file)
33
+ return (0, merge_1.deepFreeze)(await resolveSingleFile(file, source));
34
+ // Ninguna de las dos formas existe. La superficie entera es opcional: un
35
+ // proyecto sin configuración es un proyecto válido.
36
+ return (0, merge_1.deepFreeze)({});
37
+ }
38
+ /** Forma de carpeta: un archivo físico por capa. */
39
+ async function resolveFolder(dir, source) {
40
+ let result = {};
41
+ for (const layer of source.layerNames) {
42
+ const layerFile = await findConfigFile(dir, layer);
43
+ if (!layerFile) {
44
+ if (layer === 'default' && source.requireDefault) {
45
+ throw new Error(`[ress] "${source.base}/" existe pero no tiene "${source.base}/default.*". ` +
46
+ 'Una carpeta con sólo las capas de un modo casi siempre es un archivo a ' +
47
+ 'medio renombrar.');
48
+ }
49
+ continue;
50
+ }
51
+ result = (0, merge_1.deepMerge)(result, await evaluate(layerFile));
52
+ }
53
+ return result;
54
+ }
55
+ /** Forma de archivo único: la raíz es `default` y cada modo es una clave anidada. */
56
+ async function resolveSingleFile(file, source) {
57
+ const whole = (await evaluate(file));
58
+ // Se sacan del resultado **todas** las claves de capa, no sólo las del modo
59
+ // activo: con un trío fijo, `--mode staging` no fusionaba nada y dejaba
60
+ // `staging` adentro; sacando sólo las del modo activo, en desarrollo quedaba
61
+ // `production: { ... }` colada como si fuera una sección de configuración.
62
+ const reserved = (0, types_1.reservedKeysFor)(source.mode);
63
+ let result = Object.fromEntries(Object.entries(whole).filter(([key]) => !reserved.has(key)));
64
+ for (const layer of source.layerNames) {
65
+ if (layer === 'default')
66
+ continue;
67
+ const override = whole[layer];
68
+ if (override != null)
69
+ result = (0, merge_1.deepMerge)(result, override);
70
+ }
71
+ return result;
72
+ }
73
+ /**
74
+ * El archivo de una capa, si existe.
75
+ *
76
+ * Dos archivos que compiten por la misma capa son un error: cuál gana dependería
77
+ * del orden en que se busque, y ese orden es una decisión interna que nadie
78
+ * puede ver desde afuera.
79
+ */
80
+ async function findConfigFile(dir, name) {
81
+ const entries = await safeReaddir(dir);
82
+ const matches = types_1.CONFIG_EXTENSIONS.map((ext) => name + ext).filter((f) => entries.includes(f));
83
+ if (matches.length > 1) {
84
+ throw new Error(`[ress] Hay más de un "${name}" en ${dir}: ${matches.join(', ')}. Dejá uno solo.`);
85
+ }
86
+ return matches[0] ? node_path_1.default.join(dir, matches[0]) : undefined;
87
+ }
88
+ async function findDirectory(root, base) {
89
+ const target = node_path_1.default.join(root, base);
90
+ const entries = await safeReaddir(root, true);
91
+ return entries.some((e) => e.name === base && e.isDirectory()) ? target : undefined;
92
+ }
93
+ async function safeReaddir(dir, withTypes) {
94
+ try {
95
+ return withTypes ? await (0, promises_1.readdir)(dir, { withFileTypes: true }) : await (0, promises_1.readdir)(dir);
96
+ }
97
+ catch {
98
+ // Un directorio que no existe no es un error: significa que el proyecto no
99
+ // usa esa forma de configuración.
100
+ return [];
101
+ }
102
+ }
103
+ /**
104
+ * Evalúa un archivo de configuración.
105
+ *
106
+ * En desarrollo el archivo cambia, así que la URL lleva una marca de tiempo: sin
107
+ * ella el caché de módulos de Node devuelve la versión anterior y editar la
108
+ * configuración no tiene ningún efecto.
109
+ */
110
+ async function evaluate(file) {
111
+ const url = (0, node_url_1.pathToFileURL)(file).href + `?t=${Date.now()}`;
112
+ const module = (await import(/* @vite-ignore */ url));
113
+ return module.default ?? module;
114
+ }
package/dist/load.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * La forma de la configuración del proyecto.
3
+ *
4
+ * Vacía a propósito: la forma la definen los archivos de `config/`, y el `.d.ts`
5
+ * generado la amplía para que el editor la conozca sin escribirla a mano.
6
+ */
7
+ export interface ProjectConfig {
8
+ }
9
+ /**
10
+ * No es `async`: una función `async` envuelve su retorno en una promesa nueva
11
+ * cada vez, así que quien pidiera lo mismo dos veces recibiría dos promesas
12
+ * distintas aunque por dentro no se vuelva a leer nada.
13
+ */
14
+ export declare function resolveConfig(root: string, mode: string): Promise<ProjectConfig>;
15
+ /** Descarta lo resuelto. Lo usa la recarga en caliente al cambiar una capa. */
16
+ export declare function invalidateConfig(): void;
17
+ /**
18
+ * Lo ya resuelto, o un error que dice qué falta.
19
+ *
20
+ * Devolver un objeto vacío acá convertiría un error de orden de arranque en un
21
+ * `undefined` que aparece mucho más tarde y en otro lugar.
22
+ */
23
+ export declare function resolved(): ProjectConfig;
package/dist/load.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveConfig = resolveConfig;
4
+ exports.invalidateConfig = invalidateConfig;
5
+ exports.resolved = resolved;
6
+ const layers_1 = require("./layers");
7
+ const types_1 = require("./types");
8
+ /**
9
+ * Lo resuelto vive en `globalThis`, no en una variable de módulo.
10
+ *
11
+ * Los archivos de `config/` se evalúan dentro del cargador de Vite, que los
12
+ * empaqueta en su propio grafo: la instancia de este paquete que alcanzan puede
13
+ * no ser la misma que resolvió el servidor. Con estado de módulo, `config`
14
+ * llegaría vacío justo donde se lo lee.
15
+ */
16
+ const CACHE = Symbol.for('ressjs.config.resolved');
17
+ function store() {
18
+ const g = globalThis;
19
+ return (g[CACHE] ??= { pending: new Map() });
20
+ }
21
+ /**
22
+ * No es `async`: una función `async` envuelve su retorno en una promesa nueva
23
+ * cada vez, así que quien pidiera lo mismo dos veces recibiría dos promesas
24
+ * distintas aunque por dentro no se vuelva a leer nada.
25
+ */
26
+ function resolveConfig(root, mode) {
27
+ // La clave incluye raíz y modo: sin ella, en un monorepo la segunda aplicación
28
+ // recibía los valores de la primera, y un reinicio con otro modo seguía
29
+ // sirviendo los del anterior.
30
+ const key = `${root} ${mode}`;
31
+ const cached = store().pending.get(key);
32
+ if (cached)
33
+ return cached;
34
+ const pending = (0, layers_1.resolveLayers)({
35
+ root,
36
+ base: 'config',
37
+ mode,
38
+ layerNames: (0, types_1.layerNamesFor)(mode),
39
+ requireDefault: false,
40
+ }).then((value) => {
41
+ store().resolved = value;
42
+ return value;
43
+ });
44
+ store().pending.set(key, pending);
45
+ return pending;
46
+ }
47
+ /** Descarta lo resuelto. Lo usa la recarga en caliente al cambiar una capa. */
48
+ function invalidateConfig() {
49
+ const s = store();
50
+ s.pending.clear();
51
+ delete s.resolved;
52
+ }
53
+ /**
54
+ * Lo ya resuelto, o un error que dice qué falta.
55
+ *
56
+ * Devolver un objeto vacío acá convertiría un error de orden de arranque en un
57
+ * `undefined` que aparece mucho más tarde y en otro lugar.
58
+ */
59
+ function resolved() {
60
+ const value = store().resolved;
61
+ if (!value) {
62
+ throw new Error('[ress] Se leyó `config` antes de que la configuración estuviera resuelta. ' +
63
+ 'Pasa cuando un módulo que se evalúa antes de arrancar el servidor importa ' +
64
+ '`@ressjs/config`; movelo adentro de la función que lo necesita.');
65
+ }
66
+ return value;
67
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Fusión de capas.
3
+ *
4
+ * Los objetos se fusionan en profundidad y los arreglos se reemplazan enteros.
5
+ * Fusionar arreglos no tiene una interpretación única —¿por índice, por
6
+ * concatenación, por identidad?— y cualquiera de las tres sorprende a alguien;
7
+ * reemplazar es la única regla que se puede predecir sin leer el código.
8
+ */
9
+ export declare function deepMerge<T>(base: T, override: unknown): T;
10
+ /** Congela en profundidad: el resultado se comparte entre todo el proceso. */
11
+ export declare function deepFreeze<T>(value: T): T;
package/dist/merge.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deepMerge = deepMerge;
4
+ exports.deepFreeze = deepFreeze;
5
+ /**
6
+ * Fusión de capas.
7
+ *
8
+ * Los objetos se fusionan en profundidad y los arreglos se reemplazan enteros.
9
+ * Fusionar arreglos no tiene una interpretación única —¿por índice, por
10
+ * concatenación, por identidad?— y cualquiera de las tres sorprende a alguien;
11
+ * reemplazar es la única regla que se puede predecir sin leer el código.
12
+ */
13
+ function deepMerge(base, override) {
14
+ if (!isPlainObject(override))
15
+ return (override ?? base);
16
+ if (!isPlainObject(base))
17
+ return { ...override };
18
+ const result = { ...base };
19
+ for (const [key, value] of Object.entries(override)) {
20
+ // `undefined` no borra: una capa que no menciona una clave no la está
21
+ // desactivando, simplemente no opina sobre ella. Para desactivar se declara
22
+ // `null` o el valor vacío que corresponda.
23
+ if (value === undefined)
24
+ continue;
25
+ result[key] = isPlainObject(value) ? deepMerge(result[key], value) : value;
26
+ }
27
+ return result;
28
+ }
29
+ /**
30
+ * Un objeto literal, no una instancia.
31
+ *
32
+ * Un `Date`, un `RegExp` o una clase se reemplazan enteros: fusionar sus
33
+ * propiedades produciría un objeto que ya no es de esa clase.
34
+ */
35
+ function isPlainObject(value) {
36
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
37
+ return false;
38
+ const proto = Object.getPrototypeOf(value);
39
+ return proto === Object.prototype || proto === null;
40
+ }
41
+ /** Congela en profundidad: el resultado se comparte entre todo el proceso. */
42
+ function deepFreeze(value) {
43
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
44
+ Object.freeze(value);
45
+ for (const key of Object.getOwnPropertyNames(value)) {
46
+ deepFreeze(value[key]);
47
+ }
48
+ }
49
+ return value;
50
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Qué capas componen una superficie de configuración y de dónde salen.
3
+ *
4
+ * Hay dos superficies y un solo motor: el **comportamiento** del framework
5
+ * (`ress.config`) y los **valores** por entorno (`config`). Las dos aceptan la
6
+ * forma de archivo único y la de carpeta, y las dos se resuelven igual.
7
+ */
8
+ export interface LayerSource {
9
+ root: string;
10
+ /** `ress.config` o `config`. */
11
+ base: string;
12
+ mode: string;
13
+ /** Orden de precedencia, de menor a mayor: `['default', mode, 'local']`. */
14
+ layerNames: readonly string[];
15
+ /**
16
+ * Si la forma de carpeta exige su capa `default`.
17
+ *
18
+ * La superficie entera sigue siendo opcional: lo que esto declara es que, si
19
+ * alguien creó la carpeta, tiene que tener su base. Una carpeta con sólo
20
+ * `production.ts` casi siempre es un archivo a medio renombrar.
21
+ */
22
+ requireDefault: boolean;
23
+ }
24
+ /** Extensiones que puede tener un archivo de configuración. */
25
+ export declare const CONFIG_EXTENSIONS: readonly [".ts", ".mts", ".js", ".mjs"];
26
+ /**
27
+ * Nombres que el framework reserva como capas.
28
+ *
29
+ * Una clave con uno de estos nombres nunca es una sección de configuración, y por
30
+ * eso se saca del resultado aunque su modo no sea el activo: dejarla adentro
31
+ * colaba `production: { ... }` como si fuera una sección más, en desarrollo.
32
+ */
33
+ export declare const RESERVED_LAYERS: readonly ["development", "production", "local"];
34
+ /** Las capas de una superficie, dadas el modo activo. */
35
+ export declare function layerNamesFor(mode: string): readonly string[];
36
+ /** Toda clave que el motor tiene que sacar del resultado en la forma de archivo único. */
37
+ export declare function reservedKeysFor(mode: string): ReadonlySet<string>;
package/dist/types.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RESERVED_LAYERS = exports.CONFIG_EXTENSIONS = void 0;
4
+ exports.layerNamesFor = layerNamesFor;
5
+ exports.reservedKeysFor = reservedKeysFor;
6
+ /** Extensiones que puede tener un archivo de configuración. */
7
+ exports.CONFIG_EXTENSIONS = ['.ts', '.mts', '.js', '.mjs'];
8
+ /**
9
+ * Nombres que el framework reserva como capas.
10
+ *
11
+ * Una clave con uno de estos nombres nunca es una sección de configuración, y por
12
+ * eso se saca del resultado aunque su modo no sea el activo: dejarla adentro
13
+ * colaba `production: { ... }` como si fuera una sección más, en desarrollo.
14
+ */
15
+ exports.RESERVED_LAYERS = ['development', 'production', 'local'];
16
+ /** Las capas de una superficie, dadas el modo activo. */
17
+ function layerNamesFor(mode) {
18
+ return ['default', mode, 'local'];
19
+ }
20
+ /** Toda clave que el motor tiene que sacar del resultado en la forma de archivo único. */
21
+ function reservedKeysFor(mode) {
22
+ return new Set([...exports.RESERVED_LAYERS, mode]);
23
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Impide que los valores de servidor lleguen al bundle del cliente.
3
+ *
4
+ * Este paquete lee archivos que pueden tener credenciales. Si un componente lo
5
+ * importa —aunque sea sin querer, a través de un módulo compartido— esos valores
6
+ * terminan en un archivo que cualquiera puede descargar.
7
+ *
8
+ * El corte se hace al resolver el módulo: es el único momento en que se puede
9
+ * impedir que entre al grafo del cliente.
10
+ */
11
+ /** Forma estructural del plugin, para no depender del tipo de Vite. */
12
+ export interface GuardPlugin {
13
+ name: string;
14
+ /**
15
+ * Antes que el resolvedor de Vite.
16
+ *
17
+ * Sin esto el hook no llegaba a correr: el resolvedor incorporado atiende los
18
+ * especificadores desnudos y devuelve un resultado, y un `resolveId` posterior
19
+ * ya no se consulta. El guard quedaba registrado y mudo, que es la peor forma
20
+ * de tener una comprobación de seguridad.
21
+ */
22
+ enforce: 'pre';
23
+ resolveId: (this: {
24
+ /** El entorno que está resolviendo. Es como Vite 6+ lo expone al plugin. */
25
+ environment?: {
26
+ name?: string;
27
+ };
28
+ resolve: (source: string, importer?: string, options?: unknown) => Promise<{
29
+ id: string;
30
+ } | null>;
31
+ }, source: string, importer: string | undefined, options?: {
32
+ environment?: {
33
+ name?: string;
34
+ };
35
+ }) => Promise<null>;
36
+ }
37
+ /**
38
+ * Reconoce el paquete por su especificador **y** por el archivo al que resuelve.
39
+ *
40
+ * Comparar sólo el especificador exacto dejaba pasar un subpath
41
+ * (`@ressjs/config/load`), un barrel de la aplicación que lo reexporta, o una
42
+ * ruta relativa hacia adentro del paquete en un monorepo.
43
+ */
44
+ export declare function isConfigModule(source: string, resolvedId?: string): boolean;
45
+ export declare function configServerOnlyGuard(): GuardPlugin;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Impide que los valores de servidor lleguen al bundle del cliente.
4
+ *
5
+ * Este paquete lee archivos que pueden tener credenciales. Si un componente lo
6
+ * importa —aunque sea sin querer, a través de un módulo compartido— esos valores
7
+ * terminan en un archivo que cualquiera puede descargar.
8
+ *
9
+ * El corte se hace al resolver el módulo: es el único momento en que se puede
10
+ * impedir que entre al grafo del cliente.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.isConfigModule = isConfigModule;
14
+ exports.configServerOnlyGuard = configServerOnlyGuard;
15
+ const PACKAGE = '@ressjs/config';
16
+ /**
17
+ * Reconoce el paquete por su especificador **y** por el archivo al que resuelve.
18
+ *
19
+ * Comparar sólo el especificador exacto dejaba pasar un subpath
20
+ * (`@ressjs/config/load`), un barrel de la aplicación que lo reexporta, o una
21
+ * ruta relativa hacia adentro del paquete en un monorepo.
22
+ */
23
+ function isConfigModule(source, resolvedId) {
24
+ if (source === PACKAGE || source.startsWith(`${PACKAGE}/`))
25
+ return true;
26
+ const id = resolvedId?.replace(/\\/g, '/');
27
+ return Boolean(id && /\/packages\/config\/|\/@ressjs\/config\//.test(id));
28
+ }
29
+ function configServerOnlyGuard() {
30
+ return {
31
+ name: 'ress:config-server-only',
32
+ enforce: 'pre',
33
+ async resolveId(source, importer, options) {
34
+ // El entorno viaja en el contexto del plugin, no en las opciones del
35
+ // hook. Leerlo sólo de las opciones dejaba al guard sin disparar nunca:
36
+ // devolvía `null` en todas las resoluciones, incluidas las del cliente.
37
+ const environment = this?.environment?.name ?? options?.environment?.name;
38
+ if (environment !== 'client')
39
+ return null;
40
+ const resolved = await this.resolve(source, importer, { skipSelf: true });
41
+ if (!isConfigModule(source, resolved?.id))
42
+ return null;
43
+ throw new Error(`[ress] "${PACKAGE}" es de sólo servidor y lo importa ${importer ?? 'el punto de entrada'}, ` +
44
+ 'que forma parte del bundle de cliente.\n' +
45
+ 'Los valores que el navegador necesita se declaran en `publicEnv`.');
46
+ },
47
+ };
48
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ressjs/config",
3
+ "version": "0.6.0-experimental.0",
4
+ "description": "Valores de configuración por entorno para ress.js. Sólo servidor, sin dependencias.",
5
+ "types": "dist/index.d.ts",
6
+ "main": "dist/index.js",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "require": "./dist/index.js",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./layers": {
17
+ "types": "./dist/layers.d.ts",
18
+ "require": "./dist/layers.js",
19
+ "import": "./dist/layers.js"
20
+ },
21
+ "./vite-guard": {
22
+ "types": "./dist/vite-guard.d.ts",
23
+ "require": "./dist/vite-guard.js",
24
+ "import": "./dist/vite-guard.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "dev": "tsc --watch",
33
+ "clean": "rm -rf dist",
34
+ "test": "vitest run"
35
+ },
36
+ "keywords": [
37
+ "config",
38
+ "environment",
39
+ "ressjs"
40
+ ],
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "typescript": "^5.9.3",
44
+ "vitest": "^3.2.4"
45
+ }
46
+ }