@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,206 @@
1
+ import {styleText as st} from 'node:util';
2
+ import {print_ms, print_error} from '@fuzdev/fuz_util/print.js';
3
+ import {plural} from '@fuzdev/fuz_util/string.js';
4
+ import {z} from 'zod';
5
+
6
+ import {TaskError, type Task} from './task.ts';
7
+ import {run_gen} from './run_gen.ts';
8
+ import {RawInputPath, to_input_paths} from './input_path.ts';
9
+ import {format_file} from './format_file.ts';
10
+ import {print_path} from './paths.ts';
11
+ import {log_error_reasons} from './task_logging.ts';
12
+ import {
13
+ write_gen_results,
14
+ analyze_gen_results,
15
+ find_genfiles,
16
+ load_genfiles,
17
+ type AnalyzedGenResult,
18
+ type GenResults,
19
+ } from './gen.ts';
20
+ import {SOURCE_DIRNAME} from './constants.ts';
21
+
22
+ /** @nodocs */
23
+ export const Args = z.strictObject({
24
+ _: z.array(RawInputPath).meta({description: 'input paths to generate'}).default([SOURCE_DIRNAME]),
25
+ root_dirs: z
26
+ .array(z.string())
27
+ .meta({description: 'root directories to resolve input paths against'}) // TODO `PathId` schema
28
+ .default([process.cwd()]),
29
+ check: z
30
+ .boolean()
31
+ .meta({description: 'exit with a nonzero code if any files need to be generated'})
32
+ .default(false),
33
+ });
34
+ export type Args = z.infer<typeof Args>;
35
+
36
+ /** @nodocs */
37
+ export const task: Task<Args> = {
38
+ summary: 'run code generation scripts',
39
+ Args,
40
+ run: async ({args, filer, log, timings, config, invoke_task}): Promise<void> => {
41
+ const {_: raw_input_paths, root_dirs, check} = args;
42
+
43
+ const input_paths = to_input_paths(raw_input_paths);
44
+
45
+ // load all of the gen modules
46
+ const found = await find_genfiles(input_paths, root_dirs, config, timings);
47
+ if (!found.ok) {
48
+ if (found.type === 'input_directories_with_no_files') {
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`
50
+ log.info('no gen modules found in ' + input_paths.join(', '));
51
+ return;
52
+ } else {
53
+ log_error_reasons(log, found.reasons);
54
+ throw new TaskError('Failed to find gen modules.');
55
+ }
56
+ }
57
+ const found_genfiles = found.value;
58
+ log.info(
59
+ 'gen files',
60
+ found_genfiles.resolved_input_files.map((f) => f.id),
61
+ );
62
+ const loaded = await load_genfiles(found_genfiles, timings);
63
+ if (!loaded.ok) {
64
+ log_error_reasons(log, loaded.reasons);
65
+ throw new TaskError('Failed to load gen modules.');
66
+ }
67
+ const loaded_genfiles = loaded.value;
68
+
69
+ // run `gen` on each of the modules
70
+ const timing_to_generate_code = timings.start('generate code'); // TODO this ignores `gen_results.elapsed` - should it return `Timings` instead?
71
+ const gen_results = await run_gen(
72
+ loaded_genfiles.modules,
73
+ config,
74
+ filer,
75
+ log,
76
+ timings,
77
+ invoke_task,
78
+ format_file,
79
+ );
80
+ timing_to_generate_code();
81
+
82
+ const fail_count = gen_results.failures.length;
83
+ const analyzed_gen_results = await analyze_gen_results(gen_results);
84
+ if (check) {
85
+ // check if any files changed, and if so, throw errors,
86
+ // but if there are gen failures, skip the check and defer to their errors
87
+ if (!fail_count) {
88
+ log.info('checking generated files for changes');
89
+ const timing_to_check_results = timings.start('check results for changes');
90
+ timing_to_check_results();
91
+
92
+ let has_unexpected_changes = false;
93
+ for (const analyzed of analyzed_gen_results) {
94
+ if (!analyzed.has_changed) continue;
95
+ has_unexpected_changes = true;
96
+ log.error(
97
+ st(
98
+ 'red',
99
+ `Generated file ${print_path(analyzed.file.id)} via ${print_path(
100
+ analyzed.file.origin_id,
101
+ )} ${analyzed.is_new ? 'is new' : 'has changed'}.`,
102
+ ),
103
+ );
104
+ }
105
+ if (has_unexpected_changes) {
106
+ throw new TaskError(
107
+ 'Failed gen check. Some generated files have unexpectedly changed.' +
108
+ ' Run `gro gen` and try again.',
109
+ );
110
+ }
111
+ log.info('check passed, no files have changed');
112
+ }
113
+ } else {
114
+ const timing_to_output_results = timings.start('output results');
115
+ await write_gen_results(gen_results, analyzed_gen_results, log);
116
+ timing_to_output_results();
117
+ }
118
+
119
+ // collect and format output with summary
120
+ const output_lines = collect_output_lines(gen_results, analyzed_gen_results);
121
+ const new_count = analyzed_gen_results.filter((r) => r.is_new).length;
122
+ const changed_count = analyzed_gen_results.filter((r) => r.has_changed && !r.is_new).length;
123
+ const unchanged_count = analyzed_gen_results.filter((r) => !r.is_new && !r.has_changed).length;
124
+ const error_count = gen_results.failures.length;
125
+
126
+ log.info(
127
+ format_gen_output(output_lines) +
128
+ `\n\n\t${new_count} ${st(new_count > 0 ? 'green' : 'gray', 'new')}, ${changed_count} ${st(changed_count > 0 ? 'cyan' : 'gray', 'changed')}, ${unchanged_count} ${st('gray', 'unchanged')}${error_count ? `, ${error_count} ${st('red', 'error' + plural(error_count))}` : ''} from ${gen_results.input_count} input file${plural(gen_results.input_count)}`,
129
+ );
130
+
131
+ if (fail_count) {
132
+ for (const result of gen_results.failures) {
133
+ log.error(result.reason, '\n', print_error(result.error));
134
+ }
135
+ throw new TaskError(`Failed to generate ${fail_count} file${plural(fail_count)}.`);
136
+ }
137
+ },
138
+ };
139
+
140
+ interface GenStatus {
141
+ symbol: string;
142
+ color: Parameters<typeof st>[0];
143
+ text: string;
144
+ }
145
+
146
+ const format_gen_status = (analyzed: AnalyzedGenResult | undefined): GenStatus => {
147
+ if (!analyzed) return {symbol: '?', color: 'gray', text: 'unknown'};
148
+ if (analyzed.is_new) return {symbol: '●', color: 'green', text: 'new'};
149
+ if (analyzed.has_changed) return {symbol: '◐', color: 'cyan', text: 'changed'};
150
+ return {symbol: '○', color: 'gray', text: 'unchanged'};
151
+ };
152
+
153
+ interface OutputLine {
154
+ status: GenStatus;
155
+ elapsed: string;
156
+ source: string;
157
+ target: string;
158
+ is_error: boolean;
159
+ }
160
+
161
+ const collect_output_lines = (
162
+ gen_results: GenResults,
163
+ analyzed_gen_results: Array<AnalyzedGenResult>,
164
+ ): Array<OutputLine> => {
165
+ const output_lines: Array<OutputLine> = [];
166
+
167
+ for (const result of gen_results.results) {
168
+ if (result.ok) {
169
+ for (const file of result.files) {
170
+ const analyzed = analyzed_gen_results.find((a) => a.file.id === file.id);
171
+ output_lines.push({
172
+ status: format_gen_status(analyzed),
173
+ elapsed: print_ms(result.elapsed),
174
+ source: print_path(result.id),
175
+ target: print_path(file.id),
176
+ is_error: false,
177
+ });
178
+ }
179
+ } else {
180
+ output_lines.push({
181
+ status: {symbol: '🞩', color: 'red', text: 'error'},
182
+ elapsed: print_ms(result.elapsed),
183
+ source: print_path(result.id),
184
+ target: st('red', result.error.stack || result.error.message || 'error'),
185
+ is_error: true,
186
+ });
187
+ }
188
+ }
189
+
190
+ return output_lines;
191
+ };
192
+
193
+ const format_gen_output = (output_lines: Array<OutputLine>): string => {
194
+ // calculate column widths for alignment
195
+ const max_elapsed_length = Math.max(...output_lines.map((l) => l.elapsed.length));
196
+ const max_source_length = Math.max(...output_lines.map((l) => l.source.length));
197
+
198
+ // format the output lines
199
+ let log_result = 'gen results:';
200
+ for (const line of output_lines) {
201
+ const elapsed_text = line.elapsed.padStart(max_elapsed_length);
202
+ const source_text = line.source.padEnd(max_source_length);
203
+ log_result += `\n\t${st(line.status.color, line.status.symbol)} ${elapsed_text} ${source_text} → ${line.target}`;
204
+ }
205
+ return log_result;
206
+ };
package/src/lib/gen.ts ADDED
@@ -0,0 +1,406 @@
1
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
2
+ import {join, basename, dirname, isAbsolute} from 'node:path';
3
+ import {mkdir, readFile, writeFile} from 'node:fs/promises';
4
+ import type {Result} from '@fuzdev/fuz_util/result.js';
5
+ import type {Timings} from '@fuzdev/fuz_util/timings.js';
6
+ import {styleText as st} from 'node:util';
7
+ import type {PathId} from '@fuzdev/fuz_util/path.js';
8
+ import {each_concurrent, map_concurrent} from '@fuzdev/fuz_util/async.js';
9
+ import {fs_search} from '@fuzdev/fuz_util/fs.js';
10
+
11
+ import {print_path} from './paths.ts';
12
+ import type {GroConfig} from './gro_config.ts';
13
+ import type {ParsedSvelteConfig} from './svelte_config.ts';
14
+ import {load_modules, type LoadModulesFailure, type ModuleMeta} from './modules.ts';
15
+ import {
16
+ InputPath,
17
+ resolve_input_files,
18
+ resolve_input_paths,
19
+ type ResolvedInputFile,
20
+ type ResolvedInputPath,
21
+ } from './input_path.ts';
22
+ import type {Filer} from './filer.ts';
23
+ import type {InvokeTask} from './task.ts';
24
+
25
+ export const GEN_FILE_PATTERN_TEXT = 'gen';
26
+ export const GEN_FILE_PATTERN = '.' + GEN_FILE_PATTERN_TEXT + '.';
27
+
28
+ export const is_gen_path = (path: string): boolean => path.includes(GEN_FILE_PATTERN);
29
+
30
+ export interface GenResult {
31
+ origin_id: PathId;
32
+ files: Array<GenFile>;
33
+ }
34
+ export interface GenFile {
35
+ id: PathId;
36
+ content: string;
37
+ origin_id: PathId;
38
+ format: boolean;
39
+ }
40
+
41
+ export type GenDependencies = 'all' | GenDependenciesConfig | GenDependenciesResolver;
42
+
43
+ export interface GenDependenciesConfig {
44
+ patterns?: Array<RegExp>;
45
+ files?: Array<PathId>;
46
+ }
47
+
48
+ export type GenDependenciesResolver = (
49
+ ctx: GenContext,
50
+ ) => GenDependenciesConfig | 'all' | null | Promise<GenDependenciesConfig | 'all' | null>;
51
+
52
+ export type Gen = GenFunction | GenConfig;
53
+
54
+ export type GenFunction = (ctx: GenContext) => RawGenResult | Promise<RawGenResult>;
55
+
56
+ // TODO add a GenConfigRaw variant and change `normalize_gen_config` to `gen_cook_config`
57
+ export interface GenConfig {
58
+ generate: GenFunction;
59
+ dependencies?: GenDependencies;
60
+ // TODO think about what could be added
61
+ // cache?: boolean;
62
+ }
63
+
64
+ export interface GenContext {
65
+ config: GroConfig;
66
+ svelte_config: ParsedSvelteConfig;
67
+ filer: Filer;
68
+ log: Logger;
69
+ timings: Timings;
70
+ invoke_task: InvokeTask;
71
+ /**
72
+ * Same as `import.meta.url` but in path form.
73
+ */
74
+ origin_id: PathId;
75
+ /**
76
+ * The `origin_id` relative to the root dir.
77
+ */
78
+ origin_path: string;
79
+ /**
80
+ * The file that triggered dependency checking.
81
+ * Only available when resolving dependencies dynamically.
82
+ * `undefined` during actual generation.
83
+ */
84
+ changed_file_id: PathId | undefined;
85
+ }
86
+
87
+ // TODO consider other return data - metadata? effects? non-file build artifacts?
88
+ export type RawGenResult = string | RawGenFile | null | Array<RawGenResult>;
89
+ export interface RawGenFile {
90
+ content: string;
91
+ // Defaults to file name without the `.gen`, and can be a relative path.
92
+ // TODO maybe support a transform pattern or callback fn? like '[stem].thing.[ext]'
93
+ filename?: string;
94
+ format?: boolean; // defaults to `true`
95
+ }
96
+
97
+ export interface GenResults {
98
+ results: Array<GenfileModuleResult>;
99
+ successes: Array<GenfileModuleResultSuccess>;
100
+ failures: Array<GenfileModuleResultFailure>;
101
+ input_count: number;
102
+ output_count: number;
103
+ elapsed: number;
104
+ }
105
+ export type GenfileModuleResult = GenfileModuleResultSuccess | GenfileModuleResultFailure;
106
+ export interface GenfileModuleResultSuccess {
107
+ ok: true;
108
+ id: PathId;
109
+ files: Array<GenFile>;
110
+ elapsed: number;
111
+ }
112
+ export interface GenfileModuleResultFailure {
113
+ ok: false;
114
+ id: PathId;
115
+ reason: string;
116
+ error: Error;
117
+ elapsed: number;
118
+ }
119
+
120
+ export const to_gen_result = (origin_id: PathId, raw_result: RawGenResult): GenResult => {
121
+ return {
122
+ origin_id,
123
+ files: to_gen_files(origin_id, raw_result),
124
+ };
125
+ };
126
+
127
+ const to_gen_files = (origin_id: PathId, raw_result: RawGenResult): Array<GenFile> => {
128
+ if (raw_result === null) {
129
+ return [];
130
+ } else if (typeof raw_result === 'string') {
131
+ return [to_gen_file(origin_id, {content: raw_result})];
132
+ } else if (Array.isArray(raw_result)) {
133
+ const files = raw_result.flatMap((f) => to_gen_files(origin_id, f));
134
+ validate_gen_files(files);
135
+ return files;
136
+ }
137
+ return [to_gen_file(origin_id, raw_result)];
138
+ };
139
+
140
+ const to_gen_file = (origin_id: PathId, raw_gen_file: RawGenFile): GenFile => {
141
+ const {content, filename, format = true} = raw_gen_file;
142
+ const id = to_output_file_id(origin_id, filename);
143
+ return {id, content, origin_id, format};
144
+ };
145
+
146
+ const to_output_file_id = (origin_id: PathId, raw_file_name: string | undefined): string => {
147
+ if (raw_file_name === '') {
148
+ throw Error(`Output file name cannot be an empty string`);
149
+ }
150
+ const filename = raw_file_name ?? to_output_file_name(basename(origin_id));
151
+ if (isAbsolute(filename)) return filename;
152
+ const dir = dirname(origin_id);
153
+ const output_file_id = join(dir, filename);
154
+ if (output_file_id === origin_id) {
155
+ throw Error('Gen origin and output file ids cannot be the same');
156
+ }
157
+ return output_file_id;
158
+ };
159
+
160
+ export const to_output_file_name = (filename: string): string => {
161
+ const parts = filename.split('.');
162
+ const gen_pattern_index = parts.indexOf(GEN_FILE_PATTERN_TEXT);
163
+ if (gen_pattern_index === -1) {
164
+ throw Error(`Invalid gen file name - '${GEN_FILE_PATTERN_TEXT}' not found in '${filename}'`);
165
+ }
166
+ if (gen_pattern_index !== parts.lastIndexOf(GEN_FILE_PATTERN_TEXT)) {
167
+ throw Error(
168
+ `Invalid gen file name - multiple instances of '${GEN_FILE_PATTERN_TEXT}' found in '${filename}'`,
169
+ );
170
+ }
171
+ if (gen_pattern_index < parts.length - 3) {
172
+ // This check is technically unneccessary,
173
+ // but ensures a consistent file naming convention.
174
+ throw Error(
175
+ `Invalid gen file name - only one additional extension is allowed to follow '${GEN_FILE_PATTERN}' in '${filename}'`,
176
+ );
177
+ }
178
+ const final_parts: Array<string> = [];
179
+ const has_different_ext = gen_pattern_index === parts.length - 3;
180
+ const length = has_different_ext ? parts.length - 1 : parts.length;
181
+ for (let i = 0; i < length; i++) {
182
+ if (i === gen_pattern_index) continue; // skip the `.gen.` pattern
183
+ if (i === length - 1 && parts[i] === '') continue; // allow empty extension
184
+ final_parts.push(parts[i]!);
185
+ }
186
+ return final_parts.join('.');
187
+ };
188
+
189
+ const validate_gen_files = (files: Array<GenFile>) => {
190
+ const ids = new Set();
191
+ for (const file of files) {
192
+ if (ids.has(file.id)) {
193
+ throw Error(`Duplicate gen file id: ${file.id}`);
194
+ }
195
+ ids.add(file.id);
196
+ }
197
+ };
198
+
199
+ export type AnalyzedGenResult =
200
+ | {
201
+ file: GenFile;
202
+ existing_content: string;
203
+ is_new: false;
204
+ has_changed: boolean;
205
+ }
206
+ | {
207
+ file: GenFile;
208
+ existing_content: null;
209
+ is_new: true;
210
+ has_changed: true;
211
+ };
212
+
213
+ export const analyze_gen_results = async (
214
+ gen_results: GenResults,
215
+ ): Promise<Array<AnalyzedGenResult>> => {
216
+ const files = gen_results.successes.flatMap((result) => result.files);
217
+ return map_concurrent(files, (file) => analyze_gen_result(file), 10);
218
+ };
219
+
220
+ export const analyze_gen_result = async (file: GenFile): Promise<AnalyzedGenResult> => {
221
+ let existing_content: string;
222
+ try {
223
+ existing_content = await readFile(file.id, 'utf8');
224
+ } catch (error) {
225
+ if (error.code === 'ENOENT') {
226
+ return {
227
+ file,
228
+ existing_content: null,
229
+ is_new: true,
230
+ has_changed: true,
231
+ };
232
+ }
233
+ throw error;
234
+ }
235
+ return {
236
+ file,
237
+ existing_content,
238
+ is_new: false,
239
+ has_changed: file.content !== existing_content,
240
+ };
241
+ };
242
+
243
+ export const write_gen_results = async (
244
+ gen_results: GenResults,
245
+ analyzed_gen_results: Array<AnalyzedGenResult>,
246
+ log: Logger,
247
+ ): Promise<void> => {
248
+ const files = gen_results.successes.flatMap((result) => result.files);
249
+ await each_concurrent(
250
+ files,
251
+ async (file) => {
252
+ const analyzed = analyzed_gen_results.find((r) => r.file.id === file.id);
253
+ if (!analyzed) throw Error('Expected to find analyzed result: ' + file.id);
254
+ const log_args = [print_path(file.id), 'generated from', print_path(file.origin_id)];
255
+ if (analyzed.is_new) {
256
+ log.info('writing new', ...log_args);
257
+ await mkdir(dirname(file.id), {recursive: true});
258
+ await writeFile(file.id, file.content);
259
+ } else if (analyzed.has_changed) {
260
+ log.info('writing changed', ...log_args);
261
+ await writeFile(file.id, file.content);
262
+ } else {
263
+ log.info('skipping unchanged', ...log_args);
264
+ }
265
+ },
266
+ 10,
267
+ );
268
+ };
269
+
270
+ export interface FoundGenfiles {
271
+ resolved_input_files: Array<ResolvedInputFile>;
272
+ resolved_input_files_by_root_dir: Map<PathId, Array<ResolvedInputFile>>;
273
+ resolved_input_paths: Array<ResolvedInputPath>;
274
+ }
275
+
276
+ export type FindGenfilesResult = Result<{value: FoundGenfiles}, FindGenfilesFailure>;
277
+ export type FindGenfilesFailure =
278
+ | {
279
+ type: 'unmapped_input_paths';
280
+ unmapped_input_paths: Array<InputPath>;
281
+ resolved_input_paths: Array<ResolvedInputPath>;
282
+ reasons: Array<string>;
283
+ }
284
+ | {
285
+ type: 'input_directories_with_no_files';
286
+ input_directories_with_no_files: Array<InputPath>;
287
+ resolved_input_files: Array<ResolvedInputFile>;
288
+ resolved_input_files_by_root_dir: Map<PathId, Array<ResolvedInputFile>>;
289
+ resolved_input_paths: Array<ResolvedInputPath>;
290
+ reasons: Array<string>;
291
+ };
292
+
293
+ /**
294
+ * Finds modules from input paths. (see `src/lib/input_path.ts` for more)
295
+ */
296
+ export const find_genfiles = async (
297
+ input_paths: Array<InputPath>,
298
+ root_dirs: Array<PathId>,
299
+ config: GroConfig,
300
+ timings?: Timings,
301
+ ): Promise<FindGenfilesResult> => {
302
+ const extensions: Array<string> = [GEN_FILE_PATTERN];
303
+
304
+ // Check which extension variation works - if it's a directory, prefer others first!
305
+ const timing_to_resolve_input_paths = timings?.start('resolve input paths');
306
+ const {resolved_input_paths, unmapped_input_paths} = await resolve_input_paths(
307
+ input_paths,
308
+ root_dirs,
309
+ extensions,
310
+ );
311
+ timing_to_resolve_input_paths?.();
312
+
313
+ // Error if any input path could not be mapped.
314
+ if (unmapped_input_paths.length) {
315
+ return {
316
+ ok: false,
317
+ type: 'unmapped_input_paths',
318
+ unmapped_input_paths,
319
+ resolved_input_paths,
320
+ reasons: unmapped_input_paths.map((input_path) =>
321
+ st('red', `Input path ${print_path(input_path)} cannot be mapped to a file or directory.`),
322
+ ),
323
+ };
324
+ }
325
+
326
+ // Find all of the files for any directories.
327
+ const timing_to_fs_search = timings?.start('find files');
328
+ const {resolved_input_files, resolved_input_files_by_root_dir, input_directories_with_no_files} =
329
+ await resolve_input_files(
330
+ resolved_input_paths,
331
+ async (id) =>
332
+ await fs_search(id, {
333
+ filter: config.search_filters,
334
+ file_filter: (p) => extensions.some((e) => p.includes(e)),
335
+ }),
336
+ );
337
+ timing_to_fs_search?.();
338
+
339
+ // Error if any input path has no files. (means we have an empty directory)
340
+ if (input_directories_with_no_files.length) {
341
+ return {
342
+ ok: false,
343
+ type: 'input_directories_with_no_files',
344
+ input_directories_with_no_files,
345
+ resolved_input_files,
346
+ resolved_input_files_by_root_dir,
347
+ resolved_input_paths,
348
+ reasons: input_directories_with_no_files.map((input_path) =>
349
+ st('red', `Input directory contains no matching files: ${print_path(input_path)}`),
350
+ ),
351
+ };
352
+ }
353
+
354
+ return {
355
+ ok: true,
356
+ value: {
357
+ resolved_input_files,
358
+ resolved_input_files_by_root_dir,
359
+ resolved_input_paths,
360
+ },
361
+ };
362
+ };
363
+
364
+ export interface GenfileModule {
365
+ gen: Gen;
366
+ }
367
+
368
+ export type GenfileModuleMeta = ModuleMeta<GenfileModule>;
369
+
370
+ export interface LoadedGenfiles {
371
+ modules: Array<GenfileModuleMeta>;
372
+ found_genfiles: FoundGenfiles;
373
+ }
374
+
375
+ export type LoadGenfilesResult = Result<{value: LoadedGenfiles}, LoadGenfilesFailure>;
376
+ export type LoadGenfilesFailure = LoadModulesFailure<GenfileModuleMeta>;
377
+
378
+ export const load_genfiles = async (
379
+ found_genfiles: FoundGenfiles,
380
+ timings?: Timings,
381
+ ): Promise<LoadGenfilesResult> => {
382
+ const loaded_modules = await load_modules(
383
+ found_genfiles.resolved_input_files,
384
+ validate_gen_module,
385
+ (resolved_input_file, mod): GenfileModuleMeta => ({id: resolved_input_file.id, mod}),
386
+ timings,
387
+ );
388
+ if (!loaded_modules.ok) {
389
+ return loaded_modules;
390
+ }
391
+ return {
392
+ ok: true,
393
+ value: {modules: loaded_modules.modules, found_genfiles},
394
+ };
395
+ };
396
+
397
+ export const validate_gen_module = (mod: Record<string, any>): mod is GenfileModule => {
398
+ if (typeof mod.gen === 'function') return true;
399
+ if (typeof mod.gen === 'object' && mod.gen !== null && typeof mod.gen.generate === 'function') {
400
+ return true;
401
+ }
402
+ return false;
403
+ };
404
+
405
+ export const normalize_gen_config = (gen: Gen): GenConfig =>
406
+ typeof gen === 'function' ? {generate: gen} : gen;