@ryanatkn/gro 0.179.0 → 0.181.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 (196) hide show
  1. package/README.md +1 -1
  2. package/dist/build.task.d.ts +2 -0
  3. package/dist/build.task.d.ts.map +1 -1
  4. package/dist/build.task.js +15 -11
  5. package/dist/build_cache.d.ts +4 -4
  6. package/dist/build_cache.d.ts.map +1 -1
  7. package/dist/build_cache.js +54 -44
  8. package/dist/changelog.d.ts +2 -2
  9. package/dist/changelog.d.ts.map +1 -1
  10. package/dist/changeset.task.js +11 -11
  11. package/dist/check.task.js +2 -2
  12. package/dist/child_process_logging.js +1 -1
  13. package/dist/clean.task.js +2 -2
  14. package/dist/clean_fs.d.ts +1 -1
  15. package/dist/clean_fs.d.ts.map +1 -1
  16. package/dist/clean_fs.js +3 -4
  17. package/dist/cli.d.ts +7 -7
  18. package/dist/cli.d.ts.map +1 -1
  19. package/dist/cli.js +11 -12
  20. package/dist/commit.task.js +2 -2
  21. package/dist/deploy.task.d.ts +7 -0
  22. package/dist/deploy.task.d.ts.map +1 -1
  23. package/dist/deploy.task.js +30 -17
  24. package/dist/disknode.d.ts +1 -1
  25. package/dist/disknode.d.ts.map +1 -1
  26. package/dist/esbuild_helpers.d.ts +1 -1
  27. package/dist/esbuild_helpers.d.ts.map +1 -1
  28. package/dist/esbuild_plugin_external_worker.d.ts +1 -1
  29. package/dist/esbuild_plugin_external_worker.d.ts.map +1 -1
  30. package/dist/esbuild_plugin_external_worker.js +1 -1
  31. package/dist/esbuild_plugin_svelte.js +4 -4
  32. package/dist/esbuild_plugin_sveltekit_local_imports.js +2 -2
  33. package/dist/esbuild_plugin_sveltekit_shim_alias.js +1 -1
  34. package/dist/filer.d.ts +4 -4
  35. package/dist/filer.d.ts.map +1 -1
  36. package/dist/filer.js +105 -54
  37. package/dist/format.task.js +1 -1
  38. package/dist/format_directory.d.ts +2 -2
  39. package/dist/format_directory.d.ts.map +1 -1
  40. package/dist/format_file.js +1 -1
  41. package/dist/gen.d.ts +5 -5
  42. package/dist/gen.d.ts.map +1 -1
  43. package/dist/gen.js +28 -22
  44. package/dist/gen.task.js +3 -3
  45. package/dist/gen_helpers.d.ts +3 -3
  46. package/dist/gen_helpers.d.ts.map +1 -1
  47. package/dist/github.d.ts +2 -2
  48. package/dist/github.d.ts.map +1 -1
  49. package/dist/github.js +1 -1
  50. package/dist/gro.config.default.js +1 -1
  51. package/dist/gro_config.d.ts +1 -1
  52. package/dist/gro_config.d.ts.map +1 -1
  53. package/dist/gro_config.js +4 -4
  54. package/dist/gro_helpers.d.ts +1 -1
  55. package/dist/gro_helpers.d.ts.map +1 -1
  56. package/dist/gro_helpers.js +1 -1
  57. package/dist/gro_plugin_gen.js +4 -4
  58. package/dist/gro_plugin_server.d.ts +2 -2
  59. package/dist/gro_plugin_server.d.ts.map +1 -1
  60. package/dist/gro_plugin_server.js +7 -7
  61. package/dist/gro_plugin_sveltekit_app.d.ts.map +1 -1
  62. package/dist/gro_plugin_sveltekit_app.js +40 -36
  63. package/dist/gro_plugin_sveltekit_library.js +3 -2
  64. package/dist/input_path.d.ts +5 -5
  65. package/dist/input_path.d.ts.map +1 -1
  66. package/dist/input_path.js +17 -15
  67. package/dist/invoke.js +2 -2
  68. package/dist/invoke_task.d.ts +2 -2
  69. package/dist/invoke_task.d.ts.map +1 -1
  70. package/dist/invoke_task.js +5 -5
  71. package/dist/lint.task.js +2 -2
  72. package/dist/loader.js +2 -2
  73. package/dist/modules.d.ts +3 -3
  74. package/dist/modules.d.ts.map +1 -1
  75. package/dist/modules.js +4 -4
  76. package/dist/package_json.d.ts +6 -6
  77. package/dist/package_json.d.ts.map +1 -1
  78. package/dist/package_json.js +14 -16
  79. package/dist/parse_exports.d.ts +4 -4
  80. package/dist/parse_exports.d.ts.map +1 -1
  81. package/dist/parse_exports_context.d.ts +1 -1
  82. package/dist/parse_exports_context.d.ts.map +1 -1
  83. package/dist/parse_imports.d.ts +2 -2
  84. package/dist/parse_imports.d.ts.map +1 -1
  85. package/dist/parse_imports.js +1 -1
  86. package/dist/paths.d.ts +1 -1
  87. package/dist/paths.d.ts.map +1 -1
  88. package/dist/paths.js +1 -1
  89. package/dist/publish.task.js +8 -8
  90. package/dist/reinstall.task.js +1 -1
  91. package/dist/release.task.js +1 -1
  92. package/dist/resolve.task.js +2 -2
  93. package/dist/resolve_specifier.d.ts +2 -2
  94. package/dist/resolve_specifier.d.ts.map +1 -1
  95. package/dist/resolve_specifier.js +5 -4
  96. package/dist/run.task.js +2 -2
  97. package/dist/run_gen.d.ts +2 -2
  98. package/dist/run_gen.d.ts.map +1 -1
  99. package/dist/run_gen.js +10 -9
  100. package/dist/run_task.d.ts +2 -2
  101. package/dist/run_task.d.ts.map +1 -1
  102. package/dist/run_task.js +4 -4
  103. package/dist/source_json.d.ts +5 -5
  104. package/dist/source_json.d.ts.map +1 -1
  105. package/dist/source_json.js +18 -17
  106. package/dist/svelte_config.js +1 -1
  107. package/dist/sveltekit_helpers.d.ts +3 -3
  108. package/dist/sveltekit_helpers.d.ts.map +1 -1
  109. package/dist/sveltekit_helpers.js +4 -4
  110. package/dist/sveltekit_shim_app_forms.js +1 -1
  111. package/dist/sveltekit_shim_app_navigation.js +1 -1
  112. package/dist/sveltekit_shim_app_paths.js +1 -1
  113. package/dist/sveltekit_shim_env.js +1 -1
  114. package/dist/sync.task.js +1 -1
  115. package/dist/task.d.ts +5 -5
  116. package/dist/task.d.ts.map +1 -1
  117. package/dist/task.js +5 -5
  118. package/dist/task_logging.d.ts +1 -1
  119. package/dist/task_logging.d.ts.map +1 -1
  120. package/dist/task_logging.js +2 -2
  121. package/dist/test.task.d.ts.map +1 -1
  122. package/dist/test.task.js +5 -4
  123. package/dist/typecheck.task.js +4 -4
  124. package/dist/upgrade.task.js +6 -6
  125. package/dist/watch_dir.d.ts +1 -1
  126. package/dist/watch_dir.d.ts.map +1 -1
  127. package/dist/watch_dir.js +2 -2
  128. package/package.json +8 -8
  129. package/src/lib/build.task.ts +16 -11
  130. package/src/lib/build_cache.ts +81 -65
  131. package/src/lib/changelog.ts +2 -2
  132. package/src/lib/changeset.task.ts +12 -12
  133. package/src/lib/check.task.ts +2 -2
  134. package/src/lib/child_process_logging.ts +1 -1
  135. package/src/lib/clean.task.ts +2 -2
  136. package/src/lib/clean_fs.ts +4 -4
  137. package/src/lib/cli.ts +18 -17
  138. package/src/lib/commit.task.ts +2 -2
  139. package/src/lib/deploy.task.ts +33 -16
  140. package/src/lib/disknode.ts +1 -1
  141. package/src/lib/esbuild_helpers.ts +1 -1
  142. package/src/lib/esbuild_plugin_external_worker.ts +3 -3
  143. package/src/lib/esbuild_plugin_svelte.ts +4 -4
  144. package/src/lib/esbuild_plugin_sveltekit_local_imports.ts +2 -2
  145. package/src/lib/esbuild_plugin_sveltekit_shim_alias.ts +1 -1
  146. package/src/lib/filer.ts +117 -58
  147. package/src/lib/format.task.ts +1 -1
  148. package/src/lib/format_directory.ts +2 -2
  149. package/src/lib/format_file.ts +1 -1
  150. package/src/lib/gen.task.ts +3 -3
  151. package/src/lib/gen.ts +56 -50
  152. package/src/lib/gen_helpers.ts +3 -3
  153. package/src/lib/github.ts +2 -2
  154. package/src/lib/gro.config.default.ts +1 -1
  155. package/src/lib/gro_config.ts +5 -5
  156. package/src/lib/gro_helpers.ts +1 -1
  157. package/src/lib/gro_plugin_gen.ts +4 -4
  158. package/src/lib/gro_plugin_server.ts +9 -9
  159. package/src/lib/gro_plugin_sveltekit_app.ts +44 -40
  160. package/src/lib/gro_plugin_sveltekit_library.ts +3 -3
  161. package/src/lib/input_path.ts +23 -24
  162. package/src/lib/invoke.ts +2 -2
  163. package/src/lib/invoke_task.ts +5 -5
  164. package/src/lib/lint.task.ts +2 -2
  165. package/src/lib/loader.ts +2 -2
  166. package/src/lib/modules.ts +7 -7
  167. package/src/lib/package_json.ts +19 -23
  168. package/src/lib/parse_exports.ts +4 -4
  169. package/src/lib/parse_exports_context.ts +2 -2
  170. package/src/lib/parse_imports.ts +3 -3
  171. package/src/lib/paths.ts +2 -2
  172. package/src/lib/publish.task.ts +8 -8
  173. package/src/lib/reinstall.task.ts +1 -1
  174. package/src/lib/release.task.ts +1 -1
  175. package/src/lib/resolve.task.ts +2 -2
  176. package/src/lib/resolve_specifier.ts +9 -5
  177. package/src/lib/run.task.ts +2 -2
  178. package/src/lib/run_gen.ts +18 -13
  179. package/src/lib/run_task.ts +6 -6
  180. package/src/lib/source_json.ts +26 -22
  181. package/src/lib/svelte_config.ts +1 -1
  182. package/src/lib/sveltekit_helpers.ts +7 -7
  183. package/src/lib/sveltekit_shim_app_forms.ts +1 -1
  184. package/src/lib/sveltekit_shim_app_navigation.ts +1 -1
  185. package/src/lib/sveltekit_shim_app_paths.ts +1 -1
  186. package/src/lib/sync.task.ts +1 -1
  187. package/src/lib/task.ts +16 -14
  188. package/src/lib/task_logging.ts +3 -3
  189. package/src/lib/test.task.ts +4 -3
  190. package/src/lib/typecheck.task.ts +4 -4
  191. package/src/lib/upgrade.task.ts +6 -6
  192. package/src/lib/watch_dir.ts +3 -3
  193. package/dist/search_fs.d.ts +0 -26
  194. package/dist/search_fs.d.ts.map +0 -1
  195. package/dist/search_fs.js +0 -52
  196. package/src/lib/search_fs.ts +0 -100
@@ -1,10 +1,10 @@
1
- import {spawn} from '@ryanatkn/belt/process.js';
2
- import {print_error} from '@ryanatkn/belt/print.js';
1
+ import {spawn} from '@fuzdev/fuz_util/process.js';
2
+ import {print_error} from '@fuzdev/fuz_util/print.js';
3
3
  import {styleText as st} from 'node:util';
4
4
  import {z} from 'zod';
5
- import {cp, mkdir, rm} from 'node:fs/promises';
5
+ import {cp, mkdir, readdir, rm} from 'node:fs/promises';
6
6
  import {join, resolve} from 'node:path';
7
- import {existsSync, readdirSync} from 'node:fs';
7
+ import {fs_exists, fs_empty_dir} from '@fuzdev/fuz_util/fs.js';
8
8
  import {
9
9
  git_check_clean_workspace,
10
10
  git_checkout,
@@ -20,8 +20,7 @@ import {
20
20
  git_check_setting_pull_rebase,
21
21
  git_clone_locally,
22
22
  git_current_branch_name,
23
- } from '@ryanatkn/belt/git.js';
24
- import {fs_empty_dir} from '@ryanatkn/belt/fs.js';
23
+ } from '@fuzdev/fuz_util/git.js';
25
24
 
26
25
  import {TaskError, type Task} from './task.ts';
27
26
  import {print_path} from './paths.ts';
@@ -72,6 +71,19 @@ export const Args = z.strictObject({
72
71
  .default(false),
73
72
  build: z.boolean().meta({description: 'dual of no-build'}).default(true),
74
73
  'no-build': z.boolean().meta({description: 'opt out of building'}).default(false),
74
+ sync: z.boolean().meta({description: 'dual of no-sync'}).default(true),
75
+ 'no-sync': z.boolean().meta({description: 'opt out of gro sync in build'}).default(false),
76
+ gen: z.boolean().meta({description: 'dual of no-gen'}).default(true),
77
+ 'no-gen': z.boolean().meta({description: 'opt out of gro gen in build'}).default(false),
78
+ install: z.boolean().meta({description: 'dual of no-install'}).default(true),
79
+ 'no-install': z
80
+ .boolean()
81
+ .meta({description: 'opt out of installing packages before building'})
82
+ .default(false),
83
+ force_build: z
84
+ .boolean()
85
+ .meta({description: 'force a fresh build, ignoring the cache'})
86
+ .default(false),
75
87
  pull: z.boolean().meta({description: 'dual of no-pull'}).default(true),
76
88
  'no-pull': z.boolean().meta({description: 'opt out of git pull'}).default(false),
77
89
  });
@@ -93,6 +105,10 @@ export const task: Task<Args> = {
93
105
  dangerous,
94
106
  reset,
95
107
  build,
108
+ sync,
109
+ gen,
110
+ install,
111
+ force_build,
96
112
  pull,
97
113
  } = args;
98
114
 
@@ -154,7 +170,7 @@ export const task: Task<Args> = {
154
170
  // First, check if the deploy dir exists, and if so, attempt to sync it.
155
171
  // If anything goes wrong, delete the directory and we'll initialize it
156
172
  // using the same code path as if it didn't exist in the first place.
157
- if (existsSync(resolved_deploy_dir)) {
173
+ if (await fs_exists(resolved_deploy_dir)) {
158
174
  if (target !== (await git_current_branch_name(target_spawn_options))) {
159
175
  // We're in a bad state because the target branch has changed,
160
176
  // so delete the directory and continue as if it wasn't there.
@@ -175,7 +191,7 @@ export const task: Task<Args> = {
175
191
 
176
192
  // Second, initialize the deploy dir if needed.
177
193
  // It may not exist, or it may have been deleted after failing to sync above.
178
- if (!existsSync(resolved_deploy_dir)) {
194
+ if (!(await fs_exists(resolved_deploy_dir))) {
179
195
  const local_deploy_branch_exists = await git_local_branch_exists(target);
180
196
  await git_fetch(origin, ('+' + target + ':' + target) as GitBranch); // fetch+merge and allow non-fastforward updates with the +
181
197
  await git_clone_locally(origin, target, dir, resolved_deploy_dir);
@@ -193,7 +209,7 @@ export const task: Task<Args> = {
193
209
  // Remote target branch does not exist, so start from scratch
194
210
 
195
211
  // Delete the deploy dir and recreate it
196
- if (existsSync(resolved_deploy_dir)) {
212
+ if (await fs_exists(resolved_deploy_dir)) {
197
213
  await rm(resolved_deploy_dir, {recursive: true});
198
214
  await mkdir(resolved_deploy_dir, {recursive: true});
199
215
  }
@@ -222,14 +238,14 @@ export const task: Task<Args> = {
222
238
  // Build
223
239
  try {
224
240
  if (build) {
225
- await invoke_task('build');
241
+ await invoke_task('build', {sync, gen, install, force_build});
226
242
  }
227
- } catch (err) {
243
+ } catch (error) {
228
244
  log.error(
229
245
  st('red', 'build failed'),
230
246
  'but',
231
247
  st('green', 'no changes were made to git'),
232
- print_error(err),
248
+ print_error(error),
233
249
  );
234
250
  if (dry) {
235
251
  log.info(st('red', 'dry deploy failed'));
@@ -238,13 +254,14 @@ export const task: Task<Args> = {
238
254
  }
239
255
 
240
256
  // Verify build output exists
241
- if (!existsSync(build_dir)) {
257
+ if (!(await fs_exists(build_dir))) {
242
258
  throw new TaskError(`Directory to deploy does not exist after building: ${build_dir}`);
243
259
  }
244
260
 
245
261
  // Copy the build
262
+ const build_entries = await readdir(build_dir);
246
263
  await Promise.all(
247
- readdirSync(build_dir).map((path) =>
264
+ build_entries.map((path) =>
248
265
  cp(join(build_dir, path), join(resolved_deploy_dir, path), {recursive: true}),
249
266
  ),
250
267
  );
@@ -260,8 +277,8 @@ export const task: Task<Args> = {
260
277
  await spawn('git', ['add', '.', '-f'], target_spawn_options);
261
278
  await spawn('git', ['commit', '-m', 'deployment'], target_spawn_options);
262
279
  await spawn('git', ['push', origin, target, '-f'], target_spawn_options); // force push because we may be resetting the branch, see the checks above to make this safer
263
- } catch (err) {
264
- log.error(st('red', 'updating git failed:'), print_error(err));
280
+ } catch (error) {
281
+ log.error(st('red', 'updating git failed:'), print_error(error));
265
282
  throw new TaskError(`Deploy failed in a bad state: built but not pushed, see error above.`);
266
283
  }
267
284
 
@@ -1,4 +1,4 @@
1
- import type {PathId} from '@ryanatkn/belt/path.js';
1
+ import type {PathId} from '@fuzdev/fuz_util/path.js';
2
2
 
3
3
  // TODO extract more here from Filer
4
4
 
@@ -1,5 +1,5 @@
1
1
  import {styleText as st} from 'node:util';
2
- import type {Logger} from '@ryanatkn/belt/log.js';
2
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
3
3
  import type * as esbuild from 'esbuild';
4
4
 
5
5
  import type {ParsedSvelteConfig} from './svelte_config.ts';
@@ -1,8 +1,8 @@
1
1
  import * as esbuild from 'esbuild';
2
- import type {Logger} from '@ryanatkn/belt/log.js';
2
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
3
3
  import {basename} from 'node:path';
4
4
  import type {CompileOptions, ModuleCompileOptions, PreprocessorGroup} from 'svelte/compiler';
5
- import type {PathId} from '@ryanatkn/belt/path.js';
5
+ import type {PathId} from '@fuzdev/fuz_util/path.js';
6
6
 
7
7
  import {print_build_result, to_define_import_meta_env} from './esbuild_helpers.ts';
8
8
  import {resolve_specifier} from './resolve_specifier.ts';
@@ -84,7 +84,7 @@ export const esbuild_plugin_external_worker = ({
84
84
  };
85
85
 
86
86
  build.onResolve({filter: /\.worker(|\.js|\.ts)$/}, async ({path, resolveDir}) => {
87
- const parsed = resolve_specifier(path, resolveDir);
87
+ const parsed = await resolve_specifier(path, resolveDir);
88
88
  const {specifier, path_id, namespace} = parsed;
89
89
  const build_result = await build_worker(path_id);
90
90
  if (log) print_build_result(log, build_result);
@@ -71,8 +71,8 @@ export const esbuild_plugin_svelte = (options: EsbuildPluginSvelteOptions): esbu
71
71
  contents,
72
72
  warnings: warnings.map((w) => convert_svelte_message_to_esbuild(filename, source, w)),
73
73
  };
74
- } catch (err) {
75
- return {errors: [convert_svelte_message_to_esbuild(path, source, err)]};
74
+ } catch (error) {
75
+ return {errors: [convert_svelte_message_to_esbuild(path, source, error)]};
76
76
  }
77
77
  });
78
78
 
@@ -90,8 +90,8 @@ export const esbuild_plugin_svelte = (options: EsbuildPluginSvelteOptions): esbu
90
90
  contents,
91
91
  warnings: warnings.map((w) => convert_svelte_message_to_esbuild(filename, source, w)),
92
92
  };
93
- } catch (err) {
94
- return {errors: [convert_svelte_message_to_esbuild(path, source, err)]};
93
+ } catch (error) {
94
+ return {errors: [convert_svelte_message_to_esbuild(path, source, error)]};
95
95
  }
96
96
  });
97
97
  },
@@ -13,10 +13,10 @@ import {EVERYTHING_MATCHER} from './constants.ts';
13
13
  export const esbuild_plugin_sveltekit_local_imports = (): esbuild.Plugin => ({
14
14
  name: 'sveltekit_local_imports',
15
15
  setup: (build) => {
16
- build.onResolve({filter: /^(\/|\.)/}, (args) => {
16
+ build.onResolve({filter: /^(\/|\.)/}, async (args) => {
17
17
  const {path, importer} = args;
18
18
  if (!importer) return {path};
19
- const {path_id, namespace} = resolve_specifier(path, dirname(importer));
19
+ const {path_id, namespace} = await resolve_specifier(path, dirname(importer));
20
20
  return {path: path_id, namespace}; // `namespace` may be `undefined`, but esbuild needs the absolute path for json etc
21
21
  });
22
22
  build.onLoad(
@@ -1,5 +1,5 @@
1
1
  import type * as esbuild from 'esbuild';
2
- import {escape_regexp} from '@ryanatkn/belt/regexp.js';
2
+ import {escape_regexp} from '@fuzdev/fuz_util/regexp.js';
3
3
  import {join} from 'node:path';
4
4
 
5
5
  export interface EsbuildPluginSveltekitShimAliasOptions {
package/src/lib/filer.ts CHANGED
@@ -1,13 +1,13 @@
1
- import {EMPTY_OBJECT} from '@ryanatkn/belt/object.js';
2
- import {readFileSync, statSync} from 'node:fs';
1
+ import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
2
+ import {readFile, stat} from 'node:fs/promises';
3
3
  import {dirname, resolve} from 'node:path';
4
- import type {OmitStrict} from '@ryanatkn/belt/types.js';
4
+ import type {OmitStrict} from '@fuzdev/fuz_util/types.js';
5
5
  import {isBuiltin} from 'node:module';
6
6
  import {fileURLToPath, pathToFileURL} from 'node:url';
7
- import {UnreachableError} from '@ryanatkn/belt/error.js';
8
- import type {Logger} from '@ryanatkn/belt/log.js';
9
- import type {PackageJson} from '@ryanatkn/belt/package_json.js';
10
- import type {FileFilter, PathId} from '@ryanatkn/belt/path.js';
7
+ import {UnreachableError} from '@fuzdev/fuz_util/error.js';
8
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
9
+ import type {PackageJson} from '@fuzdev/fuz_util/package_json.js';
10
+ import type {FileFilter, PathId} from '@fuzdev/fuz_util/path.js';
11
11
 
12
12
  import {
13
13
  watch_dir,
@@ -51,6 +51,9 @@ export class Filer {
51
51
  #initing: Promise<void> | undefined;
52
52
  #closing: Promise<void> | undefined;
53
53
 
54
+ #change_queue: Array<WatcherChange> = [];
55
+ #processing_promise: Promise<void> | null = null;
56
+
54
57
  constructor(options: FilerOptions = EMPTY_OBJECT) {
55
58
  this.#watch_dir = options.watch_dir ?? watch_dir;
56
59
  this.#watch_dir_options = options.watch_dir_options ?? EMPTY_OBJECT;
@@ -82,9 +85,11 @@ export class Filer {
82
85
  dependencies: new Map(),
83
86
  };
84
87
  this.files.set(id, file);
85
- // TODO this may need to be batched/deferred
88
+ // Defer external file change notification to avoid reentrancy during queue processing
86
89
  if (file.external) {
87
- this.#on_change({type: 'add', path: file.id, is_directory: false});
90
+ queueMicrotask(() => {
91
+ this.#on_change({type: 'add', path: file.id, is_directory: false});
92
+ });
88
93
  }
89
94
  return file;
90
95
  };
@@ -117,10 +122,10 @@ export class Filer {
117
122
  this.#initing = this.#init();
118
123
  try {
119
124
  await this.#initing;
120
- } catch (err) {
125
+ } catch (error) {
121
126
  // use shared cleanup logic
122
127
  this.#cleanup();
123
- throw err;
128
+ throw error;
124
129
  } finally {
125
130
  this.#initing = undefined;
126
131
  }
@@ -136,6 +141,9 @@ export class Filer {
136
141
  try {
137
142
  await watcher.init();
138
143
 
144
+ // Wait for any queued changes from init to be processed
145
+ await this.#drain_queue();
146
+
139
147
  // check if close() was called during init
140
148
  if (this.#closing) {
141
149
  await watcher.close();
@@ -144,14 +152,14 @@ export class Filer {
144
152
 
145
153
  // only set after successful init and not closing
146
154
  this.#watching = watcher;
147
- } catch (err) {
155
+ } catch (error) {
148
156
  // clean up watcher on error, but don't let close error mask init error
149
157
  try {
150
158
  await watcher.close();
151
159
  } catch {
152
160
  // ignore close errors - init error is more important
153
161
  }
154
- throw err;
162
+ throw error;
155
163
  }
156
164
  }
157
165
 
@@ -169,6 +177,8 @@ export class Filer {
169
177
  this.#listeners.clear();
170
178
  this.files.clear();
171
179
  this.#watching = undefined;
180
+ this.#change_queue = [];
181
+ this.#processing_promise = null;
172
182
  // #initing is handled in finally block of init()
173
183
  }
174
184
 
@@ -214,20 +224,22 @@ export class Filer {
214
224
  this.#cleanup();
215
225
  }
216
226
 
217
- #update(id: PathId): Disknode | null {
227
+ async #update(id: PathId): Promise<Disknode | null> {
218
228
  const file = this.get_or_create(id);
219
229
 
220
- let stats: ReturnType<typeof statSync> | null = null;
230
+ let stats: Awaited<ReturnType<typeof stat>> | null = null;
221
231
  let new_contents: string | null = null; // TODO need to lazily load contents, probably turn `Disknode` into a class
222
232
 
223
233
  try {
224
- stats = statSync(id);
225
- new_contents = readFileSync(id, 'utf8');
226
- } catch (err) {
227
- if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
228
- throw err;
234
+ [stats, new_contents] = await Promise.all([stat(id), readFile(id, 'utf8')]);
235
+ } catch (error) {
236
+ const code = (error as NodeJS.ErrnoException).code;
237
+ // Treat file as deleted/inaccessible for common error codes
238
+ if (code === 'ENOENT' || code === 'EACCES' || code === 'EPERM') {
239
+ // File doesn't exist or is inaccessible, treat as deleted
240
+ } else {
241
+ throw error;
229
242
  }
230
- // ENOENT: file doesn't exist, treat as deleted
231
243
  }
232
244
 
233
245
  const old_mtime = file.mtime;
@@ -245,7 +257,14 @@ export class Filer {
245
257
  const dependencies_before = new Set(file.dependencies.keys());
246
258
  const dependencies_removed = new Set(dependencies_before);
247
259
 
248
- const imported = file.contents ? parse_imports(file.id, file.contents) : [];
260
+ let imported: Array<string> = [];
261
+ if (file.contents) {
262
+ try {
263
+ imported = parse_imports(file.id, file.contents);
264
+ } catch (error) {
265
+ this.#log?.error('[filer] Failed to parse imports', file.id, error);
266
+ }
267
+ }
249
268
  for (const specifier of imported) {
250
269
  if (SVELTEKIT_GLOBAL_SPECIFIER.test(specifier)) continue;
251
270
  const path = map_sveltekit_aliases(specifier, aliases);
@@ -253,7 +272,7 @@ export class Filer {
253
272
  let path_id;
254
273
  // TODO can we replace `resolve_specifier` with `import.meta.resolve` completely now outside of esbuild plugins?
255
274
  if (path[0] === '.' || path[0] === '/') {
256
- const resolved = resolve_specifier(path, dir);
275
+ const resolved = await resolve_specifier(path, dir); // eslint-disable-line no-await-in-loop
257
276
  path_id = resolved.path_id;
258
277
  } else {
259
278
  if (isBuiltin(path)) continue;
@@ -304,8 +323,8 @@ export class Filer {
304
323
  for (const disknode of this.files.values()) {
305
324
  try {
306
325
  listener({type: 'add', path: disknode.id, is_directory: false}, disknode);
307
- } catch (err) {
308
- this.#log?.error('[filer] Listener error during sync:', err);
326
+ } catch (error) {
327
+ this.#log?.error('[filer] Listener error during sync:', error);
309
328
  }
310
329
  }
311
330
  }
@@ -314,8 +333,8 @@ export class Filer {
314
333
  for (const listener of this.#listeners) {
315
334
  try {
316
335
  listener(change, disknode);
317
- } catch (err) {
318
- this.#log?.error('[filer] Listener error during change notification:', err);
336
+ } catch (error) {
337
+ this.#log?.error('[filer] Listener error during change notification:', error);
319
338
  }
320
339
  }
321
340
  }
@@ -335,26 +354,61 @@ export class Filer {
335
354
  // keep watching active even with no listeners, only close() tears down
336
355
  }
337
356
 
338
- #on_change: WatcherChangeCallback = (change) => {
339
- if (this.#closing) return; // ignore changes during close
340
- if (change.is_directory) return; // TODO manage directories?
341
- let disknode: Disknode | null;
342
- switch (change.type) {
343
- case 'add':
344
- case 'update': {
345
- disknode = this.#update(change.path);
346
- break;
357
+ async #drain_queue(): Promise<void> {
358
+ // Wait for queue to be empty and no active processing
359
+ while (this.#change_queue.length > 0 || this.#processing_promise) {
360
+ await this.#process_queue(); // eslint-disable-line no-await-in-loop
361
+ }
362
+ }
363
+
364
+ async #process_queue(): Promise<void> {
365
+ // If already processing, return the existing promise
366
+ if (this.#processing_promise) return this.#processing_promise;
367
+
368
+ // Create and track the processing promise
369
+ this.#processing_promise = this.#do_process_queue();
370
+
371
+ try {
372
+ await this.#processing_promise;
373
+ } finally {
374
+ this.#processing_promise = null;
375
+ }
376
+ }
377
+
378
+ async #do_process_queue(): Promise<void> {
379
+ while (this.#change_queue.length > 0) {
380
+ const change = this.#change_queue.shift()!;
381
+
382
+ if (this.#closing) continue; // ignore changes during close
383
+ if (change.is_directory) continue; // TODO manage directories?
384
+
385
+ let disknode: Disknode | null;
386
+ switch (change.type) {
387
+ case 'add':
388
+ case 'update': {
389
+ disknode = await this.#update(change.path); // eslint-disable-line no-await-in-loop
390
+ break;
391
+ }
392
+ case 'delete': {
393
+ disknode = this.#remove(change.path);
394
+ break;
395
+ }
396
+ default:
397
+ throw new UnreachableError(change.type);
347
398
  }
348
- case 'delete': {
349
- disknode = this.#remove(change.path);
350
- break;
399
+
400
+ if (disknode && this.#listeners.size > 0) {
401
+ this.#notify_change(change, disknode);
351
402
  }
352
- default:
353
- throw new UnreachableError(change.type);
354
- }
355
- if (disknode && this.#listeners.size > 0) {
356
- this.#notify_change(change, disknode);
357
403
  }
404
+ }
405
+
406
+ #on_change: WatcherChangeCallback = (change) => {
407
+ // Enqueue the change (sync callback from chokidar)
408
+ this.#change_queue.push(change);
409
+
410
+ // Start processing if not already running
411
+ void this.#process_queue();
358
412
  };
359
413
 
360
414
  #is_external(id: PathId): boolean {
@@ -372,21 +426,26 @@ export const filter_dependents = (
372
426
  searched: Set<PathId> = new Set(),
373
427
  log?: Logger,
374
428
  ): Set<PathId> => {
375
- const {dependents} = disknode;
376
- for (const dependent_id of dependents.keys()) {
377
- if (searched.has(dependent_id)) continue;
378
- searched.add(dependent_id);
379
- if (!filter || filter(dependent_id)) {
380
- results.add(dependent_id);
381
- }
382
- const dependent_disknode = get_by_id(dependent_id);
383
- if (!dependent_disknode) {
384
- log?.warn(
385
- `[filer.filter_dependents] dependent source file ${dependent_id} not found for ${disknode.id}`,
386
- );
387
- continue;
429
+ // Use iterative approach to avoid stack overflow on deep dependency trees
430
+ const stack = [disknode];
431
+
432
+ while (stack.length > 0) {
433
+ const current = stack.pop()!;
434
+ for (const dependent_id of current.dependents.keys()) {
435
+ if (searched.has(dependent_id)) continue;
436
+ searched.add(dependent_id);
437
+ if (!filter || filter(dependent_id)) {
438
+ results.add(dependent_id);
439
+ }
440
+ const dependent_disknode = get_by_id(dependent_id);
441
+ if (!dependent_disknode) {
442
+ log?.warn(
443
+ `[filer.filter_dependents] dependent source file ${dependent_id} not found for ${current.id}`,
444
+ );
445
+ continue;
446
+ }
447
+ stack.push(dependent_disknode);
388
448
  }
389
- filter_dependents(dependent_disknode, get_by_id, filter, results, searched);
390
449
  }
391
450
  return results;
392
451
  };
@@ -1,4 +1,4 @@
1
- import {print_spawn_result} from '@ryanatkn/belt/process.js';
1
+ import {print_spawn_result} from '@fuzdev/fuz_util/process.js';
2
2
  import {z} from 'zod';
3
3
 
4
4
  import {TaskError, type Task} from './task.ts';
@@ -1,5 +1,5 @@
1
- import type {SpawnResult} from '@ryanatkn/belt/process.js';
2
- import type {Logger} from '@ryanatkn/belt/log.js';
1
+ import type {SpawnResult} from '@fuzdev/fuz_util/process.js';
2
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
3
3
 
4
4
  import {paths} from './paths.ts';
5
5
  import {
@@ -19,7 +19,7 @@ export const format_file = async (
19
19
  const final_base_options =
20
20
  base_options !== undefined
21
21
  ? base_options
22
- : (cached_base_options = load_package_json().prettier as any);
22
+ : (cached_base_options = (await load_package_json()).prettier as any);
23
23
  let final_options = options;
24
24
  if (options.filepath && !options.parser) {
25
25
  const {filepath, ...rest} = options;
@@ -1,6 +1,6 @@
1
1
  import {styleText as st} from 'node:util';
2
- import {print_ms, print_error} from '@ryanatkn/belt/print.js';
3
- import {plural} from '@ryanatkn/belt/string.js';
2
+ import {print_ms, print_error} from '@fuzdev/fuz_util/print.js';
3
+ import {plural} from '@fuzdev/fuz_util/string.js';
4
4
  import {z} from 'zod';
5
5
 
6
6
  import {TaskError, type Task} from './task.ts';
@@ -43,7 +43,7 @@ export const task: Task<Args> = {
43
43
  const input_paths = to_input_paths(raw_input_paths);
44
44
 
45
45
  // load all of the gen modules
46
- const found = find_genfiles(input_paths, root_dirs, config, timings);
46
+ const found = await find_genfiles(input_paths, root_dirs, config, timings);
47
47
  if (!found.ok) {
48
48
  if (found.type === 'input_directories_with_no_files') {
49
49
  // TODO maybe let this error like the normal case, but only call `gro gen` if we find gen files? problem is the args would need to be hoisted to callers like `gro sync`