@ryanatkn/gro 0.143.3 → 0.144.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.
Files changed (51) hide show
  1. package/dist/esbuild_plugin_external_worker.d.ts +2 -2
  2. package/dist/esbuild_plugin_external_worker.d.ts.map +1 -1
  3. package/dist/esbuild_plugin_svelte.d.ts +2 -2
  4. package/dist/esbuild_plugin_svelte.d.ts.map +1 -1
  5. package/dist/esbuild_plugin_sveltekit_shim_alias.d.ts +2 -2
  6. package/dist/esbuild_plugin_sveltekit_shim_alias.d.ts.map +1 -1
  7. package/dist/esbuild_plugin_sveltekit_shim_app.d.ts +2 -2
  8. package/dist/esbuild_plugin_sveltekit_shim_app.d.ts.map +1 -1
  9. package/dist/esbuild_plugin_sveltekit_shim_env.d.ts +2 -2
  10. package/dist/esbuild_plugin_sveltekit_shim_env.d.ts.map +1 -1
  11. package/dist/filer.d.ts +3 -3
  12. package/dist/filer.d.ts.map +1 -1
  13. package/dist/filer.js +0 -2
  14. package/dist/gro_plugin_gen.d.ts +2 -2
  15. package/dist/gro_plugin_gen.d.ts.map +1 -1
  16. package/dist/gro_plugin_moss.d.ts +16 -0
  17. package/dist/gro_plugin_moss.d.ts.map +1 -0
  18. package/dist/gro_plugin_moss.js +89 -0
  19. package/dist/gro_plugin_server.d.ts +2 -2
  20. package/dist/gro_plugin_server.d.ts.map +1 -1
  21. package/dist/gro_plugin_sveltekit_app.d.ts +2 -2
  22. package/dist/gro_plugin_sveltekit_app.d.ts.map +1 -1
  23. package/dist/gro_plugin_sveltekit_library.d.ts +2 -2
  24. package/dist/gro_plugin_sveltekit_library.d.ts.map +1 -1
  25. package/dist/package.d.ts +14 -0
  26. package/dist/package.d.ts.map +1 -1
  27. package/dist/package.js +33 -19
  28. package/dist/package_json.d.ts +6 -4
  29. package/dist/package_json.d.ts.map +1 -1
  30. package/dist/package_json.js +12 -12
  31. package/dist/resolve_node_specifier.d.ts +2 -5
  32. package/dist/resolve_node_specifier.d.ts.map +1 -1
  33. package/dist/resolve_node_specifier.js +233 -44
  34. package/dist/watch_dir.d.ts +2 -2
  35. package/dist/watch_dir.d.ts.map +1 -1
  36. package/package.json +11 -7
  37. package/src/lib/esbuild_plugin_external_worker.ts +2 -2
  38. package/src/lib/esbuild_plugin_svelte.ts +2 -2
  39. package/src/lib/esbuild_plugin_sveltekit_shim_alias.ts +2 -2
  40. package/src/lib/esbuild_plugin_sveltekit_shim_app.ts +2 -2
  41. package/src/lib/esbuild_plugin_sveltekit_shim_env.ts +2 -2
  42. package/src/lib/filer.ts +3 -6
  43. package/src/lib/gro_plugin_gen.ts +2 -2
  44. package/src/lib/gro_plugin_moss.ts +122 -0
  45. package/src/lib/gro_plugin_server.ts +2 -2
  46. package/src/lib/gro_plugin_sveltekit_app.ts +2 -2
  47. package/src/lib/gro_plugin_sveltekit_library.ts +2 -2
  48. package/src/lib/package.ts +33 -19
  49. package/src/lib/package_json.ts +15 -14
  50. package/src/lib/resolve_node_specifier.ts +267 -49
  51. package/src/lib/watch_dir.ts +2 -2
@@ -1,17 +1,15 @@
1
- import {join} from 'node:path';
1
+ import {join, extname} from 'node:path';
2
2
  import {existsSync} from 'node:fs';
3
3
  import {DEV} from 'esm-env';
4
4
 
5
- import {Package_Json, Package_Json_Exports, load_package_json} from './package_json.js';
5
+ import {Export_Value, Package_Json, load_package_json} from './package_json.js';
6
6
  import {paths} from './paths.js';
7
7
  import {NODE_MODULES_DIRNAME} from './constants.js';
8
8
  import type {Resolved_Specifier} from './resolve_specifier.js';
9
+ import {escape_regexp} from '@ryanatkn/belt/regexp.js';
9
10
 
10
11
  /**
11
- * Like `resolve_specifier` but for Node specifiers,
12
- * typically those that aren't relative or absolute.
13
- * Optionally return `null` instead of throwing by setting
14
- * `throw_on_missing_package` to `false`.
12
+ * This likely has differences from Node - they should be fixed on a case-by-case basis.
15
13
  */
16
14
  export const resolve_node_specifier = (
17
15
  specifier: string,
@@ -19,8 +17,7 @@ export const resolve_node_specifier = (
19
17
  parent_path?: string,
20
18
  cache?: Record<string, Package_Json>,
21
19
  throw_on_missing_package = true,
22
- // TODO this needs to use `--conditions`/`-C` to determine the correct key
23
- exports_condition = DEV ? 'development' : 'default',
20
+ exports_conditions = DEV ? ['development', 'node', 'import'] : ['production', 'node', 'import'],
24
21
  ): Resolved_Specifier | null => {
25
22
  const raw = specifier.endsWith('?raw');
26
23
  const mapped_specifier = raw ? specifier.substring(0, specifier.length - 4) : specifier;
@@ -28,7 +25,6 @@ export const resolve_node_specifier = (
28
25
  // Parse the specifier
29
26
  let idx: number = -1;
30
27
  if (mapped_specifier[0] === '@') {
31
- // get the index of the second `/`
32
28
  let count = 0;
33
29
  for (let i = 0; i < mapped_specifier.length; i++) {
34
30
  if (mapped_specifier[i] === '/') count++;
@@ -40,12 +36,17 @@ export const resolve_node_specifier = (
40
36
  } else {
41
37
  idx = mapped_specifier.indexOf('/');
42
38
  }
39
+
43
40
  const pkg_name = idx === -1 ? mapped_specifier : mapped_specifier.substring(0, idx);
44
41
  const module_path = idx === -1 ? '' : mapped_specifier.substring(idx + 1);
45
42
  const subpath = module_path ? './' + module_path : '.';
46
43
  const package_dir = join(dir, NODE_MODULES_DIRNAME, pkg_name);
47
44
 
48
- if (!existsSync(package_dir)) {
45
+ // Check cache first
46
+ let package_json: Package_Json | undefined;
47
+ if (cache?.[pkg_name]) {
48
+ package_json = cache[pkg_name];
49
+ } else if (!existsSync(package_dir)) {
49
50
  if (throw_on_missing_package) {
50
51
  throw Error(
51
52
  `Package not found at ${package_dir} for specifier ${specifier}, you may need to install packages or fix the path` +
@@ -54,13 +55,31 @@ export const resolve_node_specifier = (
54
55
  } else {
55
56
  return null;
56
57
  }
58
+ } else {
59
+ package_json = load_package_json(package_dir, cache, false);
60
+ }
61
+
62
+ // Handle self-referencing
63
+ if (parent_path?.startsWith(package_dir)) {
64
+ if (!package_json.exports) {
65
+ throw Error(
66
+ `Self-referencing is only available if package.json has "exports" field: ${specifier}` +
67
+ (parent_path ? ` imported from ${parent_path}` : ''),
68
+ );
69
+ }
70
+ }
71
+
72
+ const exported = resolve_subpath(package_json, subpath);
73
+
74
+ if (typeof exported === 'string') {
75
+ const validated = validate_export_target(exported, throw_on_missing_package);
76
+ if (validated === null) {
77
+ return null;
78
+ }
57
79
  }
58
80
 
59
- const package_json = load_package_json(package_dir, cache, false);
60
- const {exported, exports_key} = resolve_subpath(package_json, specifier, subpath);
61
81
  if (!exported) {
62
82
  if (throw_on_missing_package) {
63
- // same error message as Node
64
83
  throw Error(
65
84
  `[ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath '${subpath}' is not defined by 'exports' in ${package_dir}/package.json` +
66
85
  (parent_path ? ` imported from ${parent_path}` : ''),
@@ -69,18 +88,20 @@ export const resolve_node_specifier = (
69
88
  return null;
70
89
  }
71
90
  }
72
- const exported_value = resolve_exported_value(exported, exports_key, exports_condition);
91
+
92
+ const exported_value = resolve_exported_value(exported, exports_conditions);
73
93
  if (exported_value === undefined) {
74
94
  if (throw_on_missing_package) {
75
95
  throw Error(
76
- `Package subpath '${subpath}' does not define the key '${exports_key}' in 'exports' in ${package_dir}/package.json` +
96
+ `No valid export found for subpath '${subpath}' in ${package_dir}/package.json with the following conditions: ${exports_conditions.join(', ')}` +
77
97
  (parent_path ? ` imported from ${parent_path}` : ''),
78
98
  );
79
99
  } else {
80
100
  return null;
81
101
  }
82
102
  }
83
- const path_id = join(package_dir, exported_value);
103
+
104
+ const path_id = normalize_extension(join(package_dir, exported_value));
84
105
 
85
106
  return {
86
107
  path_id,
@@ -92,47 +113,244 @@ export const resolve_node_specifier = (
92
113
  };
93
114
  };
94
115
 
95
- /**
96
- * Resolves the subpath of a package.json `exports` field based on the `specifier`.
97
- */
98
- const resolve_subpath = (
99
- package_json: Package_Json,
100
- specifier: string,
101
- subpath: string,
102
- ): {exported: Package_Json_Exports[string]; exports_key: string} => {
103
- const exports_key = specifier.endsWith('.svelte') ? 'svelte' : 'default';
116
+ const replace_wildcards = (pattern: string, wildcards: string[]): string => {
117
+ if (!pattern.includes('*')) return pattern;
104
118
 
105
- const exported =
106
- subpath === '.' && !package_json.exports
107
- ? {[exports_key]: package_json.main}
108
- : package_json.exports?.[subpath];
119
+ let result = pattern;
120
+ let wildcard_index = 0;
121
+ while (result.includes('*') && wildcard_index < wildcards.length) {
122
+ result = result.replace('*', wildcards[wildcard_index++]);
123
+ }
124
+ return result;
125
+ };
126
+
127
+ const resolve_subpath = (package_json: Package_Json, subpath: string): unknown => {
128
+ // If no exports field exists, fallback to main field for the root subpath
129
+ if (!package_json.exports) {
130
+ return subpath === '.' && package_json.main ? package_json.main : null;
131
+ }
109
132
 
110
- return {exported, exports_key};
133
+ const exports = package_json.exports;
134
+
135
+ // Handle exports sugar syntax
136
+ if (typeof exports === 'string') {
137
+ return subpath === '.' ? exports : null;
138
+ }
139
+
140
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
141
+ if (typeof exports === 'object' && exports !== null) {
142
+ // Check for exact match first
143
+ if (subpath in exports) {
144
+ return exports[subpath];
145
+ }
146
+
147
+ // Sort patterns by specificity
148
+ const patterns = Object.entries(exports)
149
+ .filter(([pattern]) => pattern.includes('*'))
150
+ .map(([pattern, target]) => ({
151
+ pattern,
152
+ target,
153
+ static_prefix: pattern.split('*')[0],
154
+ segments: pattern.split('/').length,
155
+ wildcards: (pattern.match(/\*/g) ?? []).length,
156
+ }))
157
+ .sort((a, b) => {
158
+ // Sort by static prefix length first
159
+ const prefix_diff = b.static_prefix.length - a.static_prefix.length;
160
+ if (prefix_diff !== 0) return prefix_diff;
161
+
162
+ // Then by number of segments
163
+ const segment_diff = b.segments - a.segments;
164
+ if (segment_diff !== 0) return segment_diff;
165
+
166
+ // Then by number of wildcards (fewer is more specific)
167
+ const wildcard_diff = a.wildcards - b.wildcards;
168
+ if (wildcard_diff !== 0) return wildcard_diff;
169
+
170
+ // Finally by total pattern length
171
+ return b.pattern.length - a.pattern.length;
172
+ });
173
+
174
+ // Track matched wildcards for later use
175
+ let matched_wildcards: string[] = [];
176
+
177
+ // Check patterns in order of specificity
178
+ for (const {pattern, target} of patterns) {
179
+ // Convert pattern to regex, handling path segments properly
180
+ const regex_pattern = pattern.split('*').map(escape_regexp).join('([^/]+)');
181
+ const regex = new RegExp(`^${regex_pattern}$`);
182
+ const match = subpath.match(regex);
183
+
184
+ if (match) {
185
+ // If this is a null pattern and it matches, block access
186
+ if (target === null) return null;
187
+
188
+ // Extract captured wildcards and store them
189
+ matched_wildcards = match.slice(1);
190
+
191
+ if (typeof target === 'string') {
192
+ return replace_wildcards(target, matched_wildcards);
193
+ }
194
+
195
+ if (typeof target === 'object' && target !== null) {
196
+ // For conditional exports, return an object with resolved wildcards
197
+ return Object.fromEntries(
198
+ Object.entries(target).map(([key, value]) => {
199
+ if (typeof value === 'string') {
200
+ return [key, replace_wildcards(value, matched_wildcards)];
201
+ }
202
+ // Handle nested conditions
203
+ if (typeof value === 'object' && value !== null) {
204
+ return [
205
+ key,
206
+ Object.fromEntries(
207
+ Object.entries(value).map(([nested_key, nested_value]) => [
208
+ nested_key,
209
+ typeof nested_value === 'string'
210
+ ? replace_wildcards(nested_value, matched_wildcards)
211
+ : nested_value,
212
+ ]),
213
+ ),
214
+ ];
215
+ }
216
+ return [key, value];
217
+ }),
218
+ );
219
+ }
220
+ }
221
+ }
222
+
223
+ // Handle catch-all patterns for remaining cases
224
+ const catch_all_patterns = patterns.filter(
225
+ ({pattern}) => pattern.endsWith('/*') || pattern === './*',
226
+ );
227
+
228
+ for (const {pattern, target} of catch_all_patterns) {
229
+ const base_pattern = pattern.slice(0, -1); // Remove trailing '*'
230
+ if (subpath.startsWith(base_pattern)) {
231
+ if (target === null) return null;
232
+
233
+ const remainder = subpath.slice(base_pattern.length);
234
+ if (typeof target === 'string') {
235
+ return target.slice(0, -1) + remainder;
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ return null;
111
242
  };
112
243
 
113
- /**
114
- * Resolves the exported value based on the exports key and condition.
115
- */
116
244
  const resolve_exported_value = (
117
- exported: Exclude<Package_Json_Exports[string], undefined>,
118
- exports_key: string,
119
- exports_condition: string,
245
+ exported: Export_Value,
246
+ conditions: string[],
120
247
  ): string | undefined => {
121
- let exported_value = typeof exported === 'string' ? exported : exported[exports_key];
248
+ if (typeof exported === 'string') {
249
+ return exported;
250
+ }
251
+
252
+ if (typeof exported !== 'object' || exported === null) {
253
+ return undefined;
254
+ }
255
+
256
+ const exported_obj = exported as Record<string, unknown>;
257
+
258
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
259
+ let default_value: Export_Value | undefined;
260
+
261
+ // For each key in exported_obj, in order
262
+ for (const [condition, value] of Object.entries(exported_obj)) {
263
+ // Skip invalid conditions
264
+ if (!is_valid_condition(condition)) {
265
+ continue;
266
+ }
267
+
268
+ if (condition === 'default') {
269
+ // Store default value to try last
270
+ default_value = value;
271
+ } else if (conditions.includes(condition)) {
272
+ const resolved = resolve_exported_value(value, conditions);
273
+ if (resolved !== undefined) {
274
+ return resolved;
275
+ }
276
+ }
277
+ }
278
+
279
+ // If no conditions matched, try default
280
+ if (default_value !== undefined) {
281
+ const resolved = resolve_exported_value(default_value, conditions);
282
+ if (resolved !== undefined) {
283
+ return resolved;
284
+ }
285
+ }
286
+
287
+ return undefined;
288
+ };
289
+
290
+ const is_valid_condition = (condition: string): boolean => {
291
+ if (
292
+ condition.length === 0 ||
293
+ condition.startsWith('.') ||
294
+ condition.includes(',') ||
295
+ /^\d+$/.test(condition)
296
+ ) {
297
+ return false;
298
+ }
299
+ return /^[a-zA-Z0-9:_\-=]+$/.test(condition);
300
+ };
301
+
302
+ const normalize_extension = (path: string): string => {
303
+ if (path.endsWith('.d.ts')) {
304
+ return path.slice(0, -5) + '.js';
305
+ }
122
306
 
123
- // TODO best effort fallback, support `default` but fall back to `import` or `node` as the exports key.
124
- if (exported_value === undefined && typeof exported !== 'string' && exports_key === 'default') {
125
- exported_value = exported.import ?? exported.node;
307
+ // No extension handling needed if path already has an extension
308
+ if (extname(path)) {
309
+ return path;
126
310
  }
127
311
 
128
- // Possibly resolve to conditional exports.
129
- exported_value =
130
- exported_value === undefined || typeof exported_value === 'string'
131
- ? exported_value
132
- : (exported_value[exports_condition] ??
133
- exported_value.default ??
134
- exported_value.import ??
135
- exported_value.node); // TODO this fallback has corner case bugs for off-spec exports
312
+ // If no extension at all, add .js
313
+ return path + '.js';
314
+ };
315
+
316
+ const validate_export_target = (target: string, throw_on_missing_package: boolean): void | null => {
317
+ // Must start with './'
318
+ if (!target.startsWith('./') && !target.startsWith('../')) {
319
+ if (throw_on_missing_package) {
320
+ throw new Error('ERR_INVALID_PACKAGE_TARGET: Export target must start with "./" or "../"');
321
+ } else {
322
+ return null;
323
+ }
324
+ }
136
325
 
137
- return exported_value;
326
+ // Can't contain node_modules
327
+ if (target.includes('node_modules')) {
328
+ if (throw_on_missing_package) {
329
+ throw new Error('ERR_INVALID_PACKAGE_TARGET: Export target cannot contain node_modules');
330
+ } else {
331
+ return null;
332
+ }
333
+ }
334
+
335
+ // Check for package boundary escape
336
+ const parts = target.split('/');
337
+ let depth = 0;
338
+
339
+ for (const part of parts) {
340
+ if (part === '..') {
341
+ depth--;
342
+ // If we go above root, it's escaping the package boundary
343
+ if (depth < 0) {
344
+ if (throw_on_missing_package) {
345
+ throw new Error(
346
+ 'ERR_INVALID_PACKAGE_TARGET: Export target cannot escape package boundary',
347
+ );
348
+ } else {
349
+ return null;
350
+ }
351
+ }
352
+ } else if (part !== '.' && part !== '') {
353
+ depth++;
354
+ }
355
+ }
138
356
  };
@@ -20,7 +20,7 @@ export interface Watcher_Change {
20
20
  export type Watcher_Change_Type = 'add' | 'update' | 'delete';
21
21
  export type Watcher_Change_Callback = (change: Watcher_Change) => void;
22
22
 
23
- export interface Options {
23
+ export interface Watch_Dir_Options {
24
24
  dir: string;
25
25
  on_change: Watcher_Change_Callback;
26
26
  filter?: Path_Filter | null | undefined;
@@ -41,7 +41,7 @@ export const watch_dir = ({
41
41
  filter,
42
42
  absolute = true,
43
43
  chokidar,
44
- }: Options): Watch_Node_Fs => {
44
+ }: Watch_Dir_Options): Watch_Node_Fs => {
45
45
  let watcher: FSWatcher | undefined;
46
46
  let initing: Deferred<void> | undefined;
47
47