@walkeros/config 1.0.2 → 1.1.0-next-1771252576264

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/jest/index.mjs CHANGED
@@ -1,57 +1,75 @@
1
1
  import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
- import { readFileSync } from 'fs';
4
-
5
- const isWatchMode = process.argv.includes('--watchAll');
6
- const packagesDir = path.join(
7
- path.dirname(fileURLToPath(import.meta.url)),
8
- '..',
9
- '..',
10
- '..',
11
- '/',
12
- );
13
-
14
- function getDirectory(dir) {
15
- return path.join(packagesDir, dir);
3
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'fs';
4
+
5
+ // Resolve symlinks so import.meta.url always points to the real file path,
6
+ // even when Jest resolves through node_modules symlinks.
7
+ const configDir = realpathSync(path.dirname(fileURLToPath(import.meta.url)));
8
+ const packagesDir = path.join(configDir, '..', '..', '..', '/');
9
+
10
+ function escapeRegex(str) {
11
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
12
  }
17
13
 
18
14
  function getModuleMapper() {
19
- if (!isWatchMode) return {};
15
+ const mapper = {};
16
+ const scanDir = path.join(packagesDir, 'packages');
20
17
 
21
- return {
22
- '^@walkeros/core$': getDirectory('core/src/'),
23
- '^@walkeros/collector$': getDirectory('collector/src/'),
24
- '^@walkeros/web-core$': getDirectory('web/core/src/'),
25
- '^@walkeros/web-destination-gtag$': getDirectory(
26
- 'web/destinations/gtag/src/',
27
- ),
28
- '^@walkeros/web-destination-api$': getDirectory(
29
- 'web/destinations/api/src/',
30
- ),
31
- '^@walkeros/web-destination-meta$': getDirectory(
32
- 'web/destinations/meta/src/',
33
- ),
34
- '^@walkeros/web-destination-piwikpro$': getDirectory(
35
- 'web/destinations/piwikpro/src/',
36
- ),
37
- '^@walkeros/web-destination-plausible$': getDirectory(
38
- 'web/destinations/plausible/src/',
39
- ),
40
- '^@walkeros/web-source-browser$': getDirectory('web/sources/browser/src/'),
41
- '^@walkeros/web-source-datalayer$': getDirectory(
42
- 'web/sources/dataLayer/src/',
43
- ),
44
- '^@walkeros/server-core$': getDirectory('server/core/src/'),
45
- '^@walkeros/server-destination-aws$': getDirectory(
46
- 'server/destinations/aws/src/',
47
- ),
48
- '^@walkeros/server-destination-gcp$': getDirectory(
49
- 'server/destinations/gcp/src/',
50
- ),
51
- '^@walkeros/server-destination-meta$': getDirectory(
52
- 'server/destinations/meta/src/',
53
- ),
54
- };
18
+ function scanForPackages(dir) {
19
+ if (!existsSync(dir)) return;
20
+ const entries = readdirSync(dir, { withFileTypes: true });
21
+
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory()) continue;
24
+ if (entry.name === 'node_modules' || entry.name === 'dist') continue;
25
+
26
+ const fullPath = path.join(dir, entry.name);
27
+ const pkgJsonPath = path.join(fullPath, 'package.json');
28
+
29
+ if (existsSync(pkgJsonPath)) {
30
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
31
+
32
+ if (pkg.name?.startsWith('@walkeros/') && existsSync(path.join(fullPath, 'src'))) {
33
+ // Map root export: @walkeros/core → packages/core/src/
34
+ mapper[`^${escapeRegex(pkg.name)}$`] = path.join(fullPath, 'src/');
35
+
36
+ // Map subpath exports: @walkeros/core/dev → packages/core/src/dev
37
+ if (pkg.exports) {
38
+ for (const subpath of Object.keys(pkg.exports)) {
39
+ if (subpath === '.' || !subpath.startsWith('./')) continue;
40
+ const subName = subpath.slice(2);
41
+ const srcFile = path.join(fullPath, 'src', subName);
42
+ if (
43
+ existsSync(srcFile + '.ts') ||
44
+ existsSync(srcFile + '.tsx') ||
45
+ existsSync(path.join(srcFile, 'index.ts'))
46
+ ) {
47
+ mapper[`^${escapeRegex(pkg.name)}/${escapeRegex(subName)}$`] = srcFile;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ // Recurse into subdirectories (handles packages/web/core/, packages/server/destinations/api/, etc.)
55
+ scanForPackages(fullPath);
56
+ }
57
+ }
58
+
59
+ scanForPackages(path.join(packagesDir, 'packages'));
60
+
61
+ if (Object.keys(mapper).length === 0) {
62
+ throw new Error(
63
+ `[jest-config] moduleNameMapper is empty — no @walkeros/* packages found.\n` +
64
+ ` packagesDir: ${packagesDir}\n` +
65
+ ` scanDir: ${scanDir}\n` +
66
+ ` scanDir exists: ${existsSync(scanDir)}\n` +
67
+ `This usually means a stale @walkeros/config copy was loaded from node_modules ` +
68
+ `instead of the workspace version. Run: npm dedupe @walkeros/config`,
69
+ );
70
+ }
71
+
72
+ return mapper;
55
73
  }
56
74
 
57
75
  function getGlobals() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkeros/config",
3
- "version": "1.0.2",
3
+ "version": "1.1.0-next-1771252576264",
4
4
  "type": "module",
5
5
  "description": "Shared development configuration for walkerOS packages (TypeScript, ESLint, Jest, tsup)",
6
6
  "license": "MIT",
@@ -60,7 +60,6 @@
60
60
  "url": "https://github.com/elbwalker/walkerOS/issues"
61
61
  },
62
62
  "keywords": [
63
- "walker",
64
63
  "walkerOS",
65
64
  "config",
66
65
  "typescript",
@@ -18,6 +18,8 @@
18
18
  "target": "es2022",
19
19
  "allowJs": true,
20
20
  "resolveJsonModule": true,
21
+ "incremental": true,
22
+ "tsBuildInfoFile": ".tsbuildinfo",
21
23
  "sourceMap": true,
22
24
  "outDir": "dist",
23
25
  "baseUrl": "."
@@ -0,0 +1,220 @@
1
+ import { toSerializable } from '../index.mjs';
2
+
3
+ describe('toSerializable', () => {
4
+ // Primitives
5
+ it('passes through strings', () => {
6
+ expect(toSerializable('hello')).toBe('hello');
7
+ });
8
+
9
+ it('passes through numbers', () => {
10
+ expect(toSerializable(42)).toBe(42);
11
+ expect(toSerializable(0)).toBe(0);
12
+ expect(toSerializable(-1.5)).toBe(-1.5);
13
+ });
14
+
15
+ it('passes through booleans', () => {
16
+ expect(toSerializable(true)).toBe(true);
17
+ expect(toSerializable(false)).toBe(false);
18
+ });
19
+
20
+ it('passes through null', () => {
21
+ expect(toSerializable(null)).toBeNull();
22
+ });
23
+
24
+ it('passes through undefined', () => {
25
+ expect(toSerializable(undefined)).toBeUndefined();
26
+ });
27
+
28
+ // Functions
29
+ it('converts functions to $code objects', () => {
30
+ const fn = () => 'test';
31
+ const result = toSerializable(fn);
32
+ expect(result).toEqual({ $code: fn.toString() });
33
+ });
34
+
35
+ it('converts named functions to $code objects', () => {
36
+ function myFunc(a, b) {
37
+ return a + b;
38
+ }
39
+ const result = toSerializable(myFunc);
40
+ expect(result).toHaveProperty('$code');
41
+ expect(result.$code).toContain('myFunc');
42
+ });
43
+
44
+ // Zod schema instances (objects with _def.typeName starting with 'Zod')
45
+ it('should filter out Zod schema instances', () => {
46
+ const zodSchema = {
47
+ parse: (val) => val,
48
+ safeParse: (val) => ({ success: true, data: val }),
49
+ _def: { typeName: 'ZodObject' },
50
+ };
51
+ expect(toSerializable(zodSchema)).toBeUndefined();
52
+ });
53
+
54
+ it('filters out other Zod types', () => {
55
+ const zodString = { parse: () => {}, _def: { typeName: 'ZodString' } };
56
+ expect(toSerializable(zodString)).toBeUndefined();
57
+ });
58
+
59
+ it('filters out Zod 4 instances (have _zod property)', () => {
60
+ // Zod 4 instances have _zod but no _def.typeName
61
+ const zod4Schema = {
62
+ parse: () => {},
63
+ safeParse: () => {},
64
+ _def: { type: 'object', shape: {} },
65
+ _zod: { def: {}, constr: {}, traits: new Set(), bag: {} },
66
+ };
67
+ expect(toSerializable(zod4Schema)).toBeUndefined();
68
+ });
69
+
70
+ it('filters out Zod 4 instances from nested objects', () => {
71
+ const obj = {
72
+ settings: { type: 'object', properties: { url: { type: 'string' } } },
73
+ SettingsSchema: {
74
+ parse: () => {},
75
+ _zod: { def: {}, constr: {} },
76
+ },
77
+ MappingSchema: {
78
+ parse: () => {},
79
+ _def: { type: 'object' },
80
+ _zod: { def: {} },
81
+ },
82
+ };
83
+ const result = toSerializable(obj);
84
+ expect(result).toEqual({
85
+ settings: { type: 'object', properties: { url: { type: 'string' } } },
86
+ });
87
+ expect(result).not.toHaveProperty('SettingsSchema');
88
+ expect(result).not.toHaveProperty('MappingSchema');
89
+ });
90
+
91
+ it('should NOT filter out non-Zod objects that have a .parse method', () => {
92
+ const csvParser = { parse: (str) => str.split(','), data: [1, 2, 3] };
93
+ const result = toSerializable(csvParser);
94
+ expect(result).toEqual({ data: [1, 2, 3], parse: { $code: csvParser.parse.toString() } });
95
+ });
96
+
97
+ // Plain objects
98
+ it('passes through plain objects', () => {
99
+ const obj = { a: 1, b: 'two', c: true };
100
+ expect(toSerializable(obj)).toEqual({ a: 1, b: 'two', c: true });
101
+ });
102
+
103
+ it('handles nested objects', () => {
104
+ const obj = { outer: { inner: { value: 42 } } };
105
+ expect(toSerializable(obj)).toEqual({ outer: { inner: { value: 42 } } });
106
+ });
107
+
108
+ it('strips Zod instances from nested objects', () => {
109
+ const obj = {
110
+ settings: { type: 'object', properties: {} },
111
+ SettingsSchema: { parse: () => {}, _def: { typeName: 'ZodObject' } },
112
+ };
113
+ const result = toSerializable(obj);
114
+ expect(result).toEqual({
115
+ settings: { type: 'object', properties: {} },
116
+ });
117
+ expect(result).not.toHaveProperty('SettingsSchema');
118
+ });
119
+
120
+ it('converts nested functions to $code', () => {
121
+ const noop = () => {};
122
+ const obj = {
123
+ name: 'test',
124
+ handler: noop,
125
+ nested: { callback: noop },
126
+ };
127
+ const result = toSerializable(obj);
128
+ expect(result.name).toBe('test');
129
+ expect(result.handler).toEqual({ $code: noop.toString() });
130
+ expect(result.nested.callback).toEqual({ $code: noop.toString() });
131
+ });
132
+
133
+ // Arrays
134
+ it('passes through arrays of primitives', () => {
135
+ expect(toSerializable([1, 2, 3])).toEqual([1, 2, 3]);
136
+ });
137
+
138
+ it('replaces Zod instances in arrays with undefined (preserves indices)', () => {
139
+ const arr = [
140
+ { name: 'keep' },
141
+ { parse: () => {}, _def: { typeName: 'ZodString' } },
142
+ { name: 'also keep' },
143
+ ];
144
+ const result = toSerializable(arr);
145
+ expect(result).toEqual([{ name: 'keep' }, undefined, { name: 'also keep' }]);
146
+ expect(result).toHaveLength(3);
147
+ });
148
+
149
+ it('converts functions inside arrays', () => {
150
+ const fn = () => 'test';
151
+ const result = toSerializable([1, fn, 'three']);
152
+ expect(result).toEqual([1, { $code: fn.toString() }, 'three']);
153
+ });
154
+
155
+ it('handles nested arrays', () => {
156
+ const result = toSerializable([[1, 2], [3, 4]]);
157
+ expect(result).toEqual([[1, 2], [3, 4]]);
158
+ });
159
+
160
+ // Complex/realistic scenario
161
+ it('handles a realistic dev module export structure', () => {
162
+ const noop = () => Promise.resolve({ ok: true });
163
+ const devExports = {
164
+ env: {
165
+ init: { sendServer: undefined },
166
+ standard: { sendServer: noop },
167
+ },
168
+ events: {
169
+ pageView: { event: 'page view', data: { title: 'Home' } },
170
+ },
171
+ };
172
+
173
+ const result = toSerializable(devExports);
174
+
175
+ // sendServer undefined should be stripped (key omitted)
176
+ expect(result.env.init).toEqual({});
177
+ // sendServer function should become $code
178
+ expect(result.env.standard.sendServer).toHaveProperty('$code');
179
+ expect(result.env.standard.sendServer.$code).toContain('Promise.resolve');
180
+ // Plain data passes through
181
+ expect(result.events.pageView).toEqual({
182
+ event: 'page view',
183
+ data: { title: 'Home' },
184
+ });
185
+ });
186
+
187
+ // JSON roundtrip safety
188
+ it('produces JSON-safe output', () => {
189
+ const fn = (a) => a * 2;
190
+ const input = {
191
+ name: 'test',
192
+ handler: fn,
193
+ zodSchema: { parse: () => {}, _def: { typeName: 'ZodObject' } },
194
+ data: [1, 'two', { nested: true }],
195
+ };
196
+
197
+ const result = toSerializable(input);
198
+ const json = JSON.stringify(result);
199
+ const parsed = JSON.parse(json);
200
+
201
+ expect(parsed.name).toBe('test');
202
+ expect(parsed.handler.$code).toContain('a * 2');
203
+ expect(parsed).not.toHaveProperty('zodSchema');
204
+ expect(parsed.data).toEqual([1, 'two', { nested: true }]);
205
+ });
206
+
207
+ // Edge cases
208
+ it('handles empty objects', () => {
209
+ expect(toSerializable({})).toEqual({});
210
+ });
211
+
212
+ it('handles empty arrays', () => {
213
+ expect(toSerializable([])).toEqual([]);
214
+ });
215
+
216
+ it('does not treat objects with non-function .parse as Zod', () => {
217
+ const obj = { parse: 'not a function', value: 42 };
218
+ expect(toSerializable(obj)).toEqual({ parse: 'not a function', value: 42 });
219
+ });
220
+ });
package/tsup/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineConfig } from 'tsup';
2
- import { readFileSync } from 'fs';
3
- import { resolve } from 'path';
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+ import { pathToFileURL } from 'url';
4
5
 
5
6
  const baseConfig = {
6
7
  entry: ['src/index.ts'],
@@ -73,11 +74,133 @@ const buildES5 = (customConfig = {}) => ({
73
74
  ...customConfig,
74
75
  });
75
76
 
77
+ /**
78
+ * Deep-clone a value for JSON serialization.
79
+ *
80
+ * Conventions:
81
+ * - Functions → { $code: fn.toString() } — serialized source for documentation, not executable
82
+ * - Zod instances → filtered out (returns undefined)
83
+ * Zod 3: detected via _def.typeName starting with 'Zod'
84
+ * Zod 4: detected via _zod property
85
+ * - Everything else → recursively passed through
86
+ */
87
+ const toSerializable = (value) => {
88
+ if (value === null || value === undefined) return value;
89
+ if (typeof value === 'function') return { $code: value.toString() };
90
+ if (
91
+ typeof value === 'object' &&
92
+ (value._def?.typeName?.startsWith?.('Zod') || '_zod' in value)
93
+ )
94
+ return undefined;
95
+ if (Array.isArray(value)) {
96
+ return value.map((item) => toSerializable(item));
97
+ }
98
+ if (typeof value === 'object') {
99
+ const result = {};
100
+ for (const [key, val] of Object.entries(value)) {
101
+ const serialized = toSerializable(val);
102
+ if (serialized !== undefined) result[key] = serialized;
103
+ }
104
+ return result;
105
+ }
106
+ return value;
107
+ };
108
+
109
+ // Dev build: builds src/dev.ts and generates walkerOS.json
110
+ const buildDev = (customConfig = {}) => {
111
+ const { onSuccess: customOnSuccess, ...restConfig } = customConfig;
112
+
113
+ const modulesConfig = buildModules({
114
+ entry: ['src/dev.ts'],
115
+ ...restConfig,
116
+ });
117
+
118
+ return {
119
+ ...modulesConfig,
120
+ async onSuccess() {
121
+ const cwd = process.cwd();
122
+ const packagePath = resolve(cwd, 'package.json');
123
+ let pkg = { name: 'unknown', version: '0.0.0' };
124
+ try {
125
+ pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
126
+ } catch (error) {
127
+ console.warn('[buildDev] Could not read package.json:', error.message);
128
+ }
129
+
130
+ // Check the built module exists
131
+ const devMjsPath = resolve(cwd, 'dist/dev.mjs');
132
+ if (!existsSync(devMjsPath)) {
133
+ console.warn('[buildDev] dist/dev.mjs not found, skipping walkerOS.json generation');
134
+ return;
135
+ }
136
+
137
+ // Import the built module (cache-bust for watch mode rebuilds)
138
+ const devModulePath = pathToFileURL(devMjsPath).href;
139
+ let devModule;
140
+ try {
141
+ devModule = await import(`${devModulePath}?t=${Date.now()}`);
142
+ } catch (error) {
143
+ console.warn(
144
+ `[buildDev] Could not import dist/dev.mjs: ${error.message}. Skipping walkerOS.json generation`,
145
+ );
146
+ return;
147
+ }
148
+
149
+ // Extract schemas (Zod instances filtered by toSerializable)
150
+ const schemas = toSerializable(devModule.schemas || {}) || {};
151
+
152
+ // Extract examples (convert functions to $code strings)
153
+ const rawExamples = devModule.examples || {};
154
+ const examples = toSerializable(rawExamples) || {};
155
+
156
+ // Read type and platform from package.json walkerOS field
157
+ const walkerOS = pkg.walkerOS || {};
158
+ const meta = { package: pkg.name, version: pkg.version };
159
+ if (walkerOS.type) meta.type = walkerOS.type;
160
+ if (walkerOS.platform) meta.platform = walkerOS.platform;
161
+
162
+ const output = { $meta: meta, schemas, examples };
163
+
164
+ // Validate
165
+ if (Object.keys(schemas).length === 0) {
166
+ console.warn('[buildDev] Warning: schemas is empty');
167
+ }
168
+
169
+ // Verify valid JSON roundtrip
170
+ const jsonString = JSON.stringify(output, null, 2);
171
+ try {
172
+ JSON.parse(jsonString);
173
+ } catch (error) {
174
+ console.error(
175
+ '[buildDev] Error: output is not valid JSON:',
176
+ error.message,
177
+ );
178
+ return;
179
+ }
180
+
181
+ // Write to dist/walkerOS.json
182
+ const outDir = resolve(cwd, 'dist');
183
+ mkdirSync(outDir, { recursive: true });
184
+ writeFileSync(join(outDir, 'walkerOS.json'), jsonString);
185
+ console.log(
186
+ `[buildDev] Generated dist/walkerOS.json for ${pkg.name}@${pkg.version}`,
187
+ );
188
+
189
+ // Run custom onSuccess if provided
190
+ if (typeof customOnSuccess === 'function') {
191
+ await customOnSuccess();
192
+ }
193
+ },
194
+ };
195
+ };
196
+
76
197
  export {
77
198
  baseConfig,
78
199
  buildModules,
79
200
  buildExamples,
80
201
  buildBrowser,
81
202
  buildES5,
203
+ buildDev,
204
+ toSerializable,
82
205
  defineConfig,
83
206
  };