@ryanatkn/gro 0.129.4 → 0.129.5

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 (124) hide show
  1. package/dist/package.js +3 -3
  2. package/package.json +3 -2
  3. package/src/lib/args.test.ts +59 -0
  4. package/src/lib/args.ts +169 -0
  5. package/src/lib/build.task.ts +37 -0
  6. package/src/lib/changelog.test.ts +138 -0
  7. package/src/lib/changelog.ts +69 -0
  8. package/src/lib/changeset.task.ts +206 -0
  9. package/src/lib/changeset_helpers.ts +13 -0
  10. package/src/lib/check.task.ts +90 -0
  11. package/src/lib/clean.task.ts +46 -0
  12. package/src/lib/clean_fs.ts +54 -0
  13. package/src/lib/cli.ts +97 -0
  14. package/src/lib/commit.task.ts +33 -0
  15. package/src/lib/config.test.ts +71 -0
  16. package/src/lib/config.ts +161 -0
  17. package/src/lib/deploy.task.ts +243 -0
  18. package/src/lib/dev.task.ts +43 -0
  19. package/src/lib/docs/README.gen.md.ts +63 -0
  20. package/src/lib/docs/README.md +20 -0
  21. package/src/lib/docs/build.md +41 -0
  22. package/src/lib/docs/config.md +213 -0
  23. package/src/lib/docs/deploy.md +32 -0
  24. package/src/lib/docs/dev.md +40 -0
  25. package/src/lib/docs/gen.md +269 -0
  26. package/src/lib/docs/gro_plugin_sveltekit_app.md +113 -0
  27. package/src/lib/docs/package_json.md +33 -0
  28. package/src/lib/docs/plugin.md +50 -0
  29. package/src/lib/docs/publish.md +137 -0
  30. package/src/lib/docs/task.md +391 -0
  31. package/src/lib/docs/tasks.gen.md.ts +90 -0
  32. package/src/lib/docs/tasks.md +37 -0
  33. package/src/lib/docs/test.md +52 -0
  34. package/src/lib/env.ts +75 -0
  35. package/src/lib/esbuild_helpers.ts +50 -0
  36. package/src/lib/esbuild_plugin_external_worker.ts +92 -0
  37. package/src/lib/esbuild_plugin_svelte.test.ts +88 -0
  38. package/src/lib/esbuild_plugin_svelte.ts +108 -0
  39. package/src/lib/esbuild_plugin_sveltekit_local_imports.ts +31 -0
  40. package/src/lib/esbuild_plugin_sveltekit_shim_alias.ts +25 -0
  41. package/src/lib/esbuild_plugin_sveltekit_shim_app.ts +41 -0
  42. package/src/lib/esbuild_plugin_sveltekit_shim_env.ts +46 -0
  43. package/src/lib/format.task.ts +30 -0
  44. package/src/lib/format_directory.ts +55 -0
  45. package/src/lib/format_file.test.ts +20 -0
  46. package/src/lib/format_file.ts +49 -0
  47. package/src/lib/fs.ts +18 -0
  48. package/src/lib/gen.task.ts +134 -0
  49. package/src/lib/gen.test.ts +306 -0
  50. package/src/lib/gen.ts +360 -0
  51. package/src/lib/git.test.ts +34 -0
  52. package/src/lib/git.ts +297 -0
  53. package/src/lib/github.ts +46 -0
  54. package/src/lib/gro.config.default.ts +34 -0
  55. package/src/lib/gro.ts +25 -0
  56. package/src/lib/gro_helpers.ts +101 -0
  57. package/src/lib/gro_plugin_gen.ts +95 -0
  58. package/src/lib/gro_plugin_server.ts +288 -0
  59. package/src/lib/gro_plugin_sveltekit_app.ts +257 -0
  60. package/src/lib/gro_plugin_sveltekit_library.ts +74 -0
  61. package/src/lib/hash.test.ts +33 -0
  62. package/src/lib/hash.ts +19 -0
  63. package/src/lib/index.ts +4 -0
  64. package/src/lib/input_path.test.ts +230 -0
  65. package/src/lib/input_path.ts +255 -0
  66. package/src/lib/invoke.ts +27 -0
  67. package/src/lib/invoke_task.ts +116 -0
  68. package/src/lib/lint.task.ts +38 -0
  69. package/src/lib/loader.test.ts +49 -0
  70. package/src/lib/loader.ts +226 -0
  71. package/src/lib/module.test.ts +46 -0
  72. package/src/lib/module.ts +13 -0
  73. package/src/lib/modules.test.ts +63 -0
  74. package/src/lib/modules.ts +112 -0
  75. package/src/lib/package.gen.ts +33 -0
  76. package/src/lib/package.ts +998 -0
  77. package/src/lib/package_json.test.ts +101 -0
  78. package/src/lib/package_json.ts +330 -0
  79. package/src/lib/package_meta.ts +86 -0
  80. package/src/lib/path.ts +23 -0
  81. package/src/lib/path_constants.ts +30 -0
  82. package/src/lib/paths.test.ts +77 -0
  83. package/src/lib/paths.ts +101 -0
  84. package/src/lib/plugin.test.ts +57 -0
  85. package/src/lib/plugin.ts +113 -0
  86. package/src/lib/publish.task.ts +194 -0
  87. package/src/lib/register.ts +3 -0
  88. package/src/lib/reinstall.task.ts +42 -0
  89. package/src/lib/release.task.ts +21 -0
  90. package/src/lib/resolve.task.ts +43 -0
  91. package/src/lib/resolve_node_specifier.test.ts +31 -0
  92. package/src/lib/resolve_node_specifier.ts +55 -0
  93. package/src/lib/resolve_specifier.test.ts +76 -0
  94. package/src/lib/resolve_specifier.ts +61 -0
  95. package/src/lib/run.task.ts +41 -0
  96. package/src/lib/run_gen.test.ts +196 -0
  97. package/src/lib/run_gen.ts +95 -0
  98. package/src/lib/run_task.test.ts +86 -0
  99. package/src/lib/run_task.ts +75 -0
  100. package/src/lib/search_fs.test.ts +56 -0
  101. package/src/lib/search_fs.ts +93 -0
  102. package/src/lib/src_json.test.ts +49 -0
  103. package/src/lib/src_json.ts +153 -0
  104. package/src/lib/svelte_helpers.ts +2 -0
  105. package/src/lib/sveltekit_config.ts +101 -0
  106. package/src/lib/sveltekit_config_global.ts +6 -0
  107. package/src/lib/sveltekit_helpers.ts +132 -0
  108. package/src/lib/sveltekit_shim_app.ts +42 -0
  109. package/src/lib/sveltekit_shim_app_environment.ts +14 -0
  110. package/src/lib/sveltekit_shim_app_forms.ts +20 -0
  111. package/src/lib/sveltekit_shim_app_navigation.ts +23 -0
  112. package/src/lib/sveltekit_shim_app_paths.ts +16 -0
  113. package/src/lib/sveltekit_shim_app_stores.ts +25 -0
  114. package/src/lib/sveltekit_shim_env.ts +45 -0
  115. package/src/lib/sync.task.ts +47 -0
  116. package/src/lib/task.test.ts +84 -0
  117. package/src/lib/task.ts +235 -0
  118. package/src/lib/task_logging.ts +180 -0
  119. package/src/lib/test.task.ts +50 -0
  120. package/src/lib/throttle.test.ts +52 -0
  121. package/src/lib/throttle.ts +63 -0
  122. package/src/lib/typecheck.task.ts +57 -0
  123. package/src/lib/upgrade.task.ts +108 -0
  124. package/src/lib/watch_dir.ts +88 -0
@@ -0,0 +1,306 @@
1
+ import {test} from 'uvu';
2
+ import * as assert from 'uvu/assert';
3
+ import {resolve} from 'node:path';
4
+
5
+ import {to_gen_result, find_genfiles, validate_gen_module} from './gen.js';
6
+ import {paths} from './paths.js';
7
+ import {create_empty_config} from './config.js';
8
+
9
+ const origin_id = resolve('src/foo.gen.ts');
10
+
11
+ test('to_gen_result plain string', () => {
12
+ assert.equal(to_gen_result(origin_id, '/**/'), {
13
+ origin_id,
14
+ files: [{id: resolve('src/foo.ts'), content: '/**/', origin_id, format: true}],
15
+ });
16
+ });
17
+
18
+ test('to_gen_result object with a content string', () => {
19
+ assert.equal(to_gen_result(origin_id, {content: '/**/'}), {
20
+ origin_id,
21
+ files: [{id: resolve('src/foo.ts'), content: '/**/', origin_id, format: true}],
22
+ });
23
+ });
24
+
25
+ test('to_gen_result fail with an unresolved id', () => {
26
+ assert.throws(() => to_gen_result('src/foo.ts', {content: '/**/'}));
27
+ });
28
+
29
+ test('to_gen_result fail with a build id', () => {
30
+ assert.throws(() => to_gen_result(resolve('.gro/foo.js'), {content: '/**/'}));
31
+ });
32
+
33
+ test('to_gen_result fail with an empty id', () => {
34
+ assert.throws(() => to_gen_result('', {content: '/**/'}));
35
+ });
36
+
37
+ test('to_gen_result custom file name', () => {
38
+ assert.equal(
39
+ to_gen_result(origin_id, {
40
+ filename: 'fooz.ts',
41
+ content: '/**/',
42
+ }),
43
+ {
44
+ origin_id,
45
+ files: [{id: resolve('src/fooz.ts'), content: '/**/', origin_id, format: true}],
46
+ },
47
+ );
48
+ });
49
+
50
+ test('to_gen_result custom file name that matches the default file name', () => {
51
+ assert.equal(
52
+ to_gen_result(origin_id, {
53
+ filename: 'foo.ts',
54
+ content: '/**/',
55
+ }),
56
+ {
57
+ origin_id,
58
+ files: [{id: resolve('src/foo.ts'), content: '/**/', origin_id, format: true}],
59
+ },
60
+ );
61
+ });
62
+
63
+ test('to_gen_result fail when custom file name explicitly matches the origin', () => {
64
+ assert.throws(() => {
65
+ to_gen_result(origin_id, {
66
+ filename: 'foo.gen.ts',
67
+ content: '/**/',
68
+ });
69
+ });
70
+ });
71
+
72
+ test('to_gen_result fail when file name implicitly matches the origin', () => {
73
+ assert.throws(() => {
74
+ to_gen_result(resolve('src/foo.ts'), {content: '/**/'});
75
+ });
76
+ });
77
+
78
+ test('to_gen_result fail with an empty file name', () => {
79
+ assert.throws(() => to_gen_result(origin_id, {filename: '', content: '/**/'}));
80
+ });
81
+
82
+ test('to_gen_result additional file name parts', () => {
83
+ assert.equal(to_gen_result(resolve('src/foo.bar.gen.ts'), {content: '/**/'}), {
84
+ origin_id: resolve('src/foo.bar.gen.ts'),
85
+ files: [
86
+ {
87
+ id: resolve('src/foo.bar.ts'),
88
+ content: '/**/',
89
+ origin_id: resolve('src/foo.bar.gen.ts'),
90
+ format: true,
91
+ },
92
+ ],
93
+ });
94
+ });
95
+
96
+ test('to_gen_result js', () => {
97
+ assert.equal(
98
+ to_gen_result(origin_id, {
99
+ filename: 'foo.js',
100
+ content: '/**/',
101
+ }),
102
+ {
103
+ origin_id,
104
+ files: [{id: resolve('src/foo.js'), content: '/**/', origin_id, format: true}],
105
+ },
106
+ );
107
+ });
108
+
109
+ test('to_gen_result implicit custom file extension', () => {
110
+ assert.equal(to_gen_result(resolve('src/foo.gen.json.ts'), '[/**/]'), {
111
+ origin_id: resolve('src/foo.gen.json.ts'),
112
+ files: [
113
+ {
114
+ id: resolve('src/foo.json'),
115
+ content: '[/**/]',
116
+ origin_id: resolve('src/foo.gen.json.ts'),
117
+ format: true,
118
+ },
119
+ ],
120
+ });
121
+ });
122
+
123
+ test('to_gen_result implicit empty file extension', () => {
124
+ assert.equal(to_gen_result(resolve('src/foo.gen..ts'), '[/**/]'), {
125
+ origin_id: resolve('src/foo.gen..ts'),
126
+ files: [
127
+ {
128
+ id: resolve('src/foo'),
129
+ content: '[/**/]',
130
+ origin_id: resolve('src/foo.gen..ts'),
131
+ format: true,
132
+ },
133
+ ],
134
+ });
135
+ });
136
+
137
+ test('to_gen_result implicit custom file extension with additional file name parts', () => {
138
+ assert.equal(to_gen_result(resolve('src/foo.bar.gen.json.ts'), {content: '[/**/]'}), {
139
+ origin_id: resolve('src/foo.bar.gen.json.ts'),
140
+ files: [
141
+ {
142
+ id: resolve('src/foo.bar.json'),
143
+ content: '[/**/]',
144
+ origin_id: resolve('src/foo.bar.gen.json.ts'),
145
+ format: true,
146
+ },
147
+ ],
148
+ });
149
+ });
150
+
151
+ test('to_gen_result implicit custom file extension with many dots in between', () => {
152
+ assert.equal(to_gen_result(resolve('src/foo...gen.ts'), '[/**/]'), {
153
+ origin_id: resolve('src/foo...gen.ts'),
154
+ files: [
155
+ {
156
+ id: resolve('src/foo...ts'),
157
+ content: '[/**/]',
158
+ origin_id: resolve('src/foo...gen.ts'),
159
+ format: true,
160
+ },
161
+ ],
162
+ });
163
+ });
164
+
165
+ test('to_gen_result fail with two parts following the .gen. pattern in the file name', () => {
166
+ // This just ensures consistent file names - maybe loosen the restriction?
167
+ // You can still implicitly name files like this,
168
+ // but you have to move ".bar" before ".gen".
169
+ assert.throws(() => to_gen_result(resolve('src/foo.gen.bar.json.ts'), '/**/'));
170
+ });
171
+
172
+ test('to_gen_result fail implicit file extension ending with a dot', () => {
173
+ // This just ensures consistent file names - maybe loosen the restriction?
174
+ // This one is more restrictive than the above,
175
+ // because to have a file ending with a dot
176
+ // you have to use an explicit file name.
177
+ assert.throws(() => to_gen_result(resolve('src/foo.gen...ts'), '[/**/]'));
178
+ });
179
+
180
+ test('to_gen_result fail without a .gen. pattern in the file name', () => {
181
+ assert.throws(() => {
182
+ to_gen_result(resolve('src/foo.ts'), '/**/');
183
+ });
184
+ });
185
+
186
+ test('to_gen_result fail without a .gen. pattern in a file name that has multiple other patterns', () => {
187
+ assert.throws(() => {
188
+ to_gen_result(resolve('src/foo.bar.baz.ts'), '/**/');
189
+ });
190
+ });
191
+
192
+ test('to_gen_result fail with two .gen. patterns in the file name', () => {
193
+ assert.throws(() => to_gen_result(resolve('src/lib/gen.gen.ts'), '/**/'));
194
+ assert.throws(() => to_gen_result(resolve('src/foo.gen.gen.ts'), '/**/'));
195
+ assert.throws(() => to_gen_result(resolve('src/foo.gen.bar.gen.ts'), '/**/'));
196
+ assert.throws(() => to_gen_result(resolve('src/foo.gen.bar.gen.baz.ts'), '/**/'));
197
+ });
198
+
199
+ test('to_gen_result explicit custom file extension', () => {
200
+ assert.equal(
201
+ to_gen_result(origin_id, {
202
+ filename: 'foo.json',
203
+ content: '[/**/]',
204
+ }),
205
+ {
206
+ origin_id,
207
+ files: [{id: resolve('src/foo.json'), content: '[/**/]', origin_id, format: true}],
208
+ },
209
+ );
210
+ });
211
+
212
+ test('to_gen_result explicit custom empty file extension', () => {
213
+ assert.equal(
214
+ to_gen_result(origin_id, {
215
+ filename: 'foo',
216
+ content: '[/**/]',
217
+ }),
218
+ {
219
+ origin_id,
220
+ files: [{id: resolve('src/foo'), content: '[/**/]', origin_id, format: true}],
221
+ },
222
+ );
223
+ });
224
+
225
+ test('to_gen_result explicit custom file extension ending with a dot', () => {
226
+ assert.equal(
227
+ to_gen_result(origin_id, {
228
+ filename: 'foo.',
229
+ content: '[/**/]',
230
+ }),
231
+ {
232
+ origin_id,
233
+ files: [{id: resolve('src/foo.'), content: '[/**/]', origin_id, format: true}],
234
+ },
235
+ );
236
+ });
237
+
238
+ test('to_gen_result simple array of raw files', () => {
239
+ assert.equal(
240
+ to_gen_result(origin_id, [{content: '/*1*/'}, {filename: 'foo2.ts', content: '/*2*/'}]),
241
+ {
242
+ origin_id,
243
+ files: [
244
+ {id: resolve('src/foo.ts'), content: '/*1*/', origin_id, format: true},
245
+ {id: resolve('src/foo2.ts'), content: '/*2*/', origin_id, format: true},
246
+ ],
247
+ },
248
+ );
249
+ });
250
+
251
+ test('to_gen_result complex array of raw files', () => {
252
+ assert.equal(
253
+ to_gen_result(origin_id, [
254
+ {content: '/*1*/'},
255
+ {filename: 'foo2.ts', content: '/*2*/'},
256
+ {filename: 'foo3.ts', content: '/*3*/'},
257
+ {filename: 'foo4.ts', content: '/*4*/'},
258
+ {filename: 'foo5.json', content: '[/*5*/]'},
259
+ ]),
260
+ {
261
+ origin_id,
262
+ files: [
263
+ {id: resolve('src/foo.ts'), content: '/*1*/', origin_id, format: true},
264
+ {id: resolve('src/foo2.ts'), content: '/*2*/', origin_id, format: true},
265
+ {id: resolve('src/foo3.ts'), content: '/*3*/', origin_id, format: true},
266
+ {id: resolve('src/foo4.ts'), content: '/*4*/', origin_id, format: true},
267
+ {id: resolve('src/foo5.json'), content: '[/*5*/]', origin_id, format: true},
268
+ ],
269
+ },
270
+ );
271
+ });
272
+
273
+ test('to_gen_result fail with duplicate names because of omissions', () => {
274
+ assert.throws(() => {
275
+ to_gen_result(origin_id, [{content: '/*1*/'}, {content: '/*2*/'}]);
276
+ });
277
+ });
278
+
279
+ test('to_gen_result fail with duplicate explicit names', () => {
280
+ assert.throws(() => {
281
+ to_gen_result(origin_id, [
282
+ {filename: 'foo.ts', content: '/*1*/'},
283
+ {filename: 'foo.ts', content: '/*2*/'},
284
+ ]);
285
+ });
286
+ });
287
+
288
+ test('to_gen_result fail with duplicate explicit and implicit names', () => {
289
+ assert.throws(() => {
290
+ to_gen_result(origin_id, [{content: '/*1*/'}, {filename: 'foo.ts', content: '/*2*/'}]);
291
+ });
292
+ });
293
+
294
+ test('validate_gen_module basic behavior', () => {
295
+ assert.ok(validate_gen_module({gen: Function.prototype}));
296
+ assert.ok(!validate_gen_module({gen: {}}));
297
+ assert.ok(!validate_gen_module({task: {run: {}}}));
298
+ });
299
+
300
+ test('find_genfiles_result finds gen modules in a directory', () => {
301
+ const find_genfiles_result = find_genfiles(['docs'], [paths.lib], create_empty_config());
302
+ assert.ok(find_genfiles_result.ok);
303
+ assert.ok(find_genfiles_result.value.resolved_input_paths.length);
304
+ });
305
+
306
+ test.run();
package/src/lib/gen.ts ADDED
@@ -0,0 +1,360 @@
1
+ import type {Logger} from '@ryanatkn/belt/log.js';
2
+ import {join, basename, dirname, isAbsolute} from 'node:path';
3
+ import {mkdir, readFile, writeFile} from 'node:fs/promises';
4
+ import {z} from 'zod';
5
+ import type {Result} from '@ryanatkn/belt/result.js';
6
+ import type {Timings} from '@ryanatkn/belt/timings.js';
7
+ import {red} from '@ryanatkn/belt/styletext.js';
8
+ import {existsSync} from 'node:fs';
9
+
10
+ import {print_path} from './paths.js';
11
+ import type {Path_Id} from './path.js';
12
+ import type {Gro_Config} from './config.js';
13
+ import type {Parsed_Sveltekit_Config} from './sveltekit_config.js';
14
+ import {load_modules, type Load_Modules_Failure, type Module_Meta} from './modules.js';
15
+ import {
16
+ Input_Path,
17
+ resolve_input_files,
18
+ resolve_input_paths,
19
+ type Resolved_Input_File,
20
+ type Resolved_Input_Path,
21
+ } from './input_path.js';
22
+ import {search_fs} from './search_fs.js';
23
+
24
+ export const GEN_FILE_PATTERN_TEXT = 'gen';
25
+ export const GEN_FILE_PATTERN = '.' + GEN_FILE_PATTERN_TEXT + '.';
26
+
27
+ export const is_gen_path = (path: string): boolean => path.includes(GEN_FILE_PATTERN);
28
+
29
+ export interface Gen_Result {
30
+ origin_id: Path_Id;
31
+ files: Gen_File[];
32
+ }
33
+ export interface Gen_File {
34
+ id: Path_Id;
35
+ content: string;
36
+ origin_id: Path_Id;
37
+ format: boolean;
38
+ }
39
+
40
+ export type Gen = (ctx: Gen_Context) => Raw_Gen_Result | Promise<Raw_Gen_Result>;
41
+ export interface Gen_Context {
42
+ config: Gro_Config;
43
+ sveltekit_config: Parsed_Sveltekit_Config;
44
+ /**
45
+ * Same as `import.meta.url` but in path form.
46
+ */
47
+ origin_id: Path_Id;
48
+ log: Logger;
49
+ }
50
+ // TODO consider other return data - metadata? effects? non-file build artifacts?
51
+ export type Raw_Gen_Result = string | Raw_Gen_File | null | Raw_Gen_Result[];
52
+ export interface Raw_Gen_File {
53
+ content: string;
54
+ // Defaults to file name without the `.gen`, and can be a relative path.
55
+ // TODO maybe support a transform pattern or callback fn? like '[stem].thing.[ext]'
56
+ filename?: string;
57
+ format?: boolean; // defaults to `true`
58
+ }
59
+
60
+ export const Gen_Config = z.object({
61
+ imports: z.record(z.string(), z.string()).default({}),
62
+ });
63
+ export type Gen_Config = z.infer<typeof Gen_Config>;
64
+
65
+ export interface Gen_Results {
66
+ results: Genfile_Module_Result[];
67
+ successes: Genfile_Module_Result_Success[];
68
+ failures: Genfile_Module_Result_Failure[];
69
+ input_count: number;
70
+ output_count: number;
71
+ elapsed: number;
72
+ }
73
+ export type Genfile_Module_Result = Genfile_Module_Result_Success | Genfile_Module_Result_Failure;
74
+ export interface Genfile_Module_Result_Success {
75
+ ok: true;
76
+ id: Path_Id;
77
+ files: Gen_File[];
78
+ elapsed: number;
79
+ }
80
+ export interface Genfile_Module_Result_Failure {
81
+ ok: false;
82
+ id: Path_Id;
83
+ reason: string;
84
+ error: Error;
85
+ elapsed: number;
86
+ }
87
+
88
+ export const to_gen_result = (origin_id: Path_Id, raw_result: Raw_Gen_Result): Gen_Result => {
89
+ return {
90
+ origin_id,
91
+ files: to_gen_files(origin_id, raw_result),
92
+ };
93
+ };
94
+
95
+ const to_gen_files = (origin_id: Path_Id, raw_result: Raw_Gen_Result): Gen_File[] => {
96
+ if (raw_result === null) {
97
+ return [];
98
+ } else if (typeof raw_result === 'string') {
99
+ return [to_gen_file(origin_id, {content: raw_result})];
100
+ } else if (Array.isArray(raw_result)) {
101
+ const files = raw_result.flatMap((f) => to_gen_files(origin_id, f));
102
+ validate_gen_files(files);
103
+ return files;
104
+ }
105
+ return [to_gen_file(origin_id, raw_result)];
106
+ };
107
+
108
+ const to_gen_file = (origin_id: Path_Id, raw_gen_file: Raw_Gen_File): Gen_File => {
109
+ const {content, filename, format = true} = raw_gen_file;
110
+ const id = to_output_file_id(origin_id, filename);
111
+ return {id, content, origin_id, format};
112
+ };
113
+
114
+ const to_output_file_id = (origin_id: Path_Id, raw_file_name: string | undefined): string => {
115
+ if (raw_file_name === '') {
116
+ throw Error(`Output file name cannot be an empty string`);
117
+ }
118
+ const filename = raw_file_name ?? to_output_file_name(basename(origin_id));
119
+ if (isAbsolute(filename)) return filename;
120
+ const dir = dirname(origin_id);
121
+ const output_file_id = join(dir, filename);
122
+ if (output_file_id === origin_id) {
123
+ throw Error('Gen origin and output file ids cannot be the same');
124
+ }
125
+ return output_file_id;
126
+ };
127
+
128
+ export const to_output_file_name = (filename: string): string => {
129
+ const parts = filename.split('.');
130
+ const gen_pattern_index = parts.indexOf(GEN_FILE_PATTERN_TEXT);
131
+ if (gen_pattern_index === -1) {
132
+ throw Error(`Invalid gen file name - '${GEN_FILE_PATTERN_TEXT}' not found in '${filename}'`);
133
+ }
134
+ if (gen_pattern_index !== parts.lastIndexOf(GEN_FILE_PATTERN_TEXT)) {
135
+ throw Error(
136
+ `Invalid gen file name - multiple instances of '${GEN_FILE_PATTERN_TEXT}' found in '${filename}'`,
137
+ );
138
+ }
139
+ if (gen_pattern_index < parts.length - 3) {
140
+ // This check is technically unneccessary,
141
+ // but ensures a consistent file naming convention.
142
+ throw Error(
143
+ `Invalid gen file name - only one additional extension is allowed to follow '${GEN_FILE_PATTERN}' in '${filename}'`,
144
+ );
145
+ }
146
+ const final_parts: string[] = [];
147
+ const has_different_ext = gen_pattern_index === parts.length - 3;
148
+ const length = has_different_ext ? parts.length - 1 : parts.length;
149
+ for (let i = 0; i < length; i++) {
150
+ if (i === gen_pattern_index) continue; // skip the `.gen.` pattern
151
+ if (i === length - 1 && parts[i] === '') continue; // allow empty extension
152
+ final_parts.push(parts[i]);
153
+ }
154
+ return final_parts.join('.');
155
+ };
156
+
157
+ const validate_gen_files = (files: Gen_File[]) => {
158
+ const ids = new Set();
159
+ for (const file of files) {
160
+ if (ids.has(file.id)) {
161
+ throw Error(`Duplicate gen file id: ${file.id}`);
162
+ }
163
+ ids.add(file.id);
164
+ }
165
+ };
166
+
167
+ export type Analyzed_Gen_Result =
168
+ | {
169
+ file: Gen_File;
170
+ existing_content: string;
171
+ is_new: false;
172
+ has_changed: boolean;
173
+ }
174
+ | {
175
+ file: Gen_File;
176
+ existing_content: null;
177
+ is_new: true;
178
+ has_changed: true;
179
+ };
180
+
181
+ export const analyze_gen_results = (gen_results: Gen_Results): Promise<Analyzed_Gen_Result[]> =>
182
+ Promise.all(
183
+ gen_results.successes
184
+ .map((result) => result.files.map((file) => analyze_gen_result(file)))
185
+ .flat(),
186
+ );
187
+
188
+ export const analyze_gen_result = async (file: Gen_File): Promise<Analyzed_Gen_Result> => {
189
+ if (!existsSync(file.id)) {
190
+ return {
191
+ file,
192
+ existing_content: null,
193
+ is_new: true,
194
+ has_changed: true,
195
+ };
196
+ }
197
+ const existing_content = await readFile(file.id, 'utf8');
198
+ return {
199
+ file,
200
+ existing_content,
201
+ is_new: false,
202
+ has_changed: file.content !== existing_content,
203
+ };
204
+ };
205
+
206
+ export const write_gen_results = async (
207
+ gen_results: Gen_Results,
208
+ analyzed_gen_results: Analyzed_Gen_Result[],
209
+ log: Logger,
210
+ ): Promise<void> => {
211
+ await Promise.all(
212
+ gen_results.successes
213
+ .map((result) =>
214
+ result.files.map(async (file) => {
215
+ const analyzed = analyzed_gen_results.find((r) => r.file.id === file.id);
216
+ if (!analyzed) throw Error('Expected to find analyzed result: ' + file.id);
217
+ const log_args = [print_path(file.id), 'generated from', print_path(file.origin_id)];
218
+ if (analyzed.is_new) {
219
+ log.info('writing new', ...log_args);
220
+ await mkdir(dirname(file.id), {recursive: true});
221
+ await writeFile(file.id, file.content);
222
+ } else if (analyzed.has_changed) {
223
+ log.info('writing changed', ...log_args);
224
+ await writeFile(file.id, file.content);
225
+ } else {
226
+ log.info('skipping unchanged', ...log_args);
227
+ }
228
+ }),
229
+ )
230
+ .flat(),
231
+ );
232
+ };
233
+
234
+ export interface Found_Genfiles {
235
+ resolved_input_files: Resolved_Input_File[];
236
+ resolved_input_files_by_root_dir: Map<Path_Id, Resolved_Input_File[]>;
237
+ resolved_input_paths: Resolved_Input_Path[];
238
+ }
239
+
240
+ export type Find_Genfiles_Result = Result<{value: Found_Genfiles}, Find_Genfiles_Failure>;
241
+ export type Find_Genfiles_Failure =
242
+ | {
243
+ type: 'unmapped_input_paths';
244
+ unmapped_input_paths: Input_Path[];
245
+ resolved_input_paths: Resolved_Input_Path[];
246
+ reasons: string[];
247
+ }
248
+ | {
249
+ type: 'input_directories_with_no_files';
250
+ input_directories_with_no_files: Input_Path[];
251
+ resolved_input_files: Resolved_Input_File[];
252
+ resolved_input_files_by_root_dir: Map<Path_Id, Resolved_Input_File[]>;
253
+ resolved_input_paths: Resolved_Input_Path[];
254
+ reasons: string[];
255
+ };
256
+
257
+ /**
258
+ * Finds modules from input paths. (see `src/lib/input_path.ts` for more)
259
+ */
260
+ export const find_genfiles = (
261
+ input_paths: Input_Path[],
262
+ root_dirs: Path_Id[],
263
+ config: Gro_Config,
264
+ timings?: Timings,
265
+ ): Find_Genfiles_Result => {
266
+ const extensions: string[] = [GEN_FILE_PATTERN];
267
+
268
+ // Check which extension variation works - if it's a directory, prefer others first!
269
+ const timing_to_resolve_input_paths = timings?.start('resolve input paths');
270
+ const {resolved_input_paths, unmapped_input_paths} = resolve_input_paths(
271
+ input_paths,
272
+ root_dirs,
273
+ extensions,
274
+ );
275
+ timing_to_resolve_input_paths?.();
276
+
277
+ // Error if any input path could not be mapped.
278
+ if (unmapped_input_paths.length) {
279
+ return {
280
+ ok: false,
281
+ type: 'unmapped_input_paths',
282
+ unmapped_input_paths,
283
+ resolved_input_paths,
284
+ reasons: unmapped_input_paths.map((input_path) =>
285
+ red(`Input path ${print_path(input_path)} cannot be mapped to a file or directory.`),
286
+ ),
287
+ };
288
+ }
289
+
290
+ // Find all of the files for any directories.
291
+ const timing_to_search_fs = timings?.start('find files');
292
+ const {resolved_input_files, resolved_input_files_by_root_dir, input_directories_with_no_files} =
293
+ resolve_input_files(resolved_input_paths, (id) =>
294
+ search_fs(id, {
295
+ filter: config.search_filters,
296
+ file_filter: (p) => extensions.some((e) => p.includes(e)),
297
+ }),
298
+ );
299
+ timing_to_search_fs?.();
300
+
301
+ // Error if any input path has no files. (means we have an empty directory)
302
+ if (input_directories_with_no_files.length) {
303
+ return {
304
+ ok: false,
305
+ type: 'input_directories_with_no_files',
306
+ input_directories_with_no_files,
307
+ resolved_input_files,
308
+ resolved_input_files_by_root_dir,
309
+ resolved_input_paths,
310
+ reasons: input_directories_with_no_files.map((input_path) =>
311
+ red(`Input directory contains no matching files: ${print_path(input_path)}`),
312
+ ),
313
+ };
314
+ }
315
+
316
+ return {
317
+ ok: true,
318
+ value: {
319
+ resolved_input_files,
320
+ resolved_input_files_by_root_dir,
321
+ resolved_input_paths,
322
+ },
323
+ };
324
+ };
325
+
326
+ export interface Genfile_Module {
327
+ gen: Gen;
328
+ }
329
+
330
+ export type Genfile_Module_Meta = Module_Meta<Genfile_Module>;
331
+
332
+ export interface Loaded_Genfiles {
333
+ modules: Genfile_Module_Meta[];
334
+ found_genfiles: Found_Genfiles;
335
+ }
336
+
337
+ export type Load_Genfiles_Result = Result<{value: Loaded_Genfiles}, Load_Genfiles_Failure>;
338
+ export type Load_Genfiles_Failure = Load_Modules_Failure<Genfile_Module_Meta>;
339
+
340
+ export const load_genfiles = async (
341
+ found_genfiles: Found_Genfiles,
342
+ timings?: Timings,
343
+ ): Promise<Load_Genfiles_Result> => {
344
+ const loaded_modules = await load_modules(
345
+ found_genfiles.resolved_input_files,
346
+ validate_gen_module,
347
+ (resolved_input_file, mod): Genfile_Module_Meta => ({id: resolved_input_file.id, mod}),
348
+ timings,
349
+ );
350
+ if (!loaded_modules.ok) {
351
+ return loaded_modules;
352
+ }
353
+ return {
354
+ ok: true,
355
+ value: {modules: loaded_modules.modules, found_genfiles},
356
+ };
357
+ };
358
+
359
+ export const validate_gen_module = (mod: Record<string, any>): mod is Genfile_Module =>
360
+ typeof mod.gen === 'function';
@@ -0,0 +1,34 @@
1
+ import {test} from 'uvu';
2
+ import * as assert from 'uvu/assert';
3
+
4
+ import {
5
+ git_check_clean_workspace,
6
+ git_check_fully_staged_workspace,
7
+ git_current_branch_first_commit_hash,
8
+ git_current_branch_name,
9
+ git_current_commit_hash,
10
+ } from './git.js';
11
+
12
+ test('git_current_branch_name', async () => {
13
+ const branch_name = await git_current_branch_name();
14
+ assert.ok(branch_name);
15
+ });
16
+
17
+ test('git_check_clean_workspace', async () => {
18
+ await git_check_clean_workspace();
19
+ });
20
+
21
+ test('git_check_fully_staged_workspace', async () => {
22
+ await git_check_fully_staged_workspace();
23
+ });
24
+
25
+ test('git_current_commit_hash', async () => {
26
+ await git_current_commit_hash();
27
+ });
28
+
29
+ test('git_current_branch_first_commit_hash', async () => {
30
+ const first_commit_hash = await git_current_branch_first_commit_hash();
31
+ assert.ok(first_commit_hash);
32
+ });
33
+
34
+ test.run();