@fuzdev/gro 0.192.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 (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/dist/args.d.ts +37 -0
  4. package/dist/args.d.ts.map +1 -0
  5. package/dist/args.js +102 -0
  6. package/dist/build.task.d.ts +20 -0
  7. package/dist/build.task.d.ts.map +1 -0
  8. package/dist/build.task.js +119 -0
  9. package/dist/build_cache.d.ts +100 -0
  10. package/dist/build_cache.d.ts.map +1 -0
  11. package/dist/build_cache.js +299 -0
  12. package/dist/changelog.d.ts +11 -0
  13. package/dist/changelog.d.ts.map +1 -0
  14. package/dist/changelog.js +47 -0
  15. package/dist/changeset.task.d.ts +35 -0
  16. package/dist/changeset.task.d.ts.map +1 -0
  17. package/dist/changeset.task.js +151 -0
  18. package/dist/changeset_helpers.d.ts +17 -0
  19. package/dist/changeset_helpers.d.ts.map +1 -0
  20. package/dist/changeset_helpers.js +7 -0
  21. package/dist/check.task.d.ts +28 -0
  22. package/dist/check.task.d.ts.map +1 -0
  23. package/dist/check.task.js +104 -0
  24. package/dist/child_process_logging.d.ts +10 -0
  25. package/dist/child_process_logging.d.ts.map +1 -0
  26. package/dist/child_process_logging.js +26 -0
  27. package/dist/clean.task.d.ts +15 -0
  28. package/dist/clean.task.d.ts.map +1 -0
  29. package/dist/clean.task.js +40 -0
  30. package/dist/clean_fs.d.ts +9 -0
  31. package/dist/clean_fs.d.ts.map +1 -0
  32. package/dist/clean_fs.js +28 -0
  33. package/dist/cli.d.ts +34 -0
  34. package/dist/cli.d.ts.map +1 -0
  35. package/dist/cli.js +61 -0
  36. package/dist/commit.task.d.ts +11 -0
  37. package/dist/commit.task.d.ts.map +1 -0
  38. package/dist/commit.task.js +24 -0
  39. package/dist/constants.d.ts +46 -0
  40. package/dist/constants.d.ts.map +1 -0
  41. package/dist/constants.js +52 -0
  42. package/dist/deploy.task.d.ts +29 -0
  43. package/dist/deploy.task.d.ts.map +1 -0
  44. package/dist/deploy.task.js +217 -0
  45. package/dist/dev.task.d.ts +16 -0
  46. package/dist/dev.task.d.ts.map +1 -0
  47. package/dist/dev.task.js +44 -0
  48. package/dist/disknode.d.ts +23 -0
  49. package/dist/disknode.d.ts.map +1 -0
  50. package/dist/disknode.js +1 -0
  51. package/dist/env.d.ts +11 -0
  52. package/dist/env.d.ts.map +1 -0
  53. package/dist/env.js +49 -0
  54. package/dist/esbuild_helpers.d.ts +16 -0
  55. package/dist/esbuild_helpers.d.ts.map +1 -0
  56. package/dist/esbuild_helpers.js +36 -0
  57. package/dist/esbuild_plugin_external_worker.d.ts +23 -0
  58. package/dist/esbuild_plugin_external_worker.d.ts.map +1 -0
  59. package/dist/esbuild_plugin_external_worker.js +55 -0
  60. package/dist/esbuild_plugin_svelte.d.ts +15 -0
  61. package/dist/esbuild_plugin_svelte.d.ts.map +1 -0
  62. package/dist/esbuild_plugin_svelte.js +83 -0
  63. package/dist/esbuild_plugin_sveltekit_local_imports.d.ts +8 -0
  64. package/dist/esbuild_plugin_sveltekit_local_imports.d.ts.map +1 -0
  65. package/dist/esbuild_plugin_sveltekit_local_imports.js +30 -0
  66. package/dist/esbuild_plugin_sveltekit_shim_alias.d.ts +7 -0
  67. package/dist/esbuild_plugin_sveltekit_shim_alias.d.ts.map +1 -0
  68. package/dist/esbuild_plugin_sveltekit_shim_alias.js +18 -0
  69. package/dist/esbuild_plugin_sveltekit_shim_app.d.ts +9 -0
  70. package/dist/esbuild_plugin_sveltekit_shim_app.d.ts.map +1 -0
  71. package/dist/esbuild_plugin_sveltekit_shim_app.js +22 -0
  72. package/dist/esbuild_plugin_sveltekit_shim_env.d.ts +11 -0
  73. package/dist/esbuild_plugin_sveltekit_shim_env.d.ts.map +1 -0
  74. package/dist/esbuild_plugin_sveltekit_shim_env.js +18 -0
  75. package/dist/filer.d.ts +33 -0
  76. package/dist/filer.d.ts.map +1 -0
  77. package/dist/filer.js +385 -0
  78. package/dist/format.task.d.ts +11 -0
  79. package/dist/format.task.d.ts.map +1 -0
  80. package/dist/format.task.js +27 -0
  81. package/dist/format_directory.d.ts +13 -0
  82. package/dist/format_directory.d.ts.map +1 -0
  83. package/dist/format_directory.js +40 -0
  84. package/dist/format_file.d.ts +9 -0
  85. package/dist/format_file.d.ts.map +1 -0
  86. package/dist/format_file.js +42 -0
  87. package/dist/gen.d.ts +142 -0
  88. package/dist/gen.d.ts.map +1 -0
  89. package/dist/gen.js +199 -0
  90. package/dist/gen.task.d.ts +12 -0
  91. package/dist/gen.task.d.ts.map +1 -0
  92. package/dist/gen.task.js +149 -0
  93. package/dist/gen_helpers.d.ts +11 -0
  94. package/dist/gen_helpers.d.ts.map +1 -0
  95. package/dist/gen_helpers.js +76 -0
  96. package/dist/github.d.ts +19 -0
  97. package/dist/github.d.ts.map +1 -0
  98. package/dist/github.js +33 -0
  99. package/dist/gro.config.default.d.ts +13 -0
  100. package/dist/gro.config.default.d.ts.map +1 -0
  101. package/dist/gro.config.default.js +33 -0
  102. package/dist/gro.d.ts +3 -0
  103. package/dist/gro.d.ts.map +1 -0
  104. package/dist/gro.js +21 -0
  105. package/dist/gro_config.d.ts +115 -0
  106. package/dist/gro_config.d.ts.map +1 -0
  107. package/dist/gro_config.js +114 -0
  108. package/dist/gro_helpers.d.ts +49 -0
  109. package/dist/gro_helpers.d.ts.map +1 -0
  110. package/dist/gro_helpers.js +97 -0
  111. package/dist/gro_plugin_gen.d.ts +12 -0
  112. package/dist/gro_plugin_gen.d.ts.map +1 -0
  113. package/dist/gro_plugin_gen.js +101 -0
  114. package/dist/gro_plugin_server.d.ts +80 -0
  115. package/dist/gro_plugin_server.d.ts.map +1 -0
  116. package/dist/gro_plugin_server.js +167 -0
  117. package/dist/gro_plugin_sveltekit_app.d.ts +9 -0
  118. package/dist/gro_plugin_sveltekit_app.d.ts.map +1 -0
  119. package/dist/gro_plugin_sveltekit_app.js +42 -0
  120. package/dist/gro_plugin_sveltekit_library.d.ts +16 -0
  121. package/dist/gro_plugin_sveltekit_library.d.ts.map +1 -0
  122. package/dist/gro_plugin_sveltekit_library.js +34 -0
  123. package/dist/index.d.ts +9 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +4 -0
  126. package/dist/input_path.d.ts +64 -0
  127. package/dist/input_path.d.ts.map +1 -0
  128. package/dist/input_path.js +199 -0
  129. package/dist/invoke.d.ts +2 -0
  130. package/dist/invoke.d.ts.map +1 -0
  131. package/dist/invoke.js +28 -0
  132. package/dist/invoke_task.d.ts +30 -0
  133. package/dist/invoke_task.d.ts.map +1 -0
  134. package/dist/invoke_task.js +104 -0
  135. package/dist/lint.task.d.ts +11 -0
  136. package/dist/lint.task.d.ts.map +1 -0
  137. package/dist/lint.task.js +32 -0
  138. package/dist/loader.d.ts +6 -0
  139. package/dist/loader.d.ts.map +1 -0
  140. package/dist/loader.js +192 -0
  141. package/dist/module.d.ts +4 -0
  142. package/dist/module.d.ts.map +1 -0
  143. package/dist/module.js +6 -0
  144. package/dist/modules.d.ts +36 -0
  145. package/dist/modules.d.ts.map +1 -0
  146. package/dist/modules.js +71 -0
  147. package/dist/package_json.d.ts +32 -0
  148. package/dist/package_json.d.ts.map +1 -0
  149. package/dist/package_json.js +178 -0
  150. package/dist/parse_exports.d.ts +20 -0
  151. package/dist/parse_exports.d.ts.map +1 -0
  152. package/dist/parse_exports.js +65 -0
  153. package/dist/parse_exports_context.d.ts +21 -0
  154. package/dist/parse_exports_context.d.ts.map +1 -0
  155. package/dist/parse_exports_context.js +332 -0
  156. package/dist/parse_imports.d.ts +5 -0
  157. package/dist/parse_imports.d.ts.map +1 -0
  158. package/dist/parse_imports.js +140 -0
  159. package/dist/paths.d.ts +41 -0
  160. package/dist/paths.d.ts.map +1 -0
  161. package/dist/paths.js +69 -0
  162. package/dist/plugin.d.ts +36 -0
  163. package/dist/plugin.d.ts.map +1 -0
  164. package/dist/plugin.js +78 -0
  165. package/dist/publish.task.d.ts +26 -0
  166. package/dist/publish.task.d.ts.map +1 -0
  167. package/dist/publish.task.js +176 -0
  168. package/dist/register.d.ts +2 -0
  169. package/dist/register.d.ts.map +1 -0
  170. package/dist/register.js +2 -0
  171. package/dist/reinstall.task.d.ts +8 -0
  172. package/dist/reinstall.task.d.ts.map +1 -0
  173. package/dist/reinstall.task.js +35 -0
  174. package/dist/release.task.d.ts +8 -0
  175. package/dist/release.task.d.ts.map +1 -0
  176. package/dist/release.task.js +20 -0
  177. package/dist/resolve.task.d.ts +11 -0
  178. package/dist/resolve.task.d.ts.map +1 -0
  179. package/dist/resolve.task.js +38 -0
  180. package/dist/resolve_specifier.d.ts +22 -0
  181. package/dist/resolve_specifier.d.ts.map +1 -0
  182. package/dist/resolve_specifier.js +57 -0
  183. package/dist/run.task.d.ts +16 -0
  184. package/dist/run.task.d.ts.map +1 -0
  185. package/dist/run.task.js +52 -0
  186. package/dist/run_gen.d.ts +10 -0
  187. package/dist/run_gen.d.ts.map +1 -0
  188. package/dist/run_gen.js +73 -0
  189. package/dist/run_task.d.ts +17 -0
  190. package/dist/run_task.d.ts.map +1 -0
  191. package/dist/run_task.js +45 -0
  192. package/dist/source_json.d.ts +7 -0
  193. package/dist/source_json.d.ts.map +1 -0
  194. package/dist/source_json.js +145 -0
  195. package/dist/svelte_config.d.ts +57 -0
  196. package/dist/svelte_config.d.ts.map +1 -0
  197. package/dist/svelte_config.js +81 -0
  198. package/dist/sveltekit_helpers.d.ts +75 -0
  199. package/dist/sveltekit_helpers.d.ts.map +1 -0
  200. package/dist/sveltekit_helpers.js +94 -0
  201. package/dist/sveltekit_shim_app.d.ts +11 -0
  202. package/dist/sveltekit_shim_app.d.ts.map +1 -0
  203. package/dist/sveltekit_shim_app.js +31 -0
  204. package/dist/sveltekit_shim_app_environment.d.ts +13 -0
  205. package/dist/sveltekit_shim_app_environment.d.ts.map +1 -0
  206. package/dist/sveltekit_shim_app_environment.js +14 -0
  207. package/dist/sveltekit_shim_app_forms.d.ts +5 -0
  208. package/dist/sveltekit_shim_app_forms.d.ts.map +1 -0
  209. package/dist/sveltekit_shim_app_forms.js +6 -0
  210. package/dist/sveltekit_shim_app_navigation.d.ts +10 -0
  211. package/dist/sveltekit_shim_app_navigation.d.ts.map +1 -0
  212. package/dist/sveltekit_shim_app_navigation.js +11 -0
  213. package/dist/sveltekit_shim_app_paths.d.ts +17 -0
  214. package/dist/sveltekit_shim_app_paths.d.ts.map +1 -0
  215. package/dist/sveltekit_shim_app_paths.js +10 -0
  216. package/dist/sveltekit_shim_app_state.d.ts +5 -0
  217. package/dist/sveltekit_shim_app_state.d.ts.map +1 -0
  218. package/dist/sveltekit_shim_app_state.js +26 -0
  219. package/dist/sveltekit_shim_env.d.ts +5 -0
  220. package/dist/sveltekit_shim_env.d.ts.map +1 -0
  221. package/dist/sveltekit_shim_env.js +23 -0
  222. package/dist/sync.task.d.ts +16 -0
  223. package/dist/sync.task.d.ts.map +1 -0
  224. package/dist/sync.task.js +39 -0
  225. package/dist/task.d.ts +98 -0
  226. package/dist/task.d.ts.map +1 -0
  227. package/dist/task.js +109 -0
  228. package/dist/task_logging.d.ts +6 -0
  229. package/dist/task_logging.d.ts.map +1 -0
  230. package/dist/task_logging.js +201 -0
  231. package/dist/test.task.d.ts +13 -0
  232. package/dist/test.task.d.ts.map +1 -0
  233. package/dist/test.task.js +53 -0
  234. package/dist/typecheck.task.d.ts +13 -0
  235. package/dist/typecheck.task.d.ts.map +1 -0
  236. package/dist/typecheck.task.js +68 -0
  237. package/dist/upgrade.task.d.ts +20 -0
  238. package/dist/upgrade.task.d.ts.map +1 -0
  239. package/dist/upgrade.task.js +111 -0
  240. package/dist/watch_dir.d.ts +36 -0
  241. package/dist/watch_dir.d.ts.map +1 -0
  242. package/dist/watch_dir.js +69 -0
  243. package/package.json +149 -0
  244. package/src/lib/args.ts +115 -0
  245. package/src/lib/build.task.ts +151 -0
  246. package/src/lib/build_cache.ts +378 -0
  247. package/src/lib/changelog.ts +69 -0
  248. package/src/lib/changeset.task.ts +228 -0
  249. package/src/lib/changeset_helpers.ts +14 -0
  250. package/src/lib/check.task.ts +132 -0
  251. package/src/lib/child_process_logging.ts +38 -0
  252. package/src/lib/clean.task.ts +48 -0
  253. package/src/lib/clean_fs.ts +54 -0
  254. package/src/lib/cli.ts +98 -0
  255. package/src/lib/commit.task.ts +34 -0
  256. package/src/lib/constants.ts +56 -0
  257. package/src/lib/deploy.task.ts +287 -0
  258. package/src/lib/dev.task.ts +52 -0
  259. package/src/lib/disknode.ts +26 -0
  260. package/src/lib/env.ts +78 -0
  261. package/src/lib/esbuild_helpers.ts +49 -0
  262. package/src/lib/esbuild_plugin_external_worker.ts +94 -0
  263. package/src/lib/esbuild_plugin_svelte.ts +134 -0
  264. package/src/lib/esbuild_plugin_sveltekit_local_imports.ts +38 -0
  265. package/src/lib/esbuild_plugin_sveltekit_shim_alias.ts +27 -0
  266. package/src/lib/esbuild_plugin_sveltekit_shim_app.ts +42 -0
  267. package/src/lib/esbuild_plugin_sveltekit_shim_env.ts +47 -0
  268. package/src/lib/filer.ts +458 -0
  269. package/src/lib/format.task.ts +44 -0
  270. package/src/lib/format_directory.ts +65 -0
  271. package/src/lib/format_file.ts +49 -0
  272. package/src/lib/gen.task.ts +206 -0
  273. package/src/lib/gen.ts +406 -0
  274. package/src/lib/gen_helpers.ts +131 -0
  275. package/src/lib/github.ts +46 -0
  276. package/src/lib/gro.config.default.ts +42 -0
  277. package/src/lib/gro.ts +29 -0
  278. package/src/lib/gro_config.ts +254 -0
  279. package/src/lib/gro_helpers.ts +108 -0
  280. package/src/lib/gro_plugin_gen.ts +149 -0
  281. package/src/lib/gro_plugin_server.ts +288 -0
  282. package/src/lib/gro_plugin_sveltekit_app.ts +58 -0
  283. package/src/lib/gro_plugin_sveltekit_library.ts +63 -0
  284. package/src/lib/index.ts +8 -0
  285. package/src/lib/input_path.ts +254 -0
  286. package/src/lib/invoke.ts +34 -0
  287. package/src/lib/invoke_task.ts +139 -0
  288. package/src/lib/lint.task.ts +39 -0
  289. package/src/lib/loader.ts +229 -0
  290. package/src/lib/module.ts +13 -0
  291. package/src/lib/modules.ts +117 -0
  292. package/src/lib/package_json.ts +255 -0
  293. package/src/lib/parse_exports.ts +100 -0
  294. package/src/lib/parse_exports_context.ts +395 -0
  295. package/src/lib/parse_imports.ts +180 -0
  296. package/src/lib/paths.ts +111 -0
  297. package/src/lib/plugin.ts +106 -0
  298. package/src/lib/publish.task.ts +228 -0
  299. package/src/lib/register.ts +3 -0
  300. package/src/lib/reinstall.task.ts +45 -0
  301. package/src/lib/release.task.ts +26 -0
  302. package/src/lib/resolve.task.ts +43 -0
  303. package/src/lib/resolve_specifier.ts +81 -0
  304. package/src/lib/run.task.ts +65 -0
  305. package/src/lib/run_gen.ts +110 -0
  306. package/src/lib/run_task.ts +82 -0
  307. package/src/lib/source_json.ts +183 -0
  308. package/src/lib/svelte_config.ts +140 -0
  309. package/src/lib/sveltekit_helpers.ts +193 -0
  310. package/src/lib/sveltekit_shim_app.ts +41 -0
  311. package/src/lib/sveltekit_shim_app_environment.ts +16 -0
  312. package/src/lib/sveltekit_shim_app_forms.ts +13 -0
  313. package/src/lib/sveltekit_shim_app_navigation.ts +23 -0
  314. package/src/lib/sveltekit_shim_app_paths.ts +26 -0
  315. package/src/lib/sveltekit_shim_app_state.ts +35 -0
  316. package/src/lib/sveltekit_shim_env.ts +45 -0
  317. package/src/lib/sync.task.ts +47 -0
  318. package/src/lib/task.ts +245 -0
  319. package/src/lib/task_logging.ts +255 -0
  320. package/src/lib/test.task.ts +63 -0
  321. package/src/lib/typecheck.task.ts +81 -0
  322. package/src/lib/upgrade.task.ts +148 -0
  323. package/src/lib/watch_dir.ts +115 -0
@@ -0,0 +1,458 @@
1
+ import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
2
+ import {readFile, stat} from 'node:fs/promises';
3
+ import {dirname, resolve} from 'node:path';
4
+ import type {OmitStrict} from '@fuzdev/fuz_util/types.js';
5
+ import {isBuiltin} from 'node:module';
6
+ import {fileURLToPath, pathToFileURL} from 'node:url';
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
+ import {hash_secure} from '@fuzdev/fuz_util/hash.js';
12
+
13
+ import {
14
+ watch_dir,
15
+ type WatchNodeFs,
16
+ type WatcherChange,
17
+ type WatchDirOptions,
18
+ type WatcherChangeCallback,
19
+ } from './watch_dir.ts';
20
+ import {paths} from './paths.ts';
21
+ import {parse_imports} from './parse_imports.ts';
22
+ import {resolve_specifier} from './resolve_specifier.ts';
23
+ import {default_svelte_config} from './svelte_config.ts';
24
+ import {map_sveltekit_aliases} from './sveltekit_helpers.ts';
25
+ import {SVELTEKIT_GLOBAL_SPECIFIER} from './constants.ts';
26
+ import type {Disknode} from './disknode.ts';
27
+
28
+ const aliases = Object.entries(default_svelte_config.alias);
29
+
30
+ export type OnFilerChange = (change: WatcherChange, disknode: Disknode) => void;
31
+
32
+ export interface FilerOptions {
33
+ watch_dir?: typeof watch_dir;
34
+ watch_dir_options?: Partial<OmitStrict<WatchDirOptions, 'on_change'>>;
35
+ package_json_cache?: Record<string, PackageJson>;
36
+ log?: Logger;
37
+ }
38
+
39
+ export class Filer {
40
+ readonly root_dir: PathId;
41
+
42
+ // TODO rename everything to `disknode`
43
+ readonly files: Map<PathId, Disknode> = new Map();
44
+
45
+ #watch_dir: typeof watch_dir;
46
+ #watch_dir_options: Partial<WatchDirOptions>;
47
+
48
+ #log?: Logger;
49
+
50
+ #listeners: Set<OnFilerChange> = new Set();
51
+ #watching: WatchNodeFs | undefined;
52
+ #initing: Promise<void> | undefined;
53
+ #closing: Promise<void> | undefined;
54
+
55
+ #change_queue: Array<WatcherChange> = [];
56
+ #processing_promise: Promise<void> | null = null;
57
+
58
+ constructor(options: FilerOptions = EMPTY_OBJECT) {
59
+ this.#watch_dir = options.watch_dir ?? watch_dir;
60
+ this.#watch_dir_options = options.watch_dir_options ?? EMPTY_OBJECT;
61
+ this.root_dir = resolve(options.watch_dir_options?.dir ?? paths.source);
62
+ // TODO for package.json maybe another array of files/dirs to watch to invalidate everything?
63
+ // or instead of that, think of taking an array of config objects that can specify invalidation rules,
64
+ // so package.json would be configured differently than ./src, and we could add a default with
65
+ // package.json/gro.config.ts/tsconfig.json/svelte.config.js/vite.config.ts to invalidate everything
66
+ this.#log = options.log;
67
+ }
68
+ get inited(): boolean {
69
+ return this.#watching !== undefined;
70
+ }
71
+
72
+ get_by_id = (id: PathId): Disknode | undefined => {
73
+ return this.files.get(id);
74
+ };
75
+
76
+ get_or_create = (id: PathId): Disknode => {
77
+ const existing = this.get_by_id(id);
78
+ if (existing) return existing;
79
+ const file: Disknode = {
80
+ id,
81
+ contents: null,
82
+ external: this.#is_external(id), // TODO maybe filter externals by default? the user needs to configure the filer then
83
+ ctime: null,
84
+ mtime: null,
85
+ content_hash: null,
86
+ dependents: new Map(),
87
+ dependencies: new Map(),
88
+ };
89
+ this.files.set(id, file);
90
+ // Defer external file change notification to avoid reentrancy during queue processing
91
+ if (file.external) {
92
+ queueMicrotask(() => {
93
+ this.#on_change({type: 'add', path: file.id, is_directory: false});
94
+ });
95
+ }
96
+ return file;
97
+ };
98
+
99
+ filter(predicate: (disknode: Disknode) => boolean): Array<Disknode> | null {
100
+ let found: Array<Disknode> | null = null;
101
+ for (const disknode of this.files.values()) {
102
+ if (predicate(disknode)) {
103
+ (found ??= []).push(disknode);
104
+ }
105
+ }
106
+ return found;
107
+ }
108
+
109
+ /**
110
+ * Initialize the filer to populate files without watching.
111
+ * Safe to call multiple times - subsequent calls are no-ops.
112
+ * Used by gen files to access the file graph.
113
+ */
114
+ async init(): Promise<void> {
115
+ // if already initing, return the existing promise
116
+ if (this.#initing) return this.#initing;
117
+
118
+ // if already initialized, just ensure ready
119
+ if (this.#watching) {
120
+ return this.#watching.init();
121
+ }
122
+
123
+ // start new initialization
124
+ this.#initing = this.#init();
125
+ try {
126
+ await this.#initing;
127
+ } catch (error) {
128
+ // use shared cleanup logic
129
+ this.#cleanup();
130
+ throw error;
131
+ } finally {
132
+ this.#initing = undefined;
133
+ }
134
+ }
135
+
136
+ async #init(): Promise<void> {
137
+ const watcher = this.#watch_dir({
138
+ ...this.#watch_dir_options,
139
+ dir: this.root_dir,
140
+ on_change: this.#on_change,
141
+ });
142
+
143
+ try {
144
+ await watcher.init();
145
+
146
+ // Wait for any queued changes from init to be processed
147
+ await this.#drain_queue();
148
+
149
+ // check if close() was called during init
150
+ if (this.#closing) {
151
+ await watcher.close();
152
+ return;
153
+ }
154
+
155
+ // only set after successful init and not closing
156
+ this.#watching = watcher;
157
+ } catch (error) {
158
+ // clean up watcher on error, but don't let close error mask init error
159
+ try {
160
+ await watcher.close();
161
+ } catch {
162
+ // ignore close errors - init error is more important
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ async watch(listener: OnFilerChange): Promise<() => void> {
169
+ await this.#add_listener(listener);
170
+ return () => {
171
+ this.#remove_listener(listener);
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Internal cleanup of all state - can be called safely from anywhere
177
+ */
178
+ #cleanup(): void {
179
+ this.#listeners.clear();
180
+ this.files.clear();
181
+ this.#watching = undefined;
182
+ this.#change_queue = [];
183
+ this.#processing_promise = null;
184
+ // #initing is handled in finally block of init()
185
+ }
186
+
187
+ close(): Promise<void> {
188
+ // if already closing, return existing promise
189
+ if (this.#closing) return this.#closing;
190
+
191
+ // if already closed and not initing, nothing to do
192
+ if (!this.#watching && !this.#initing) return Promise.resolve();
193
+
194
+ // start new close operation
195
+ const closing = this.#close();
196
+ this.#closing = closing;
197
+ // Clean up after completion, but don't change the returned promise
198
+ // Use void to ensure we don't accidentally return the .then() promise
199
+ void closing.then(
200
+ () => {
201
+ this.#closing = undefined;
202
+ },
203
+ () => {
204
+ this.#closing = undefined;
205
+ },
206
+ );
207
+ return this.#closing;
208
+ }
209
+
210
+ async #close(): Promise<void> {
211
+ // wait for any pending initialization to complete
212
+ if (this.#initing) {
213
+ try {
214
+ await this.#initing;
215
+ } catch {
216
+ // ignore errors during close
217
+ }
218
+ }
219
+
220
+ // close watcher if it exists
221
+ if (this.#watching) {
222
+ await this.#watching.close();
223
+ }
224
+
225
+ // clean up all state
226
+ this.#cleanup();
227
+ }
228
+
229
+ async #update(id: PathId): Promise<Disknode | null> {
230
+ const file = this.get_or_create(id);
231
+
232
+ let stats: Awaited<ReturnType<typeof stat>> | null = null;
233
+ let new_contents: string | null = null; // TODO need to lazily load contents, probably turn `Disknode` into a class
234
+
235
+ try {
236
+ [stats, new_contents] = await Promise.all([stat(id), readFile(id, 'utf8')]);
237
+ } catch (error) {
238
+ const code = (error as NodeJS.ErrnoException).code;
239
+ // Treat file as deleted/inaccessible for common error codes
240
+ if (code === 'ENOENT' || code === 'EACCES' || code === 'EPERM') {
241
+ // File doesn't exist or is inaccessible, treat as deleted
242
+ } else {
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ // Compute hash for new contents
248
+ const new_hash = new_contents !== null ? await hash_secure(new_contents) : null;
249
+
250
+ file.ctime = stats?.ctimeMs ?? null;
251
+ file.mtime = stats?.mtimeMs ?? null;
252
+
253
+ // Use hash comparison for change detection (content-based, not mtime-based)
254
+ if (file.content_hash === new_hash) {
255
+ return null;
256
+ }
257
+
258
+ file.contents = new_contents;
259
+ file.content_hash = new_hash;
260
+
261
+ const dir = dirname(file.id);
262
+
263
+ const dependencies_before = new Set(file.dependencies.keys());
264
+ const dependencies_removed = new Set(dependencies_before);
265
+
266
+ let imported: Array<string> = [];
267
+ if (file.contents) {
268
+ try {
269
+ imported = parse_imports(file.id, file.contents);
270
+ } catch (error) {
271
+ this.#log?.error('[filer] Failed to parse imports', file.id, error);
272
+ }
273
+ }
274
+ for (const specifier of imported) {
275
+ if (SVELTEKIT_GLOBAL_SPECIFIER.test(specifier)) continue;
276
+ const path = map_sveltekit_aliases(specifier, aliases);
277
+
278
+ let path_id;
279
+ // TODO can we replace `resolve_specifier` with `import.meta.resolve` completely now outside of esbuild plugins?
280
+ if (path[0] === '.' || path[0] === '/') {
281
+ const resolved = await resolve_specifier(path, dir); // eslint-disable-line no-await-in-loop
282
+ path_id = resolved.path_id;
283
+ } else {
284
+ if (isBuiltin(path)) continue;
285
+ const file_url = pathToFileURL(file.id);
286
+ try {
287
+ path_id = fileURLToPath(import.meta.resolve(path, file_url.href));
288
+ } catch (error) {
289
+ // if resolving fails for any reason, just log and ignore it
290
+ this.#log?.error('[filer] failed to resolve path', path, file_url.href, error);
291
+ continue;
292
+ }
293
+ }
294
+ dependencies_removed.delete(path_id);
295
+ if (!dependencies_before.has(path_id)) {
296
+ const d = this.get_or_create(path_id);
297
+ file.dependencies.set(d.id, d);
298
+ d.dependents.set(file.id, file);
299
+ }
300
+ }
301
+
302
+ // update any removed dependencies
303
+ for (const dependency_removed of dependencies_removed) {
304
+ file.dependencies.delete(dependency_removed);
305
+ const dependency_removed_file = this.get_or_create(dependency_removed);
306
+ dependency_removed_file.dependents.delete(file.id);
307
+ }
308
+
309
+ return file;
310
+ }
311
+
312
+ #remove(id: PathId): Disknode | null {
313
+ const file = this.get_by_id(id);
314
+ if (!file) return null; // this is safe because the object would exist if any other file referenced it as a dependency or dependent
315
+
316
+ file.contents = null; // clear contents in case it gets re-added later, we want the change to be detected
317
+ file.content_hash = null; // clear hash so re-add detects the change
318
+
319
+ file.dependencies.clear();
320
+
321
+ // keep the file in memory if other files still depend on it
322
+ if (file.dependents.size === 0) {
323
+ this.files.delete(id);
324
+ }
325
+
326
+ return file;
327
+ }
328
+
329
+ #sync_listener_with_files(listener: OnFilerChange): void {
330
+ for (const disknode of this.files.values()) {
331
+ try {
332
+ listener({type: 'add', path: disknode.id, is_directory: false}, disknode);
333
+ } catch (error) {
334
+ this.#log?.error('[filer] Listener error during sync:', error);
335
+ }
336
+ }
337
+ }
338
+
339
+ #notify_change(change: WatcherChange, disknode: Disknode): void {
340
+ for (const listener of this.#listeners) {
341
+ try {
342
+ listener(change, disknode);
343
+ } catch (error) {
344
+ this.#log?.error('[filer] Listener error during change notification:', error);
345
+ }
346
+ }
347
+ }
348
+
349
+ async #add_listener(listener: OnFilerChange): Promise<void> {
350
+ this.#listeners.add(listener);
351
+
352
+ // ensure initialized
353
+ await this.init();
354
+
355
+ // notify of existing files
356
+ this.#sync_listener_with_files(listener);
357
+ }
358
+
359
+ #remove_listener(listener: OnFilerChange): void {
360
+ this.#listeners.delete(listener);
361
+ // keep watching active even with no listeners, only close() tears down
362
+ }
363
+
364
+ async #drain_queue(): Promise<void> {
365
+ // Wait for queue to be empty and no active processing
366
+ while (this.#change_queue.length > 0 || this.#processing_promise) {
367
+ await this.#process_queue(); // eslint-disable-line no-await-in-loop
368
+ }
369
+ }
370
+
371
+ async #process_queue(): Promise<void> {
372
+ // If already processing, return the existing promise
373
+ if (this.#processing_promise) return this.#processing_promise;
374
+
375
+ // Create and track the processing promise
376
+ this.#processing_promise = this.#do_process_queue();
377
+
378
+ try {
379
+ await this.#processing_promise;
380
+ } finally {
381
+ this.#processing_promise = null;
382
+ }
383
+ }
384
+
385
+ async #do_process_queue(): Promise<void> {
386
+ while (this.#change_queue.length > 0) {
387
+ const change = this.#change_queue.shift()!;
388
+
389
+ if (this.#closing) continue; // ignore changes during close
390
+ if (change.is_directory) continue; // TODO manage directories?
391
+
392
+ let disknode: Disknode | null;
393
+ switch (change.type) {
394
+ case 'add':
395
+ case 'update': {
396
+ disknode = await this.#update(change.path); // eslint-disable-line no-await-in-loop
397
+ break;
398
+ }
399
+ case 'delete': {
400
+ disknode = this.#remove(change.path);
401
+ break;
402
+ }
403
+ default:
404
+ throw new UnreachableError(change.type);
405
+ }
406
+
407
+ if (disknode && this.#listeners.size > 0) {
408
+ this.#notify_change(change, disknode);
409
+ }
410
+ }
411
+ }
412
+
413
+ #on_change: WatcherChangeCallback = (change) => {
414
+ // Enqueue the change (sync callback from chokidar)
415
+ this.#change_queue.push(change);
416
+
417
+ // Start processing if not already running
418
+ void this.#process_queue();
419
+ };
420
+
421
+ #is_external(id: PathId): boolean {
422
+ const {filter} = this.#watch_dir_options;
423
+ return !id.startsWith(this.root_dir + '/') || (!!filter && !filter(id, false));
424
+ }
425
+ }
426
+
427
+ // TODO maybe `Disknode` class?
428
+ export const filter_dependents = (
429
+ disknode: Disknode,
430
+ get_by_id: (id: PathId) => Disknode | undefined,
431
+ filter?: FileFilter,
432
+ results: Set<PathId> = new Set(),
433
+ searched: Set<PathId> = new Set(),
434
+ log?: Logger,
435
+ ): Set<PathId> => {
436
+ // Use iterative approach to avoid stack overflow on deep dependency trees
437
+ const stack = [disknode];
438
+
439
+ while (stack.length > 0) {
440
+ const current = stack.pop()!;
441
+ for (const dependent_id of current.dependents.keys()) {
442
+ if (searched.has(dependent_id)) continue;
443
+ searched.add(dependent_id);
444
+ if (!filter || filter(dependent_id)) {
445
+ results.add(dependent_id);
446
+ }
447
+ const dependent_disknode = get_by_id(dependent_id);
448
+ if (!dependent_disknode) {
449
+ log?.warn(
450
+ `[filer.filter_dependents] dependent source file ${dependent_id} not found for ${current.id}`,
451
+ );
452
+ continue;
453
+ }
454
+ stack.push(dependent_disknode);
455
+ }
456
+ }
457
+ return results;
458
+ };
@@ -0,0 +1,44 @@
1
+ import {print_spawn_result} from '@fuzdev/fuz_util/process.js';
2
+ import {z} from 'zod';
3
+
4
+ import {to_implicit_forwarded_args} from './args.ts';
5
+ import {PRETTIER_CLI_DEFAULT} from './constants.ts';
6
+ import {format_directory} from './format_directory.ts';
7
+ import {paths} from './paths.ts';
8
+ import {TaskError, type Task} from './task.ts';
9
+
10
+ /** @nodocs */
11
+ export const Args = z.strictObject({
12
+ _: z.array(z.string()).meta({description: 'files or directories to format'}).optional(),
13
+ check: z
14
+ .boolean()
15
+ .meta({description: 'exit with a nonzero code if any files are unformatted'})
16
+ .default(false),
17
+ });
18
+ export type Args = z.infer<typeof Args>;
19
+
20
+ /** @nodocs */
21
+ export const task: Task<Args> = {
22
+ summary: 'format source files',
23
+ Args,
24
+ run: async ({args, log, config}) => {
25
+ const {_: patterns, check} = args;
26
+
27
+ const format_result = await format_directory(
28
+ log,
29
+ paths.source,
30
+ check,
31
+ undefined,
32
+ undefined,
33
+ undefined,
34
+ config.pm_cli,
35
+ to_implicit_forwarded_args(PRETTIER_CLI_DEFAULT),
36
+ patterns,
37
+ );
38
+ if (!format_result.ok) {
39
+ throw new TaskError(
40
+ `Failed ${check ? 'formatting check' : 'to format'}. ${print_spawn_result(format_result)}`,
41
+ );
42
+ }
43
+ },
44
+ };
@@ -0,0 +1,65 @@
1
+ import {args_serialize, type Args} from '@fuzdev/fuz_util/args.js';
2
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
3
+ import type {SpawnResult} from '@fuzdev/fuz_util/process.js';
4
+
5
+ import {spawn_cli, to_cli_name, type Cli} from './cli.ts';
6
+ import {
7
+ GITHUB_DIRNAME,
8
+ README_FILENAME,
9
+ SVELTE_CONFIG_FILENAME,
10
+ VITE_CONFIG_FILENAME,
11
+ TSCONFIG_FILENAME,
12
+ GRO_CONFIG_FILENAME,
13
+ PM_CLI_DEFAULT,
14
+ PRETTIER_CLI_DEFAULT,
15
+ } from './constants.ts';
16
+ import {paths} from './paths.ts';
17
+
18
+ const EXTENSIONS_DEFAULT = 'ts,js,json,svelte,html,css,md,yml';
19
+ const ROOT_PATHS_DEFAULT = `${[
20
+ README_FILENAME,
21
+ GRO_CONFIG_FILENAME,
22
+ SVELTE_CONFIG_FILENAME,
23
+ VITE_CONFIG_FILENAME,
24
+ TSCONFIG_FILENAME,
25
+ GITHUB_DIRNAME,
26
+ ].join(',')}/**/*`;
27
+
28
+ /**
29
+ * Formats files on the filesystem.
30
+ * When `patterns` is provided, formats those specific files/patterns.
31
+ * Otherwise formats `dir` with default extensions, plus root files if `dir` is `paths.source`.
32
+ * This is separated from `./format_file` to avoid importing all of the `prettier` code
33
+ * inside modules that import this one. (which has a nontrivial cost)
34
+ */
35
+ export const format_directory = async (
36
+ log: Logger,
37
+ dir: string,
38
+ check = false,
39
+ extensions = EXTENSIONS_DEFAULT,
40
+ root_paths = ROOT_PATHS_DEFAULT,
41
+ prettier_cli: string | Cli = PRETTIER_CLI_DEFAULT,
42
+ pm_cli: string = PM_CLI_DEFAULT,
43
+ additional_args?: Args,
44
+ patterns?: Array<string>,
45
+ ): Promise<SpawnResult> => {
46
+ const forwarded_args = {...additional_args};
47
+ if (forwarded_args.check === undefined && forwarded_args.write === undefined) {
48
+ forwarded_args[check ? 'check' : 'write'] = true;
49
+ }
50
+ const serialized_args = args_serialize(forwarded_args);
51
+ if (patterns?.length) {
52
+ serialized_args.push(...patterns);
53
+ } else {
54
+ serialized_args.push(`${dir}**/*.{${extensions}}`);
55
+ if (dir === paths.source) {
56
+ serialized_args.push(`${paths.root}{${root_paths}}`);
57
+ }
58
+ }
59
+ const spawned = await spawn_cli(prettier_cli, serialized_args, log);
60
+ if (!spawned)
61
+ throw Error(
62
+ `failed to find \`${to_cli_name(prettier_cli)}\` CLI locally or globally, do you need to run \`${pm_cli} install\`?`,
63
+ );
64
+ return spawned;
65
+ };
@@ -0,0 +1,49 @@
1
+ import prettier from 'prettier';
2
+ import {extname} from 'node:path';
3
+
4
+ import {package_json_load} from './package_json.ts';
5
+
6
+ let cached_base_options: prettier.Options | undefined;
7
+
8
+ /**
9
+ * Formats a file with Prettier.
10
+ * @param content
11
+ * @param options
12
+ * @param base_options - defaults to the the cwd's package.json `prettier` value
13
+ */
14
+ export const format_file = async (
15
+ content: string,
16
+ options: prettier.Options,
17
+ base_options: prettier.Options | null | undefined = cached_base_options,
18
+ ): Promise<string> => {
19
+ const final_base_options =
20
+ base_options !== undefined
21
+ ? base_options
22
+ : (cached_base_options = (await package_json_load()).prettier as any);
23
+ let final_options = options;
24
+ if (options.filepath && !options.parser) {
25
+ const {filepath, ...rest} = options;
26
+ const parser = infer_parser(filepath);
27
+ if (parser) final_options = {...rest, parser};
28
+ }
29
+ try {
30
+ return await prettier.format(content, {...final_base_options, ...final_options});
31
+ } catch (_err) {
32
+ return content;
33
+ }
34
+ };
35
+
36
+ // This is just a simple convenience for callers so they can pass a file path.
37
+ // They can provide the Prettier `options.parser` for custom extensions.
38
+ const infer_parser = (path: string): string | null => {
39
+ const extension = extname(path).substring(1);
40
+ switch (extension) {
41
+ case 'svelte':
42
+ case 'xml': {
43
+ return extension;
44
+ }
45
+ default: {
46
+ return null;
47
+ }
48
+ }
49
+ };