@ryanatkn/gro 0.129.4 → 0.129.6

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,230 @@
1
+ import {test} from 'uvu';
2
+ import * as assert from 'uvu/assert';
3
+ import {resolve} from 'node:path';
4
+
5
+ import {
6
+ to_input_path,
7
+ to_input_paths,
8
+ resolve_input_files,
9
+ get_possible_paths,
10
+ type Resolved_Input_Path,
11
+ type Resolved_Input_File,
12
+ } from './input_path.js';
13
+ import {GRO_DIST_DIR, paths} from './paths.js';
14
+ import type {Resolved_Path} from './path.js';
15
+
16
+ test('to_input_path', () => {
17
+ assert.is(to_input_path(resolve('foo.ts')), resolve('foo.ts'));
18
+ assert.is(to_input_path('./foo.ts'), resolve('foo.ts'));
19
+ assert.is(to_input_path('foo.ts'), 'foo.ts');
20
+ assert.is(to_input_path('gro/foo'), GRO_DIST_DIR + 'foo');
21
+ // trailing slashes are preserved:
22
+ assert.is(to_input_path(resolve('foo/bar/')), resolve('foo/bar/'));
23
+ assert.is(to_input_path('./foo/bar/'), resolve('foo/bar/'));
24
+ assert.is(to_input_path('foo/bar/'), 'foo/bar/');
25
+ });
26
+
27
+ test('to_input_paths', () => {
28
+ assert.equal(to_input_paths([resolve('foo/bar.ts'), './baz', 'foo']), [
29
+ resolve('foo/bar.ts'),
30
+ resolve('baz'),
31
+ 'foo',
32
+ ]);
33
+ });
34
+
35
+ test('get_possible_paths with an implicit relative path', () => {
36
+ const input_path = 'src/foo/bar';
37
+ assert.equal(
38
+ get_possible_paths(
39
+ input_path,
40
+ [resolve('src/foo'), resolve('src/baz'), resolve('src'), resolve('.')],
41
+ ['.ext.ts'],
42
+ ),
43
+ [
44
+ {
45
+ id: resolve('src/foo/src/foo/bar'),
46
+ input_path: 'src/foo/bar',
47
+ root_dir: resolve('src/foo'),
48
+ },
49
+ {
50
+ id: resolve('src/foo/src/foo/bar.ext.ts'),
51
+ input_path: 'src/foo/bar',
52
+ root_dir: resolve('src/foo'),
53
+ },
54
+ {
55
+ id: resolve('src/baz/src/foo/bar'),
56
+ input_path: 'src/foo/bar',
57
+ root_dir: resolve('src/baz'),
58
+ },
59
+ {
60
+ id: resolve('src/baz/src/foo/bar.ext.ts'),
61
+ input_path: 'src/foo/bar',
62
+ root_dir: resolve('src/baz'),
63
+ },
64
+ {
65
+ id: resolve('src/src/foo/bar'),
66
+ input_path: 'src/foo/bar',
67
+ root_dir: resolve('src'),
68
+ },
69
+ {
70
+ id: resolve('src/src/foo/bar.ext.ts'),
71
+ input_path: 'src/foo/bar',
72
+ root_dir: resolve('src'),
73
+ },
74
+ {
75
+ id: resolve('src/foo/bar'),
76
+ input_path: 'src/foo/bar',
77
+ root_dir: resolve('.'),
78
+ },
79
+ {
80
+ id: resolve('src/foo/bar.ext.ts'),
81
+ input_path: 'src/foo/bar',
82
+ root_dir: resolve('.'),
83
+ },
84
+ ],
85
+ );
86
+ });
87
+
88
+ test('get_possible_paths in the gro directory', () => {
89
+ const input_path = resolve('src/foo/bar');
90
+ assert.equal(get_possible_paths(input_path, [], ['.ext.ts']), [
91
+ {id: input_path, input_path: resolve('src/foo/bar'), root_dir: resolve('src/foo')},
92
+ {id: input_path + '.ext.ts', input_path: resolve('src/foo/bar'), root_dir: resolve('src/foo')},
93
+ ]);
94
+ });
95
+
96
+ test('get_possible_paths does not repeat the extension', () => {
97
+ const input_path = resolve('src/foo/bar.ext.ts');
98
+ assert.equal(get_possible_paths(input_path, [], ['.ext.ts']), [
99
+ {id: input_path, input_path: resolve('src/foo/bar.ext.ts'), root_dir: resolve('src/foo')},
100
+ ]);
101
+ });
102
+
103
+ test('get_possible_paths does not repeat with the same root directory', () => {
104
+ const input_path = resolve('src/foo/bar.ext.ts');
105
+ assert.equal(get_possible_paths(input_path, [paths.root, paths.root], ['.ext.ts']), [
106
+ {id: input_path, input_path: resolve('src/foo/bar.ext.ts'), root_dir: resolve('src/foo')},
107
+ ]);
108
+ });
109
+
110
+ test('get_possible_paths implied to be a directory by trailing slash', () => {
111
+ const input_path = resolve('src/foo/bar') + '/';
112
+ assert.equal(get_possible_paths(input_path, [], ['.ext.ts']), [
113
+ {id: input_path, input_path: resolve('src/foo/bar') + '/', root_dir: resolve('src/foo')},
114
+ ]);
115
+ });
116
+
117
+ test('resolve_input_files', () => {
118
+ const test_files: Record<string, Resolved_Path[]> = {
119
+ 'fake/test1.ext.ts': [
120
+ {id: 'fake/test1.ext.ts', path: 'fake/test1.ext.ts', is_directory: false},
121
+ ],
122
+ 'fake/test2.ext.ts': [
123
+ {id: 'fake/test2.ext.ts', path: 'fake/test2.ext.ts', is_directory: false},
124
+ ],
125
+ 'fake/test3': [
126
+ {id: 'fake/test3', path: 'fake/test3', is_directory: true},
127
+ {id: 'a.ts', path: 'a.ts', is_directory: false},
128
+ {id: 'b.ts', path: 'b.ts', is_directory: false},
129
+ ],
130
+ // duplicate
131
+ 'fake/': [
132
+ {id: 'fake/test3', path: 'fake/test3', is_directory: true},
133
+ {id: 'test3/a.ts', path: 'test3/a.ts', is_directory: false},
134
+ ],
135
+ // duplicate and not
136
+ fake: [
137
+ {id: 'fake/test3', path: 'fake/test3', is_directory: true},
138
+ {id: 'test3/a.ts', path: 'test3/a.ts', is_directory: false},
139
+ {id: 'test3/c.ts', path: 'test3/c.ts', is_directory: false},
140
+ ],
141
+ 'fake/nomatches': [{id: 'fake/nomatches', path: 'fake/nomatches', is_directory: true}],
142
+ fake2: [{id: 'test.ext.ts', path: 'test.ext.ts', is_directory: false}],
143
+ };
144
+ const a: Resolved_Input_Path = {
145
+ id: 'fake/test1.ext.ts',
146
+ is_directory: false,
147
+ input_path: 'fake/test1.ext.ts',
148
+ root_dir: process.cwd(),
149
+ };
150
+ const b: Resolved_Input_Path = {
151
+ id: 'fake/test2.ext.ts',
152
+ is_directory: false,
153
+ input_path: 'fake/test2',
154
+ root_dir: process.cwd(),
155
+ };
156
+ const c: Resolved_Input_Path = {
157
+ id: 'fake/test3',
158
+ is_directory: true,
159
+ input_path: 'fake/test3',
160
+ root_dir: process.cwd(),
161
+ };
162
+ const d: Resolved_Input_Path = {
163
+ id: 'fake',
164
+ is_directory: true,
165
+ input_path: 'fake',
166
+ root_dir: process.cwd(),
167
+ };
168
+ const e: Resolved_Input_Path = {
169
+ id: 'fake/nomatches',
170
+ is_directory: true,
171
+ input_path: 'fake/nomatches',
172
+ root_dir: process.cwd(),
173
+ };
174
+ // These two have the same id from different directory input paths.
175
+ const f: Resolved_Input_Path = {
176
+ id: 'fake2',
177
+ is_directory: true,
178
+ input_path: 'fake2',
179
+ root_dir: process.cwd(),
180
+ };
181
+ const g: Resolved_Input_Path = {
182
+ id: 'fake2',
183
+ is_directory: true,
184
+ input_path: './fake2/',
185
+ root_dir: process.cwd(),
186
+ };
187
+ // These two have the same id from different file input paths.
188
+ const h: Resolved_Input_Path = {
189
+ id: 'fake3/test.ext.ts',
190
+ is_directory: false,
191
+ input_path: 'fake3/test.ext.ts',
192
+ root_dir: process.cwd(),
193
+ };
194
+ const i: Resolved_Input_Path = {
195
+ id: 'fake3/test.ext.ts',
196
+ is_directory: false,
197
+ input_path: 'fake3/test',
198
+ root_dir: process.cwd(),
199
+ };
200
+ const result = resolve_input_files([a, b, c, d, e, f, g, h, i], (dir) => test_files[dir]);
201
+ const resolved_input_files: Resolved_Input_File[] = [
202
+ {id: a.id, input_path: a.input_path, resolved_input_path: a},
203
+ {id: b.id, input_path: b.input_path, resolved_input_path: b},
204
+ {id: 'fake/test3/a.ts', input_path: c.input_path, resolved_input_path: c},
205
+ {id: 'fake/test3/b.ts', input_path: c.input_path, resolved_input_path: c},
206
+ {id: 'fake/test3/c.ts', input_path: d.input_path, resolved_input_path: d},
207
+ {id: 'fake2/test.ext.ts', input_path: f.input_path, resolved_input_path: f},
208
+ {id: 'fake3/test.ext.ts', input_path: h.input_path, resolved_input_path: h},
209
+ ];
210
+ assert.equal(result, {
211
+ resolved_input_files,
212
+ resolved_input_files_by_root_dir: new Map([
213
+ [
214
+ process.cwd(),
215
+ [
216
+ {id: 'fake/test1.ext.ts', input_path: a.input_path, resolved_input_path: a},
217
+ {id: 'fake/test2.ext.ts', input_path: b.input_path, resolved_input_path: b},
218
+ {id: 'fake/test3/a.ts', input_path: c.input_path, resolved_input_path: c},
219
+ {id: 'fake/test3/b.ts', input_path: c.input_path, resolved_input_path: c},
220
+ {id: 'fake/test3/c.ts', input_path: d.input_path, resolved_input_path: d},
221
+ {id: 'fake2/test.ext.ts', input_path: f.input_path, resolved_input_path: f},
222
+ {id: 'fake3/test.ext.ts', input_path: h.input_path, resolved_input_path: h},
223
+ ],
224
+ ],
225
+ ]),
226
+ input_directories_with_no_files: [e.input_path],
227
+ });
228
+ });
229
+
230
+ test.run();
@@ -0,0 +1,255 @@
1
+ import {dirname, isAbsolute, join, resolve} from 'node:path';
2
+ import {existsSync, statSync} from 'node:fs';
3
+ import {strip_start} from '@ryanatkn/belt/string.js';
4
+ import {z} from 'zod';
5
+ import type {Flavored} from '@ryanatkn/belt/types.js';
6
+
7
+ import {GRO_PACKAGE_DIR, GRO_DIST_DIR} from './paths.js';
8
+ import type {Path_Info, Path_Id, Resolved_Path} from './path.js';
9
+ import {search_fs} from './search_fs.js';
10
+ import {TASK_FILE_SUFFIX_JS} from './task.js';
11
+
12
+ // TODO Flavored doesn't work when used in schemas, use Zod brand instead? problem is ergonomics
13
+ export const Input_Path = z.string();
14
+ export type Input_Path = Flavored<z.infer<typeof Input_Path>, 'Input_Path'>;
15
+
16
+ export const Raw_Input_Path = z.string();
17
+ export type Raw_Input_Path = Flavored<z.infer<typeof Raw_Input_Path>, 'Raw_Input_Path'>;
18
+
19
+ /**
20
+ * Raw input paths are paths that users provide to Gro to reference files for tasks and gen.
21
+ *
22
+ * A raw input path can be to a file or directory in the following forms:
23
+ *
24
+ * - an absolute path, preserved
25
+ * - an explicit relative path, e.g. `./src/foo`, resolved to `root_path` which defaults to the cwd
26
+ * - an implicit relative path, e.g. `src/foo`, preserved
27
+ * - an implicit relative path prefixed with `gro/`, transformed to absolute in the Gro directory
28
+ *
29
+ * Thus, input paths are either absolute or implicitly relative.
30
+ */
31
+ export const to_input_path = (
32
+ raw_input_path: Raw_Input_Path,
33
+ root_path = process.cwd(), // TODO @multiple isn't passed in anywhere, maybe hoist to `invoke_task` and others
34
+ ): Input_Path => {
35
+ if (raw_input_path.startsWith(GRO_PACKAGE_DIR)) {
36
+ return GRO_DIST_DIR + strip_start(raw_input_path, GRO_PACKAGE_DIR);
37
+ } else if (raw_input_path[0] === '.') {
38
+ return resolve(root_path, raw_input_path);
39
+ }
40
+ return raw_input_path as Input_Path;
41
+ };
42
+
43
+ export const to_input_paths = (
44
+ raw_input_paths: Raw_Input_Path[],
45
+ root_path?: string, // TODO @multiple isn't passed in anywhere, maybe hoist to `invoke_task` and others
46
+ ): Input_Path[] => raw_input_paths.map((p) => to_input_path(p, root_path));
47
+
48
+ export interface Possible_Path {
49
+ id: Path_Id;
50
+ input_path: Input_Path;
51
+ root_dir: Path_Id;
52
+ }
53
+
54
+ /**
55
+ * Gets a list of possible source ids for each input path with `extensions`,
56
+ * duplicating each under `root_dirs`, without checking the filesystem.
57
+ */
58
+ export const get_possible_paths = (
59
+ input_path: Input_Path,
60
+ root_dirs: Path_Id[],
61
+ extensions: string[],
62
+ ): Possible_Path[] => {
63
+ const possible_paths: Set<Possible_Path> = new Set();
64
+
65
+ const add_possible_paths = (path: string, root_dir: Path_Id) => {
66
+ // Specifically for paths to the Gro package dist, optimize by only looking for `.task.js`.
67
+ if (path.startsWith(GRO_DIST_DIR)) {
68
+ possible_paths.add({
69
+ id: (path.endsWith('/') || path.endsWith(TASK_FILE_SUFFIX_JS)
70
+ ? path
71
+ : path + TASK_FILE_SUFFIX_JS) as Path_Id,
72
+ input_path,
73
+ root_dir,
74
+ });
75
+ } else {
76
+ possible_paths.add({id: path as Path_Id, input_path, root_dir});
77
+ if (!path.endsWith('/') && !extensions.some((e) => path.endsWith(e))) {
78
+ for (const extension of extensions) {
79
+ possible_paths.add({id: path + extension, input_path, root_dir});
80
+ }
81
+ }
82
+ }
83
+ };
84
+
85
+ if (isAbsolute(input_path)) {
86
+ // TODO this is hacky because it's the only place we're using sync fs calls (even if they're faster, it's oddly inconsistent),
87
+ // we probably should just change this function to check the filesystem and not return non-existing paths
88
+ add_possible_paths(
89
+ input_path,
90
+ existsSync(input_path) && statSync(input_path).isDirectory()
91
+ ? input_path
92
+ : dirname(input_path),
93
+ );
94
+ } else {
95
+ for (const root_dir of root_dirs) {
96
+ add_possible_paths(join(root_dir, input_path), root_dir);
97
+ }
98
+ }
99
+ return Array.from(possible_paths);
100
+ };
101
+
102
+ export interface Resolved_Input_Path {
103
+ input_path: Input_Path;
104
+ id: Path_Id;
105
+ is_directory: boolean;
106
+ root_dir: Path_Id;
107
+ }
108
+
109
+ export interface Resolved_Input_File {
110
+ id: Path_Id;
111
+ input_path: Input_Path;
112
+ resolved_input_path: Resolved_Input_Path;
113
+ }
114
+
115
+ export interface Resolved_Input_Paths {
116
+ resolved_input_paths: Resolved_Input_Path[];
117
+ possible_paths_by_input_path: Map<Input_Path, Possible_Path[]>;
118
+ unmapped_input_paths: Input_Path[];
119
+ }
120
+
121
+ /**
122
+ * Gets the path data for each input path, checking the filesystem for the possibilities
123
+ * and stopping at the first existing file or falling back to the first existing directory.
124
+ * If none is found for an input path, it's added to `unmapped_input_paths`.
125
+ */
126
+ export const resolve_input_paths = (
127
+ input_paths: Input_Path[],
128
+ root_dirs: Path_Id[],
129
+ extensions: string[],
130
+ ): Resolved_Input_Paths => {
131
+ const resolved_input_paths: Resolved_Input_Path[] = [];
132
+ const possible_paths_by_input_path: Map<Input_Path, Possible_Path[]> = new Map();
133
+ const unmapped_input_paths: Input_Path[] = [];
134
+ for (const input_path of input_paths) {
135
+ let found_file: [Path_Info, Possible_Path] | null = null;
136
+ let found_dirs: Array<[Path_Info, Possible_Path]> | null = null;
137
+ const possible_paths = get_possible_paths(input_path, root_dirs, extensions);
138
+ possible_paths_by_input_path.set(input_path, possible_paths);
139
+
140
+ // Find the first existing file path or fallback to the first directory path.
141
+ for (const possible_path of possible_paths) {
142
+ if (!existsSync(possible_path.id)) continue;
143
+ const stats = statSync(possible_path.id);
144
+ if (stats.isDirectory()) {
145
+ found_dirs ??= [];
146
+ found_dirs.push([{id: possible_path.id, is_directory: stats.isDirectory()}, possible_path]);
147
+ } else {
148
+ found_file = [{id: possible_path.id, is_directory: stats.isDirectory()}, possible_path];
149
+ break;
150
+ }
151
+ }
152
+ if (found_file) {
153
+ resolved_input_paths.push({
154
+ input_path,
155
+ id: found_file[0].id,
156
+ is_directory: found_file[0].is_directory,
157
+ root_dir: found_file[1].root_dir,
158
+ });
159
+ } else if (found_dirs) {
160
+ for (const found_dir of found_dirs) {
161
+ resolved_input_paths.push({
162
+ input_path,
163
+ id: found_dir[0].id,
164
+ is_directory: found_dir[0].is_directory,
165
+ root_dir: found_dir[1].root_dir,
166
+ });
167
+ }
168
+ } else {
169
+ unmapped_input_paths.push(input_path);
170
+ }
171
+ }
172
+ return {
173
+ resolved_input_paths,
174
+ possible_paths_by_input_path,
175
+ unmapped_input_paths,
176
+ };
177
+ };
178
+
179
+ export interface Resolved_Input_Files {
180
+ resolved_input_files: Resolved_Input_File[];
181
+ resolved_input_files_by_root_dir: Map<Path_Id, Resolved_Input_File[]>;
182
+ input_directories_with_no_files: Input_Path[];
183
+ }
184
+
185
+ /**
186
+ * Finds all of the matching files for the given input paths.
187
+ * De-dupes source ids.
188
+ */
189
+ export const resolve_input_files = (
190
+ resolved_input_paths: Resolved_Input_Path[],
191
+ search: (dir: string) => Resolved_Path[] = search_fs,
192
+ ): Resolved_Input_Files => {
193
+ const resolved_input_files: Resolved_Input_File[] = [];
194
+ // Add all input paths initially, and remove each when resolved to a file.
195
+ const existing_path_ids: Set<Path_Id> = new Set();
196
+
197
+ let remaining = resolved_input_paths.slice();
198
+ const handle_found = (input_path: Input_Path, id: Path_Id) => {
199
+ remaining = remaining.filter(
200
+ (r) => !(r.id === id || r.input_path === input_path || r.input_path === id), // `r.input_path === id` may be unnecessary
201
+ );
202
+ };
203
+
204
+ // TODO parallelize but would need to de-dupe and retain order
205
+ for (const resolved_input_path of resolved_input_paths) {
206
+ const {input_path, id, is_directory} = resolved_input_path;
207
+ if (is_directory) {
208
+ // Handle input paths that resolve to directories.
209
+ const files = search(id);
210
+ if (!files.length) continue;
211
+ const path_ids: Path_Id[] = [];
212
+ for (const {path, is_directory} of files) {
213
+ if (is_directory) continue;
214
+ const path_id = join(id, path);
215
+ if (!existing_path_ids.has(path_id)) {
216
+ existing_path_ids.add(path_id);
217
+ path_ids.push(path_id);
218
+ }
219
+ handle_found(input_path, path_id);
220
+ }
221
+ if (!path_ids.length) continue;
222
+ const resolved_input_files_for_input_path: Resolved_Input_File[] = [];
223
+ for (const path_id of path_ids) {
224
+ const resolved_input_file: Resolved_Input_File = {
225
+ id: path_id,
226
+ input_path,
227
+ resolved_input_path,
228
+ };
229
+ resolved_input_files.push(resolved_input_file);
230
+ resolved_input_files_for_input_path.push(resolved_input_file);
231
+ }
232
+ } else {
233
+ if (!existing_path_ids.has(id)) {
234
+ // Handle input paths that resolve to files.
235
+ existing_path_ids.add(id);
236
+ const resolved_input_file: Resolved_Input_File = {id, input_path, resolved_input_path};
237
+ resolved_input_files.push(resolved_input_file);
238
+ }
239
+ handle_found(input_path, id);
240
+ }
241
+ }
242
+ return {
243
+ resolved_input_files,
244
+ resolved_input_files_by_root_dir: resolved_input_files.reduce((map, resolved_input_file) => {
245
+ const {root_dir} = resolved_input_file.resolved_input_path;
246
+ if (map.has(root_dir)) {
247
+ map.get(root_dir)!.push(resolved_input_file);
248
+ } else {
249
+ map.set(root_dir, [resolved_input_file]);
250
+ }
251
+ return map;
252
+ }, new Map<Path_Id, Resolved_Input_File[]>()),
253
+ input_directories_with_no_files: remaining.map((r) => r.input_path),
254
+ };
255
+ };
@@ -0,0 +1,27 @@
1
+ import {attach_process_error_handlers} from '@ryanatkn/belt/process.js';
2
+
3
+ import {invoke_task} from './invoke_task.js';
4
+ import {to_task_args} from './args.js';
5
+ import {load_config} from './config.js';
6
+ import {sveltekit_sync_if_obviously_needed} from './sveltekit_helpers.js';
7
+
8
+ /*
9
+
10
+ This module invokes the Gro CLI which in turn invokes tasks.
11
+ Tasks are the CLI's primary concept.
12
+ To learn more about them, see `src/lib/docs/task.md`.
13
+
14
+ When the CLI is invoked it passes the first CLI arg as `task_name` to `invoke_task`,
15
+ and the rest of the args are forwarded to the task's `run` function.
16
+
17
+ */
18
+
19
+ // handle uncaught errors
20
+ attach_process_error_handlers((err) =>
21
+ err?.constructor?.name === 'Task_Error' ? 'Task_Error' : null,
22
+ );
23
+
24
+ await sveltekit_sync_if_obviously_needed();
25
+
26
+ const {task_name, args} = to_task_args();
27
+ await invoke_task(task_name, args, await load_config());
@@ -0,0 +1,116 @@
1
+ import {cyan, red, gray} from '@ryanatkn/belt/styletext.js';
2
+ import {System_Logger, print_log_label} from '@ryanatkn/belt/log.js';
3
+ import {create_stopwatch, Timings} from '@ryanatkn/belt/timings.js';
4
+ import {print_ms, print_timings} from '@ryanatkn/belt/print.js';
5
+
6
+ import {to_forwarded_args, type Args} from './args.js';
7
+ import {run_task} from './run_task.js';
8
+ import {to_input_path, Raw_Input_Path} from './input_path.js';
9
+ import {find_tasks, load_tasks} from './task.js';
10
+ import {load_gro_package_json} from './package_json.js';
11
+ import {log_tasks, log_error_reasons} from './task_logging.js';
12
+ import type {Gro_Config} from './config.js';
13
+
14
+ /**
15
+ * Invokes Gro tasks by name using the filesystem as the source.
16
+ *
17
+ * When a task is invoked,
18
+ * Gro first searches for tasks in the current working directory.
19
+ * and falls back to searching Gro's directory, if the two are different.
20
+ * See `src/lib/input_path.ts` for info about what "task_name" can refer to.
21
+ * If it matches a directory, all of the tasks within it are logged,
22
+ * both in the current working directory and Gro.
23
+ *
24
+ * This code is particularly hairy because
25
+ * we're accepting a wide range of user input
26
+ * and trying to do the right thing.
27
+ * Precise error messages are especially difficult and
28
+ * there are some subtle differences in the complex logical branches.
29
+ * The comments describe each condition.
30
+ *
31
+ * @param task_name - The name of the task to invoke.
32
+ * @param args - The CLI args to pass to the task.
33
+ * @param config - The Gro configuration.
34
+ * @param initial_timings - The timings to use for the top-level task, `null` for composed tasks.
35
+ */
36
+ export const invoke_task = async (
37
+ task_name: Raw_Input_Path,
38
+ args: Args | undefined,
39
+ config: Gro_Config,
40
+ initial_timings: Timings | null = null,
41
+ ): Promise<void> => {
42
+ const log = new System_Logger(print_log_label(task_name || 'gro'));
43
+ log.info('invoking', task_name ? cyan(task_name) : 'gro');
44
+
45
+ const timings = initial_timings ?? new Timings();
46
+
47
+ const total_timing = create_stopwatch();
48
+ const finish = () => {
49
+ if (!initial_timings) return; // print timings only for the top-level task
50
+ print_timings(timings, log);
51
+ log.info(`🕒 ${print_ms(total_timing())}`);
52
+ };
53
+
54
+ // Check if the caller just wants to see the version.
55
+ if (!task_name && (args?.version || args?.v)) {
56
+ const gro_package_json = load_gro_package_json();
57
+ log.info(`${gray('v')}${cyan(gro_package_json.version)}`);
58
+ finish();
59
+ return;
60
+ }
61
+
62
+ // Resolve the input path for the provided task name.
63
+ const input_path = to_input_path(task_name);
64
+
65
+ const {task_root_dirs} = config;
66
+
67
+ // Find the task or directory specified by the `input_path`.
68
+ // Fall back to searching the Gro directory as well.
69
+ const found = find_tasks([input_path], task_root_dirs, config);
70
+ if (!found.ok) {
71
+ log_error_reasons(log, found.reasons);
72
+ process.exit(1);
73
+ }
74
+
75
+ // Found a match either in the current working directory or Gro's directory.
76
+ const found_tasks = found.value;
77
+ const {resolved_input_files} = found_tasks;
78
+
79
+ // Load the task module.
80
+ const loaded = await load_tasks(found_tasks);
81
+ if (!loaded.ok) {
82
+ log_error_reasons(log, loaded.reasons);
83
+ process.exit(1);
84
+ }
85
+ const loaded_tasks = loaded.value;
86
+
87
+ if (resolved_input_files.length > 1 || resolved_input_files[0].resolved_input_path.is_directory) {
88
+ // The input path matches a directory. Log the tasks but don't run them.
89
+ log_tasks(log, loaded_tasks);
90
+ finish();
91
+ return;
92
+ }
93
+
94
+ // The input path matches a file that's presumable a task, so load and run it.
95
+ if (loaded_tasks.modules.length !== 1) throw Error('expected one loaded task'); // run only one task at a time
96
+ const task = loaded_tasks.modules[0];
97
+ log.info(`→ ${cyan(task.name)} ${(task.mod.task.summary && gray(task.mod.task.summary)) ?? ''}`);
98
+
99
+ const timing_to_run_task = timings.start('run task ' + task_name);
100
+ const result = await run_task(
101
+ task,
102
+ {...args, ...to_forwarded_args(`gro ${task.name}`)},
103
+ invoke_task,
104
+ config,
105
+ timings,
106
+ );
107
+ timing_to_run_task();
108
+ if (!result.ok) {
109
+ log.info(`${red('🞩')} ${cyan(task.name)}`);
110
+ log_error_reasons(log, [result.reason]);
111
+ throw result.error;
112
+ }
113
+ log.info(`✓ ${cyan(task.name)}`);
114
+
115
+ finish();
116
+ };
@@ -0,0 +1,38 @@
1
+ import {print_spawn_result} from '@ryanatkn/belt/process.js';
2
+ import {z} from 'zod';
3
+
4
+ import {Task_Error, type Task} from './task.js';
5
+ import {serialize_args, to_forwarded_args} from './args.js';
6
+ import {find_cli, spawn_cli} from './cli.js';
7
+
8
+ const ESLINT_CLI = 'eslint';
9
+
10
+ export const Args = z
11
+ .object({
12
+ _: z.array(z.string(), {description: 'paths to serve'}).default([]),
13
+ eslint_cli: z.string({description: 'the ESLint CLI to use'}).default(ESLINT_CLI),
14
+ })
15
+ .strict();
16
+ export type Args = z.infer<typeof Args>;
17
+
18
+ export const task: Task<Args> = {
19
+ summary: 'run eslint',
20
+ Args,
21
+ run: async ({log, args}): Promise<void> => {
22
+ const {_, eslint_cli} = args;
23
+
24
+ const found_eslint_cli = find_cli(eslint_cli);
25
+ if (!found_eslint_cli) {
26
+ // TODO maybe make this an option?
27
+ log.info('ESLint is not installed; skipping linting');
28
+ return;
29
+ }
30
+
31
+ const forwarded_args = {_, 'max-warnings': 0, ...to_forwarded_args(eslint_cli)};
32
+ const serialized_args = serialize_args(forwarded_args);
33
+ const eslintResult = await spawn_cli(found_eslint_cli, serialized_args, log);
34
+ if (!eslintResult?.ok) {
35
+ throw new Task_Error(`ESLint found some problems. ${print_spawn_result(eslintResult!)}`);
36
+ }
37
+ },
38
+ };