create-aziz-app 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +658 -207
  2. package/bin/create-aziz-app.js +302 -351
  3. package/package.json +1 -1
  4. package/template/README.md +14 -279
  5. package/template/_framework/api.js +219 -219
  6. package/template/_framework/boot.js +33 -33
  7. package/template/_framework/hooks.js +367 -291
  8. package/template/_framework/next-response.js +128 -128
  9. package/template/_framework/route-match.js +145 -145
  10. package/template/_framework/router.js +465 -259
  11. package/template/_framework/runtime.js +264 -264
  12. package/template/app/layout.js +3 -5
  13. package/template/app/page.js +9 -32
  14. package/template/docs/01-getting-started.md +151 -151
  15. package/template/docs/02-project-structure.md +109 -109
  16. package/template/docs/03-pages-and-layouts.md +140 -140
  17. package/template/docs/04-routing.md +120 -120
  18. package/template/docs/05-hooks.md +169 -169
  19. package/template/docs/06-api-routes.md +210 -210
  20. package/template/docs/07-api-client.md +151 -151
  21. package/template/docs/08-styling.md +94 -94
  22. package/template/docs/09-imports-and-modules.md +97 -97
  23. package/template/docs/10-database-and-models.md +154 -154
  24. package/template/docs/11-authentication.md +171 -171
  25. package/template/docs/12-production-and-deployment.md +121 -121
  26. package/template/docs/13-internals.md +179 -179
  27. package/template/docs/README.md +67 -67
  28. package/template/index.html +20 -20
  29. package/template/package.json +16 -16
  30. package/template/scripts/dev-server.js +493 -381
  31. package/template/scripts/lib/api-router.js +525 -525
  32. package/template/scripts/lib/js-transform.js +213 -141
  33. package/template/scripts/lib/module-resolver.js +132 -132
@@ -1,141 +1,213 @@
1
- // ============================================================
2
- // Aziz.js — JS/JSX transform pipeline (server-side)
3
- // - JSX → h() calls via @babel/standalone
4
- // - Bare / @/ import rewriting to browser-servable URLs
5
- // - mtime-based transform cache (big dev-server speed win)
6
- // ============================================================
7
-
8
- const path = require('path');
9
- const fs = require('fs');
10
-
11
- // ─── Babel loading (with fallback resolution for monorepos/tests) ───
12
- let Babel = null;
13
- try {
14
- Babel = require('@babel/standalone');
15
- } catch (e) {
16
- const fallbacks = [
17
- path.join(__dirname, '..', '..', 'node_modules', '@babel', 'standalone'),
18
- path.join(process.cwd(), 'node_modules', '@babel', 'standalone'),
19
- ];
20
- for (const fb of fallbacks) {
21
- try { Babel = require(fb); break; } catch { /* keep looking */ }
22
- }
23
- }
24
-
25
- function hasBabel() {
26
- return !!Babel;
27
- }
28
-
29
- // ─── JSX transform ───────────────────────────────────────────
30
-
31
- function isFrameworkPath(filePath) {
32
- return filePath.includes('/_framework/') || filePath.includes('\\_framework\\');
33
- }
34
-
35
- function transformJSX(code, filename) {
36
- if (!Babel) return code;
37
- const result = Babel.transform(code, {
38
- filename,
39
- presets: [
40
- ['react', { pragma: 'h', pragmaFrag: 'Fragment' }]
41
- ],
42
- plugins: [],
43
- sourceType: 'module',
44
- });
45
- return result.code;
46
- }
47
-
48
- // ─── Import rewriting ─────────────────────────────────────────
49
- // import X from 'mongoose' → '/_modules/mongoose'
50
- // import X from '@/lib/db' → '/lib/db'
51
- // import X from '/absolute.js' → unchanged
52
- // import('pkg') → import('/_modules/pkg')
53
- //
54
- // Runs on the whole module (not line-by-line) so multi-line
55
- // import statements are handled correctly.
56
-
57
- function isRelativeOrAbsolute(spec) {
58
- return spec.startsWith('/') ||
59
- spec.startsWith('./') ||
60
- spec.startsWith('../') ||
61
- spec.startsWith('#') ||
62
- spec.startsWith('node:') ||
63
- spec.startsWith('data:') ||
64
- spec.startsWith('file:') ||
65
- /^https?:\/\//.test(spec);
66
- }
67
-
68
- function mapSpecifier(spec) {
69
- if (isRelativeOrAbsolute(spec)) return spec;
70
- if (spec.startsWith('@/')) return '/' + spec.slice(2);
71
- return '/_modules/' + spec;
72
- }
73
-
74
- function rewriteImports(code) {
75
- // from 'specifier' (covers `import ... from`, `export ... from`)
76
- code = code.replace(/(\bfrom\s*)(['"])([^'"\n]+)\2/g, (m, from, q, spec) => {
77
- const mapped = mapSpecifier(spec);
78
- return mapped === spec ? m : `${from}${q}${mapped}${q}`;
79
- });
80
-
81
- // side-effect imports: import 'specifier';
82
- // (requires the quote right after `import`, so `import(` is not matched)
83
- code = code.replace(/(\bimport\s+)(['"])([^'"\n]+)\2/g, (m, kw, q, spec) => {
84
- const mapped = mapSpecifier(spec);
85
- return mapped === spec ? m : `${kw}${q}${mapped}${q}`;
86
- });
87
-
88
- // dynamic imports: import('specifier')
89
- code = code.replace(/(\bimport\s*\(\s*)(['"])([^'"\n]+)\2(\s*\))/g, (m, pre, q, spec, post) => {
90
- const mapped = mapSpecifier(spec);
91
- return mapped === spec ? m : `${pre}${q}${mapped}${q}${post}`;
92
- });
93
-
94
- return code;
95
- }
96
-
97
- // ─── mtime-keyed transform cache ─────────────────────────────
98
-
99
- const cache = new Map(); // filePath → { mtimeMs, code }
100
-
101
- function transformModule(code, filePath, mtimeMs) {
102
- // Framework internals contain no JSX and only use absolute imports.
103
- if (isFrameworkPath(filePath)) return code;
104
-
105
- const hit = cache.get(filePath);
106
- if (hit && hit.mtimeMs === mtimeMs) return hit.code;
107
-
108
- let out;
109
- try {
110
- out = transformJSX(code, path.basename(filePath));
111
- } catch (err) {
112
- console.error(`❌ JSX transform error in ${filePath}:`, err.message);
113
- out = `throw new Error(${JSON.stringify('JSX transform error in ' + filePath + ': ' + err.message)});`;
114
- }
115
- out = rewriteImports(out);
116
- cache.set(filePath, { mtimeMs, code: out });
117
- return out;
118
- }
119
-
120
- function invalidate(filePath) {
121
- cache.delete(filePath);
122
- }
123
-
124
- function clearCache() {
125
- cache.clear();
126
- }
127
-
128
- function cacheSize() {
129
- return cache.size;
130
- }
131
-
132
- module.exports = {
133
- hasBabel,
134
- transformJSX,
135
- rewriteImports,
136
- transformModule,
137
- invalidate,
138
- clearCache,
139
- cacheSize,
140
- isFrameworkPath,
141
- };
1
+ // ============================================================
2
+ // Aziz.js — JS/JSX transform pipeline (server-side)
3
+ // - JSX → h() calls via @babel/standalone
4
+ // - Bare / @/ import rewriting to browser-servable URLs
5
+ // - mtime-based transform cache (big dev-server speed win)
6
+ // ============================================================
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ // ─── Babel loading (with fallback resolution for monorepos/tests) ───
12
+ let Babel = null;
13
+ try {
14
+ Babel = require('@babel/standalone');
15
+ } catch (e) {
16
+ const fallbacks = [
17
+ path.join(__dirname, '..', '..', 'node_modules', '@babel', 'standalone'),
18
+ path.join(process.cwd(), 'node_modules', '@babel', 'standalone'),
19
+ ];
20
+ for (const fb of fallbacks) {
21
+ try { Babel = require(fb); break; } catch { /* keep looking */ }
22
+ }
23
+ }
24
+
25
+ function hasBabel() {
26
+ return !!Babel;
27
+ }
28
+
29
+ // Thrown whenever a module needs compiling but Babel is unavailable.
30
+ // Never fall back to the untransformed source: the browser cannot execute
31
+ // JSX, the module import rejects, and the router silently renders its
32
+ // built-in 404 — a missing dependency disguised as a routing bug.
33
+ const BABEL_MISSING_MESSAGE =
34
+ '@babel/standalone is not installed, so JSX cannot be compiled. ' +
35
+ 'Run "npm install" in the project folder, then restart the dev server.';
36
+
37
+ // ─── JSX transform ───────────────────────────────────────────
38
+
39
+ function isFrameworkPath(filePath) {
40
+ return filePath.includes('/_framework/') || filePath.includes('\\_framework\\');
41
+ }
42
+
43
+ function transformJSX(code, filename) {
44
+ if (!Babel) throw new Error(BABEL_MISSING_MESSAGE);
45
+ const result = Babel.transform(code, {
46
+ filename,
47
+ presets: [
48
+ // runtime: 'classic' is required. Babel 8 defaults to the automatic
49
+ // runtime, which rejects an explicit pragma ("pragma and pragmaFrag
50
+ // cannot be set when runtime is automatic"). We compile to the
51
+ // global `h`/`Fragment` that boot.js installs, not to jsx-runtime.
52
+ ['react', { runtime: 'classic', pragma: 'h', pragmaFrag: 'Fragment' }]
53
+ ],
54
+ plugins: [],
55
+ sourceType: 'module',
56
+ });
57
+ return result.code;
58
+ }
59
+
60
+ // ─── Import rewriting ─────────────────────────────────────────
61
+ // import X from 'mongoose' → '/_modules/mongoose'
62
+ // import X from '@/lib/db' → '/lib/db'
63
+ // import X from '/absolute.js' → unchanged
64
+ // import('pkg') → import('/_modules/pkg')
65
+ //
66
+ // Runs on the whole module (not line-by-line) so multi-line
67
+ // import statements are handled correctly.
68
+ //
69
+ // Only keywords sitting in CODE position are rewritten. A `from`
70
+ // or `import` inside a string literal / template literal / comment
71
+ // is prose (e.g. documentation text in JSX) and must be left alone
72
+ // — rewriting it corrupts the module and the page fails to load.
73
+
74
+ // Uint8Array marking every character that is in code position
75
+ // (outside string literals, template literals and comments).
76
+ function codePositions(code) {
77
+ const n = code.length;
78
+ const isCode = new Uint8Array(n);
79
+ const stack = []; // '{' for blocks, 'tpl' for ${ … } in templates
80
+ let mode = 'code'; // code | ' | " | ` | // | /*
81
+ let i = 0;
82
+
83
+ while (i < n) {
84
+ const c = code[i];
85
+
86
+ if (mode === 'code') {
87
+ isCode[i] = 1;
88
+ if (c === '\\') { i += 2; continue; }
89
+ if (c === "'" || c === '"' || c === '`') mode = c;
90
+ else if (c === '/' && code[i + 1] === '/') mode = '//';
91
+ else if (c === '/' && code[i + 1] === '*') mode = '/*';
92
+ else if (c === '{') stack.push('{');
93
+ else if (c === '}') { if (stack.pop() === 'tpl') mode = '`'; }
94
+ i++;
95
+ } else if (mode === "'" || mode === '"' || mode === '`') {
96
+ if (c === '\\') { i += 2; continue; }
97
+ if (c === mode) mode = 'code';
98
+ else if (mode === '`' && c === '$' && code[i + 1] === '{') {
99
+ stack.push('tpl');
100
+ mode = 'code';
101
+ isCode[i] = 1;
102
+ isCode[i + 1] = 1;
103
+ i += 2;
104
+ continue;
105
+ }
106
+ i++;
107
+ } else if (mode === '//') {
108
+ if (c === '\n') mode = 'code';
109
+ i++;
110
+ } else { // block comment
111
+ if (c === '*' && code[i + 1] === '/') { mode = 'code'; i += 2; continue; }
112
+ i++;
113
+ }
114
+ }
115
+ return isCode;
116
+ }
117
+
118
+ function isRelativeOrAbsolute(spec) {
119
+ return spec.startsWith('/') ||
120
+ spec.startsWith('./') ||
121
+ spec.startsWith('../') ||
122
+ spec.startsWith('#') ||
123
+ spec.startsWith('node:') ||
124
+ spec.startsWith('data:') ||
125
+ spec.startsWith('file:') ||
126
+ /^https?:\/\//.test(spec);
127
+ }
128
+
129
+ function mapSpecifier(spec) {
130
+ if (isRelativeOrAbsolute(spec)) return spec;
131
+ if (spec.startsWith('@/')) return '/' + spec.slice(2);
132
+ return '/_modules/' + spec;
133
+ }
134
+
135
+ function rewriteImports(code) {
136
+ const isCode = codePositions(code);
137
+
138
+ // from 'specifier' (covers `import ... from`, `export ... from`)
139
+ code = code.replace(/(\bfrom\s*)(['"])([^'"\n]+)\2/g, (m, from, q, spec, offset) => {
140
+ if (!isCode[offset]) return m;
141
+ const mapped = mapSpecifier(spec);
142
+ return mapped === spec ? m : `${from}${q}${mapped}${q}`;
143
+ });
144
+
145
+ // side-effect imports: import 'specifier';
146
+ // (requires the quote right after `import`, so `import(` is not matched)
147
+ code = code.replace(/(\bimport\s+)(['"])([^'"\n]+)\2/g, (m, kw, q, spec, offset) => {
148
+ if (!isCode[offset]) return m;
149
+ const mapped = mapSpecifier(spec);
150
+ return mapped === spec ? m : `${kw}${q}${mapped}${q}`;
151
+ });
152
+
153
+ // dynamic imports: import('specifier')
154
+ code = code.replace(/(\bimport\s*\(\s*)(['"])([^'"\n]+)\2(\s*\))/g, (m, pre, q, spec, post, offset) => {
155
+ if (!isCode[offset]) return m;
156
+ const mapped = mapSpecifier(spec);
157
+ return mapped === spec ? m : `${pre}${q}${mapped}${q}${post}`;
158
+ });
159
+
160
+ return code;
161
+ }
162
+
163
+ // ─── mtime-keyed transform cache ─────────────────────────────
164
+
165
+ const cache = new Map(); // filePath → { mtimeMs, code }
166
+
167
+ function transformModule(code, filePath, mtimeMs) {
168
+ // Framework internals contain no JSX and only use absolute imports.
169
+ if (isFrameworkPath(filePath)) return code;
170
+
171
+ const hit = cache.get(filePath);
172
+ if (hit && hit.mtimeMs === mtimeMs) return hit.code;
173
+
174
+ let out;
175
+ try {
176
+ out = transformJSX(code, path.basename(filePath));
177
+ } catch (err) {
178
+ if (err.message === BABEL_MISSING_MESSAGE) {
179
+ // Startup prints an actionable banner — don't repeat it for every file.
180
+ out = `throw new Error(${JSON.stringify(BABEL_MISSING_MESSAGE)});`;
181
+ } else {
182
+ console.error(`❌ JSX transform error in ${filePath}:`, err.message);
183
+ out = `throw new Error(${JSON.stringify('JSX transform error in ' + filePath + ': ' + err.message)});`;
184
+ }
185
+ }
186
+ out = rewriteImports(out);
187
+ cache.set(filePath, { mtimeMs, code: out });
188
+ return out;
189
+ }
190
+
191
+ function invalidate(filePath) {
192
+ cache.delete(filePath);
193
+ }
194
+
195
+ function clearCache() {
196
+ cache.clear();
197
+ }
198
+
199
+ function cacheSize() {
200
+ return cache.size;
201
+ }
202
+
203
+ module.exports = {
204
+ hasBabel,
205
+ BABEL_MISSING_MESSAGE,
206
+ transformJSX,
207
+ rewriteImports,
208
+ transformModule,
209
+ invalidate,
210
+ clearCache,
211
+ cacheSize,
212
+ isFrameworkPath,
213
+ };
@@ -1,132 +1,132 @@
1
- // ============================================================
2
- // Aziz.js — node_modules resolver for browser bare imports
3
- // Maps /_modules/<pkg>… to a real file inside node_modules.
4
- // Supports: package.json "exports", "browser", "module", "main".
5
- // ============================================================
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
-
10
- const EXTENSIONS = ['.js', '.mjs', '.cjs', '.json'];
11
-
12
- function statIsFile(p) {
13
- try { return fs.statSync(p).isFile(); } catch { return false; }
14
- }
15
-
16
- function firstExisting(candidates) {
17
- for (const c of candidates) {
18
- if (statIsFile(c)) return c;
19
- for (const ext of EXTENSIONS) {
20
- if (statIsFile(c + ext)) return c + ext;
21
- }
22
- if (statIsFile(path.join(c, 'index.js'))) return path.join(c, 'index.js');
23
- }
24
- return null;
25
- }
26
-
27
- // Resolve a value from the "exports" field (string or nested conditions).
28
- // Condition order prefers browser builds, then ESM, then CJS.
29
- function resolveExportsValue(value) {
30
- if (typeof value === 'string') return value;
31
- if (Array.isArray(value)) {
32
- for (const v of value) {
33
- const r = resolveExportsValue(v);
34
- if (r) return r;
35
- }
36
- return null;
37
- }
38
- if (value && typeof value === 'object') {
39
- for (const cond of ['browser', 'import', 'module', 'default', 'require']) {
40
- if (cond in value) {
41
- const r = resolveExportsValue(value[cond]);
42
- if (r) return r;
43
- }
44
- }
45
- }
46
- return null;
47
- }
48
-
49
- function resolveExportsField(exportsField, subPath) {
50
- if (!exportsField) return null;
51
-
52
- // "exports": "./index.js"
53
- if (typeof exportsField === 'string') {
54
- return subPath ? null : exportsField;
55
- }
56
-
57
- // Conditional exports at the top level (no subpaths)
58
- if (!subPath) {
59
- if ('.' in exportsField || Object.keys(exportsField).some(k => k.startsWith('./'))) {
60
- return '.' in exportsField ? resolveExportsValue(exportsField['.']) : null;
61
- }
62
- return resolveExportsValue(exportsField);
63
- }
64
-
65
- // Subpath exports
66
- const key = './' + subPath;
67
- if (key in exportsField) return resolveExportsValue(exportsField[key]);
68
-
69
- // Wildcard patterns: "./*": "./dist/*.js"
70
- for (const patternKey of Object.keys(exportsField)) {
71
- if (!patternKey.includes('*')) continue;
72
- const [pre, post = ''] = patternKey.split('*');
73
- if (key.startsWith(pre) && key.endsWith(post) && key.length >= pre.length + post.length) {
74
- const star = key.slice(pre.length, key.length - post.length);
75
- const target = resolveExportsValue(exportsField[patternKey]);
76
- if (target) return target.replace('*', star);
77
- }
78
- }
79
-
80
- return null;
81
- }
82
-
83
- // Resolve a bare specifier ("pkg", "pkg/sub", "@scope/pkg/sub")
84
- // to an absolute file path inside ROOT/node_modules.
85
- function resolveNodeModule(specifier, ROOT) {
86
- const parts = specifier.split('/');
87
- const isScoped = parts[0].startsWith('@');
88
- const pkgName = isScoped ? parts[0] + '/' + parts[1] : parts[0];
89
- const subPath = isScoped ? parts.slice(2).join('/') : parts.slice(1).join('/');
90
-
91
- const pkgDir = path.join(ROOT, 'node_modules', pkgName);
92
- const pkgJsonPath = path.join(pkgDir, 'package.json');
93
- if (!fs.existsSync(pkgJsonPath)) return null;
94
-
95
- let pkg;
96
- try {
97
- pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
98
- } catch {
99
- return null;
100
- }
101
-
102
- // 1. package.json "exports" (modern packages)
103
- if (pkg.exports) {
104
- const target = resolveExportsField(pkg.exports, subPath);
105
- if (target) {
106
- const resolved = firstExisting([path.join(pkgDir, target)]);
107
- if (resolved) return resolved;
108
- }
109
- // "exports" exists but didn't resolve — fall through only when
110
- // no subpath was requested, so legacy entry fields can still win.
111
- if (subPath) return null;
112
- }
113
-
114
- if (subPath) {
115
- return firstExisting([
116
- path.join(pkgDir, subPath),
117
- path.join(pkgDir, 'dist', subPath),
118
- path.join(pkgDir, 'src', subPath),
119
- ]);
120
- }
121
-
122
- // 2. Legacy entry fields: browser (string) → module → main
123
- const entries = [];
124
- if (typeof pkg.browser === 'string') entries.push(pkg.browser);
125
- if (pkg.module) entries.push(pkg.module);
126
- if (pkg.main) entries.push(pkg.main);
127
- entries.push('index.js');
128
-
129
- return firstExisting(entries.map(e => path.join(pkgDir, e)));
130
- }
131
-
132
- module.exports = { resolveNodeModule, resolveExportsField };
1
+ // ============================================================
2
+ // Aziz.js — node_modules resolver for browser bare imports
3
+ // Maps /_modules/<pkg>… to a real file inside node_modules.
4
+ // Supports: package.json "exports", "browser", "module", "main".
5
+ // ============================================================
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const EXTENSIONS = ['.js', '.mjs', '.cjs', '.json'];
11
+
12
+ function statIsFile(p) {
13
+ try { return fs.statSync(p).isFile(); } catch { return false; }
14
+ }
15
+
16
+ function firstExisting(candidates) {
17
+ for (const c of candidates) {
18
+ if (statIsFile(c)) return c;
19
+ for (const ext of EXTENSIONS) {
20
+ if (statIsFile(c + ext)) return c + ext;
21
+ }
22
+ if (statIsFile(path.join(c, 'index.js'))) return path.join(c, 'index.js');
23
+ }
24
+ return null;
25
+ }
26
+
27
+ // Resolve a value from the "exports" field (string or nested conditions).
28
+ // Condition order prefers browser builds, then ESM, then CJS.
29
+ function resolveExportsValue(value) {
30
+ if (typeof value === 'string') return value;
31
+ if (Array.isArray(value)) {
32
+ for (const v of value) {
33
+ const r = resolveExportsValue(v);
34
+ if (r) return r;
35
+ }
36
+ return null;
37
+ }
38
+ if (value && typeof value === 'object') {
39
+ for (const cond of ['browser', 'import', 'module', 'default', 'require']) {
40
+ if (cond in value) {
41
+ const r = resolveExportsValue(value[cond]);
42
+ if (r) return r;
43
+ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function resolveExportsField(exportsField, subPath) {
50
+ if (!exportsField) return null;
51
+
52
+ // "exports": "./index.js"
53
+ if (typeof exportsField === 'string') {
54
+ return subPath ? null : exportsField;
55
+ }
56
+
57
+ // Conditional exports at the top level (no subpaths)
58
+ if (!subPath) {
59
+ if ('.' in exportsField || Object.keys(exportsField).some(k => k.startsWith('./'))) {
60
+ return '.' in exportsField ? resolveExportsValue(exportsField['.']) : null;
61
+ }
62
+ return resolveExportsValue(exportsField);
63
+ }
64
+
65
+ // Subpath exports
66
+ const key = './' + subPath;
67
+ if (key in exportsField) return resolveExportsValue(exportsField[key]);
68
+
69
+ // Wildcard patterns: "./*": "./dist/*.js"
70
+ for (const patternKey of Object.keys(exportsField)) {
71
+ if (!patternKey.includes('*')) continue;
72
+ const [pre, post = ''] = patternKey.split('*');
73
+ if (key.startsWith(pre) && key.endsWith(post) && key.length >= pre.length + post.length) {
74
+ const star = key.slice(pre.length, key.length - post.length);
75
+ const target = resolveExportsValue(exportsField[patternKey]);
76
+ if (target) return target.replace('*', star);
77
+ }
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ // Resolve a bare specifier ("pkg", "pkg/sub", "@scope/pkg/sub")
84
+ // to an absolute file path inside ROOT/node_modules.
85
+ function resolveNodeModule(specifier, ROOT) {
86
+ const parts = specifier.split('/');
87
+ const isScoped = parts[0].startsWith('@');
88
+ const pkgName = isScoped ? parts[0] + '/' + parts[1] : parts[0];
89
+ const subPath = isScoped ? parts.slice(2).join('/') : parts.slice(1).join('/');
90
+
91
+ const pkgDir = path.join(ROOT, 'node_modules', pkgName);
92
+ const pkgJsonPath = path.join(pkgDir, 'package.json');
93
+ if (!fs.existsSync(pkgJsonPath)) return null;
94
+
95
+ let pkg;
96
+ try {
97
+ pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
98
+ } catch {
99
+ return null;
100
+ }
101
+
102
+ // 1. package.json "exports" (modern packages)
103
+ if (pkg.exports) {
104
+ const target = resolveExportsField(pkg.exports, subPath);
105
+ if (target) {
106
+ const resolved = firstExisting([path.join(pkgDir, target)]);
107
+ if (resolved) return resolved;
108
+ }
109
+ // "exports" exists but didn't resolve — fall through only when
110
+ // no subpath was requested, so legacy entry fields can still win.
111
+ if (subPath) return null;
112
+ }
113
+
114
+ if (subPath) {
115
+ return firstExisting([
116
+ path.join(pkgDir, subPath),
117
+ path.join(pkgDir, 'dist', subPath),
118
+ path.join(pkgDir, 'src', subPath),
119
+ ]);
120
+ }
121
+
122
+ // 2. Legacy entry fields: browser (string) → module → main
123
+ const entries = [];
124
+ if (typeof pkg.browser === 'string') entries.push(pkg.browser);
125
+ if (pkg.module) entries.push(pkg.module);
126
+ if (pkg.main) entries.push(pkg.main);
127
+ entries.push('index.js');
128
+
129
+ return firstExisting(entries.map(e => path.join(pkgDir, e)));
130
+ }
131
+
132
+ module.exports = { resolveNodeModule, resolveExportsField };