@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,45 @@
1
+ import {load_env} from './env.js';
2
+
3
+ // TODO might want to do more escaping and validation
4
+
5
+ /**
6
+ * Generates a module shim for SvelteKit's `$env` imports.
7
+ */
8
+ export const render_env_shim_module = (
9
+ dev: boolean,
10
+ mode: 'static' | 'dynamic',
11
+ visibility: 'public' | 'private',
12
+ public_prefix = 'PUBLIC_',
13
+ private_prefix = '',
14
+ env_dir?: string,
15
+ env_files?: string[],
16
+ ambient_env?: Record<string, string | undefined>,
17
+ ): string => {
18
+ const env = load_env(
19
+ dev,
20
+ visibility,
21
+ public_prefix,
22
+ private_prefix,
23
+ env_dir,
24
+ env_files,
25
+ ambient_env,
26
+ );
27
+ if (mode === 'static') {
28
+ return `// shim for $env/static/${visibility}
29
+ // @see https://github.com/sveltejs/kit/issues/1485
30
+ ${Object.entries(env)
31
+ .map(([k, v]) => `export let ${k} = ${JSON.stringify(v)};`)
32
+ .join('\n')}
33
+ `;
34
+ } else {
35
+ return `// shim for $env/dynamic/${visibility}
36
+ // @see https://github.com/sveltejs/kit/issues/1485
37
+ import {load_env} from '@ryanatkn/gro/env.js';
38
+ export const env = load_env(${dev}, ${JSON.stringify(visibility)}, ${JSON.stringify(
39
+ public_prefix,
40
+ )}, ${JSON.stringify(private_prefix)}, ${JSON.stringify(env_dir)}, ${JSON.stringify(
41
+ env_files,
42
+ )}, ${JSON.stringify(ambient_env)});
43
+ `;
44
+ }
45
+ };
@@ -0,0 +1,47 @@
1
+ import {z} from 'zod';
2
+ import {spawn} from '@ryanatkn/belt/process.js';
3
+
4
+ import {Task_Error, type Task} from './task.js';
5
+ import {sync_package_json} from './package_json.js';
6
+ import {sveltekit_sync} from './sveltekit_helpers.js';
7
+
8
+ export const Args = z
9
+ .object({
10
+ sveltekit: z.boolean({description: 'dual of no-sveltekit'}).default(true),
11
+ 'no-sveltekit': z.boolean({description: 'opt out of svelte-kit sync'}).default(false),
12
+ package_json: z.boolean({description: 'dual of no-package_json'}).default(true),
13
+ 'no-package_json': z.boolean({description: 'opt out of package.json sync'}).default(false),
14
+ gen: z.boolean({description: 'dual of no-gen'}).default(true),
15
+ 'no-gen': z.boolean({description: 'opt out of gen sync'}).default(false),
16
+ install: z.boolean({description: 'run npm install'}).default(false),
17
+ })
18
+ .strict();
19
+ export type Args = z.infer<typeof Args>;
20
+
21
+ export const task: Task<Args> = {
22
+ summary: 'run `gro gen`, update `package.json`, and optionally `npm i` to sync up',
23
+ Args,
24
+ run: async ({args, invoke_task, config, log}): Promise<void> => {
25
+ const {sveltekit, package_json, gen, install} = args;
26
+
27
+ if (install) {
28
+ const result = await spawn('npm', ['i']);
29
+ if (!result.ok) {
30
+ throw new Task_Error('Failed npm install');
31
+ }
32
+ }
33
+
34
+ if (sveltekit) {
35
+ await sveltekit_sync();
36
+ log.info('synced SvelteKit');
37
+ }
38
+
39
+ if (package_json && config.map_package_json) {
40
+ await sync_package_json(config.map_package_json, log);
41
+ }
42
+
43
+ if (gen) {
44
+ await invoke_task('gen');
45
+ }
46
+ },
47
+ };
@@ -0,0 +1,84 @@
1
+ import {test} from 'uvu';
2
+ import * as assert from 'uvu/assert';
3
+ import {resolve} from 'node:path';
4
+
5
+ import {is_task_path, to_task_name, validate_task_module, find_tasks, load_tasks} from './task.js';
6
+ import * as actual_test_task_module from './test.task.js';
7
+ import {create_empty_config} from './config.js';
8
+
9
+ test('is_task_path basic behavior', () => {
10
+ assert.ok(is_task_path('foo.task.ts'));
11
+ assert.ok(is_task_path('foo.task.js'));
12
+ assert.ok(!is_task_path('foo.ts'));
13
+ assert.ok(is_task_path('bar/baz/foo.task.ts'));
14
+ assert.ok(!is_task_path('bar/baz/foo.ts'));
15
+ });
16
+
17
+ test('to_task_name basic behavior', () => {
18
+ assert.is(to_task_name('foo.task.ts', process.cwd(), '', ''), 'foo');
19
+ assert.is(to_task_name('bar/baz/foo.task.ts', process.cwd(), '', ''), 'bar/baz/foo');
20
+ assert.is(to_task_name('a/b/c/foo.task.ts', 'a/b/c', '', ''), 'foo');
21
+ assert.is(to_task_name('a/b/c/foo.task.ts', 'a', '', ''), 'b/c/foo');
22
+ assert.is(to_task_name('a/b/c/foo.task.ts', 'a/b', '', ''), 'c/foo');
23
+ assert.is(to_task_name('/a/b/c/foo.task.ts', '/a/b', '/a/b', '/a/b/d'), '../c/foo');
24
+ assert.is(to_task_name('/a/b/c/foo.task.ts', '/a/b', '/a/b', '/a/b'), 'c/foo');
25
+ assert.is(to_task_name('/a/b/c/foo.task.ts', '/a/b', '/a/b', '/a/b/c'), 'foo');
26
+ assert.is(to_task_name('/a/b/d/foo.task.js', '/a/b/d', '/a/b/d/foo', '/a/c'), '../b/d/foo');
27
+ assert.is(
28
+ to_task_name(
29
+ '/a/node_modules/b/c/foo.task.js',
30
+ '/a/node_modules/b/c',
31
+ '/a/node_modules/b/c/foo',
32
+ '/a',
33
+ ),
34
+ 'foo',
35
+ );
36
+ assert.is(
37
+ to_task_name(
38
+ '/a/node_modules/b/c/foo.task.js',
39
+ '/a/node_modules/b/c/', // compared to the above, adds a trailing slash here
40
+ '/a/node_modules/b/c/foo',
41
+ '/a',
42
+ ),
43
+ 'foo',
44
+ );
45
+ assert.is(
46
+ to_task_name(
47
+ '/a/node_modules/b/c/foo.task.js',
48
+ '/a/node_modules/b/c',
49
+ '/a/node_modules/b/c/foo',
50
+ '/a/', // compared to the above, adds a trailing slash here
51
+ ),
52
+ 'foo',
53
+ );
54
+ assert.is(
55
+ to_task_name(resolve('a/b'), resolve('b'), '', ''),
56
+ resolve('a/b'),
57
+ 'falls back to the id when unresolved',
58
+ );
59
+ });
60
+
61
+ // TODO if we import directly, svelte-package generates types in `src/fixtures`
62
+ const test_task_module = await import('../fixtures/' + 'test_task_module.task_fixture'); // eslint-disable-line no-useless-concat
63
+ const test_invalid_task_module = await import('../fixtures/' + 'test_invalid_task_module.js'); // eslint-disable-line no-useless-concat
64
+
65
+ test('validate_task_module basic behavior', () => {
66
+ assert.ok(validate_task_module(test_task_module));
67
+ assert.ok(!validate_task_module(test_invalid_task_module));
68
+ assert.ok(!validate_task_module({task: {run: {}}}));
69
+ });
70
+
71
+ test('load_tasks basic behavior', async () => {
72
+ const found = find_tasks(
73
+ [resolve('src/lib/test'), resolve('src/lib/test.task.ts')],
74
+ [resolve('src/lib')],
75
+ create_empty_config(),
76
+ );
77
+ assert.ok(found.ok);
78
+ const result = await load_tasks(found.value);
79
+ assert.ok(result.ok);
80
+ assert.is(result.value.modules.length, 1);
81
+ assert.is(result.value.modules[0].mod, actual_test_task_module);
82
+ });
83
+
84
+ test.run();
@@ -0,0 +1,235 @@
1
+ import type {Logger} from '@ryanatkn/belt/log.js';
2
+ import {strip_end, strip_start} from '@ryanatkn/belt/string.js';
3
+ import type {z} from 'zod';
4
+ import type {Timings} from '@ryanatkn/belt/timings.js';
5
+ import {red} from '@ryanatkn/belt/styletext.js';
6
+ import type {Result} from '@ryanatkn/belt/result.js';
7
+ import {isAbsolute, join, relative} from 'node:path';
8
+
9
+ import type {Args} from './args.js';
10
+ import type {Path_Id} from './path.js';
11
+ import type {Gro_Config} from './config.js';
12
+ import type {Parsed_Sveltekit_Config} from './sveltekit_config.js';
13
+ import {
14
+ resolve_input_files,
15
+ resolve_input_paths,
16
+ type Input_Path,
17
+ type Resolved_Input_File,
18
+ type Resolved_Input_Path,
19
+ } from './input_path.js';
20
+ import {print_path} from './paths.js';
21
+ import {search_fs} from './search_fs.js';
22
+ import {load_modules, type Load_Modules_Failure, type Module_Meta} from './modules.js';
23
+
24
+ export interface Task<
25
+ T_Args = Args, // same as `z.infer<typeof Args>`
26
+ T_Args_Schema extends z.ZodType = z.ZodType,
27
+ T_Return = unknown,
28
+ > {
29
+ run: (ctx: Task_Context<T_Args>) => T_Return | Promise<T_Return>; // TODO unused return value
30
+ summary?: string;
31
+ Args?: T_Args_Schema;
32
+ }
33
+
34
+ export interface Task_Context<T_Args = object> {
35
+ args: T_Args;
36
+ config: Gro_Config;
37
+ sveltekit_config: Parsed_Sveltekit_Config;
38
+ // TODO should this go here or on `config` for convenience?
39
+ // sveltekit_config: Parsed_Sveltekit_Config;
40
+ log: Logger;
41
+ timings: Timings;
42
+ invoke_task: (task_name: string, args?: Args, config?: Gro_Config) => Promise<void>;
43
+ }
44
+
45
+ export const TASK_FILE_SUFFIX_TS = '.task.ts';
46
+ export const TASK_FILE_SUFFIX_JS = '.task.js';
47
+ export const TASK_FILE_SUFFIXES = [TASK_FILE_SUFFIX_TS, TASK_FILE_SUFFIX_JS]; // TODO from `Gro_Config`, but needs to be used everywhere the constants are
48
+
49
+ export const is_task_path = (path: string): boolean =>
50
+ path.endsWith(TASK_FILE_SUFFIX_TS) || path.endsWith(TASK_FILE_SUFFIX_JS);
51
+
52
+ export const to_task_name = (
53
+ id: Path_Id,
54
+ task_root_dir: Path_Id,
55
+ input_path: Input_Path,
56
+ root_path: Path_Id,
57
+ ): string => {
58
+ let task_name = id.startsWith(task_root_dir)
59
+ ? strip_start(strip_start(id, task_root_dir), '/')
60
+ : id;
61
+ for (const suffix of TASK_FILE_SUFFIXES) {
62
+ task_name = strip_end(task_name, suffix);
63
+ }
64
+ if (isAbsolute(input_path)) {
65
+ // is a bit hacky, but does what we want
66
+ const relative_to_root = relative(root_path, join(task_root_dir, task_name));
67
+ if (relative_to_root.startsWith('node_modules/')) {
68
+ return task_name;
69
+ }
70
+ return relative_to_root;
71
+ }
72
+ return task_name;
73
+ };
74
+
75
+ /**
76
+ * This is used by tasks to signal a known failure.
77
+ * It's useful for cleaning up logging because
78
+ * we usually don't need their stack trace.
79
+ */
80
+ export class Task_Error extends Error {}
81
+
82
+ export interface Found_Task {
83
+ input_path: Input_Path;
84
+ id: Path_Id;
85
+ task_root_dir: Path_Id;
86
+ }
87
+
88
+ export interface Found_Tasks {
89
+ resolved_input_files: Resolved_Input_File[];
90
+ resolved_input_files_by_root_dir: Map<Path_Id, Resolved_Input_File[]>;
91
+ resolved_input_paths: Resolved_Input_Path[];
92
+ input_paths: Input_Path[];
93
+ task_root_dirs: Path_Id[];
94
+ }
95
+
96
+ export type Find_Tasks_Result = Result<{value: Found_Tasks}, Find_Modules_Failure>;
97
+ export type Find_Modules_Failure =
98
+ | {
99
+ type: 'unmapped_input_paths';
100
+ unmapped_input_paths: Input_Path[];
101
+ resolved_input_paths: Resolved_Input_Path[];
102
+ input_paths: Input_Path[];
103
+ task_root_dirs: Path_Id[];
104
+ reasons: string[];
105
+ }
106
+ | {
107
+ type: 'input_directories_with_no_files';
108
+ input_directories_with_no_files: Input_Path[];
109
+ resolved_input_files: Resolved_Input_File[];
110
+ resolved_input_files_by_root_dir: Map<Path_Id, Resolved_Input_File[]>;
111
+ resolved_input_paths: Resolved_Input_Path[];
112
+ input_paths: Input_Path[];
113
+ task_root_dirs: Path_Id[];
114
+ reasons: string[];
115
+ };
116
+
117
+ /**
118
+ * Finds modules from input paths. (see `src/lib/input_path.ts` for more)
119
+ */
120
+ export const find_tasks = (
121
+ input_paths: Input_Path[],
122
+ task_root_dirs: Path_Id[],
123
+ config: Gro_Config,
124
+ timings?: Timings,
125
+ ): Find_Tasks_Result => {
126
+ // Check which extension variation works - if it's a directory, prefer others first!
127
+ const timing_to_resolve_input_paths = timings?.start('resolve input paths');
128
+ const {resolved_input_paths, unmapped_input_paths} = resolve_input_paths(
129
+ input_paths,
130
+ task_root_dirs,
131
+ TASK_FILE_SUFFIXES,
132
+ );
133
+ timing_to_resolve_input_paths?.();
134
+
135
+ // Error if any input path could not be mapped.
136
+ if (unmapped_input_paths.length) {
137
+ return {
138
+ ok: false,
139
+ type: 'unmapped_input_paths',
140
+ unmapped_input_paths,
141
+ resolved_input_paths,
142
+ input_paths,
143
+ task_root_dirs,
144
+ reasons: unmapped_input_paths.map((input_path) =>
145
+ red(`Input path ${print_path(input_path)} cannot be mapped to a file or directory.`),
146
+ ),
147
+ };
148
+ }
149
+
150
+ // Find all of the files for any directories.
151
+ const timing_to_resolve_input_files = timings?.start('resolve input files');
152
+ const {resolved_input_files, resolved_input_files_by_root_dir, input_directories_with_no_files} =
153
+ resolve_input_files(resolved_input_paths, (id) =>
154
+ search_fs(id, {
155
+ filter: config.search_filters,
156
+ file_filter: (p) => TASK_FILE_SUFFIXES.some((s) => p.endsWith(s)),
157
+ }),
158
+ );
159
+ timing_to_resolve_input_files?.();
160
+
161
+ // Error if any input path has no files. (means we have an empty directory)
162
+ if (input_directories_with_no_files.length) {
163
+ return {
164
+ ok: false,
165
+ type: 'input_directories_with_no_files',
166
+ input_directories_with_no_files,
167
+ resolved_input_files,
168
+ resolved_input_files_by_root_dir,
169
+ resolved_input_paths,
170
+ input_paths,
171
+ task_root_dirs,
172
+ reasons: input_directories_with_no_files.map((input_path) =>
173
+ red(`Input directory contains no matching files: ${print_path(input_path)}`),
174
+ ),
175
+ };
176
+ }
177
+
178
+ return {
179
+ ok: true,
180
+ value: {
181
+ resolved_input_files,
182
+ resolved_input_files_by_root_dir,
183
+ resolved_input_paths,
184
+ input_paths,
185
+ task_root_dirs,
186
+ },
187
+ };
188
+ };
189
+
190
+ export interface Loaded_Tasks {
191
+ modules: Task_Module_Meta[];
192
+ found_tasks: Found_Tasks;
193
+ }
194
+
195
+ export interface Task_Module {
196
+ task: Task;
197
+ }
198
+
199
+ export interface Task_Module_Meta extends Module_Meta<Task_Module> {
200
+ name: string;
201
+ }
202
+
203
+ export type Load_Tasks_Result = Result<{value: Loaded_Tasks}, Load_Tasks_Failure>;
204
+ export type Load_Tasks_Failure = Load_Modules_Failure<Task_Module_Meta>;
205
+
206
+ export const load_tasks = async (
207
+ found_tasks: Found_Tasks,
208
+ root_path: Path_Id = process.cwd(), // TODO @multiple isn't passed in anywhere, maybe hoist to `invoke_task` and others
209
+ ): Promise<Load_Tasks_Result> => {
210
+ const loaded_modules = await load_modules(
211
+ found_tasks.resolved_input_files,
212
+ validate_task_module,
213
+ (resolved_input_file, mod): Task_Module_Meta => ({
214
+ id: resolved_input_file.id,
215
+ mod,
216
+ name: to_task_name(
217
+ resolved_input_file.id,
218
+ resolved_input_file.resolved_input_path.root_dir,
219
+ resolved_input_file.resolved_input_path.input_path,
220
+ root_path,
221
+ ),
222
+ }),
223
+ );
224
+ if (!loaded_modules.ok) {
225
+ return loaded_modules;
226
+ }
227
+
228
+ return {
229
+ ok: true,
230
+ value: {modules: loaded_modules.modules, found_tasks},
231
+ };
232
+ };
233
+
234
+ export const validate_task_module = (mod: Record<string, any>): mod is Task_Module =>
235
+ !!mod.task && typeof mod.task.run === 'function';
@@ -0,0 +1,180 @@
1
+ import {cyan, gray, green, red} from '@ryanatkn/belt/styletext.js';
2
+ import type {Logger} from '@ryanatkn/belt/log.js';
3
+ import {plural} from '@ryanatkn/belt/string.js';
4
+ import {print_value} from '@ryanatkn/belt/print.js';
5
+ import {ZodFirstPartyTypeKind, type ZodObjectDef, type ZodTypeAny, type ZodTypeDef} from 'zod';
6
+
7
+ import type {Arg_Schema} from './args.js';
8
+ import type {Loaded_Tasks, Task_Module_Meta} from './task.js';
9
+ import {print_path} from './paths.js';
10
+
11
+ export const log_tasks = (log: Logger, loaded_tasks: Loaded_Tasks, log_intro = true): void => {
12
+ const {modules, found_tasks} = loaded_tasks;
13
+ const {resolved_input_files_by_root_dir} = found_tasks;
14
+
15
+ const logged: string[] = [];
16
+ if (log_intro) {
17
+ logged.unshift(
18
+ `\n\n${gray('Run a task:')} gro [name]`,
19
+ `\n${gray('View help:')} gro [name] --help`,
20
+ );
21
+ }
22
+
23
+ for (const [root_dir, resolved_input_files] of resolved_input_files_by_root_dir) {
24
+ const dir_label = print_path(root_dir);
25
+ if (!resolved_input_files.length) {
26
+ log.info(`No tasks found in ${dir_label}.`);
27
+ continue;
28
+ }
29
+ logged.push(
30
+ `${log_intro ? '\n\n' : ''}${resolved_input_files.length} task${plural(
31
+ resolved_input_files.length,
32
+ )} in ${dir_label}:\n`,
33
+ );
34
+ const longest_task_name = to_max_length(modules, (m) => m.name);
35
+ for (const resolved_input_file of resolved_input_files) {
36
+ const meta = modules.find((m) => m.id === resolved_input_file.id)!;
37
+ logged.push(
38
+ '\n' + cyan(pad(meta.name, longest_task_name)),
39
+ ' ',
40
+ meta.mod.task.summary ?? '',
41
+ );
42
+ }
43
+ }
44
+ log[log_intro ? 'info' : 'plain'](logged.join('') + '\n');
45
+ };
46
+
47
+ export const log_error_reasons = (log: Logger, reasons: string[]): void => {
48
+ for (const reason of reasons) {
49
+ log.error(red(reason));
50
+ }
51
+ };
52
+
53
+ const ARGS_PROPERTY_NAME = '[...args]';
54
+
55
+ export const log_task_help = (log: Logger, meta: Task_Module_Meta): void => {
56
+ const {
57
+ name,
58
+ mod: {task},
59
+ } = meta;
60
+ const logged: string[] = [];
61
+ logged.push(
62
+ cyan(name),
63
+ 'help',
64
+ cyan(`\n\ngro ${name}`) + `: ${task.summary ?? '(no summary available)'}\n`,
65
+ );
66
+ if (task.Args) {
67
+ const properties = to_arg_properties(task.Args._def, meta);
68
+ // TODO hacky padding for some quick and dirty tables
69
+ const longest_task_name = Math.max(
70
+ ARGS_PROPERTY_NAME.length,
71
+ to_max_length(properties, (p) => p.name),
72
+ );
73
+ const longest_type = to_max_length(properties, (p) => p.schema.type);
74
+ const longest_default = to_max_length(properties, (p) => print_value(p.schema.default));
75
+ for (const property of properties) {
76
+ const name = property.name === '_' ? ARGS_PROPERTY_NAME : property.name;
77
+ logged.push(
78
+ `\n${green(pad(name, longest_task_name))} `,
79
+ gray(pad(property.schema.type, longest_type)) + ' ',
80
+ pad(print_value(property.schema.default), longest_default) + ' ',
81
+ property.schema.description || '(no description available)',
82
+ );
83
+ }
84
+ if (!properties.length) {
85
+ logged.push('\n' + gray('this task has no args'));
86
+ }
87
+ }
88
+ log.info(...logged, '\n');
89
+ };
90
+
91
+ interface Arg_Schema_Property {
92
+ name: string;
93
+ schema: Arg_Schema;
94
+ }
95
+
96
+ const to_arg_properties = (def: ZodTypeDef, meta: Task_Module_Meta): Arg_Schema_Property[] => {
97
+ const type_name = to_type_name(def);
98
+ if (type_name !== ZodFirstPartyTypeKind.ZodObject) {
99
+ throw Error(
100
+ `Expected Args for task "${meta.name}" to be a ZodObject schema but got ${type_name}`,
101
+ );
102
+ }
103
+ const shape = (def as ZodObjectDef).shape();
104
+ const properties: Arg_Schema_Property[] = [];
105
+ for (const name in shape) {
106
+ if ('no-' + name in shape) continue;
107
+ const s = shape[name];
108
+ const schema: Arg_Schema = {
109
+ type: to_args_schema_type(s),
110
+ description: to_args_schema_description(s),
111
+ default: to_args_schema_default(s),
112
+ };
113
+ properties.push({name, schema});
114
+ }
115
+ return properties;
116
+ };
117
+
118
+ // quick n dirty padding logic
119
+ const pad = (s: string, n: number): string => s + ' '.repeat(n - s.length);
120
+ const to_max_length = <T>(items: T[], toString: (item: T) => string) =>
121
+ items.reduce((max, m) => Math.max(toString(m).length, max), 0);
122
+
123
+ // The following Zod helpers only need to support single-depth schemas for CLI args,
124
+ // but there's generic recursion to handle things like `ZodOptional` and `ZodDefault`.
125
+ const to_type_name = (def: ZodTypeDef): ZodFirstPartyTypeKind => (def as any).typeName;
126
+ const to_args_schema_type = ({_def}: ZodTypeAny): Arg_Schema['type'] => {
127
+ const t = to_type_name(_def);
128
+ switch (t) {
129
+ case ZodFirstPartyTypeKind.ZodBoolean:
130
+ return 'boolean';
131
+ case ZodFirstPartyTypeKind.ZodString:
132
+ return 'string';
133
+ case ZodFirstPartyTypeKind.ZodNumber:
134
+ return 'number';
135
+ case ZodFirstPartyTypeKind.ZodArray:
136
+ return 'string[]'; // TODO support arrays of arbitrary types, or more hardcoded ones as needed
137
+ case ZodFirstPartyTypeKind.ZodEnum:
138
+ return _def.values.map((v: string) => `'${v}'`).join(' | ');
139
+ case ZodFirstPartyTypeKind.ZodUnion:
140
+ return 'string | string[]'; // TODO support unions of arbitrary types, or more hardcoded ones as needed
141
+ default: {
142
+ const subschema = to_subschema(_def);
143
+ if (subschema) {
144
+ return to_args_schema_type(subschema);
145
+ } else {
146
+ throw Error('Unknown zod type ' + t);
147
+ }
148
+ }
149
+ }
150
+ };
151
+ const to_args_schema_description = ({_def}: ZodTypeAny): string => {
152
+ if (_def.description) {
153
+ return _def.description;
154
+ }
155
+ const subschema = to_subschema(_def);
156
+ if (subschema) {
157
+ return to_args_schema_description(subschema);
158
+ }
159
+ return '';
160
+ };
161
+ const to_args_schema_default = ({_def}: ZodTypeAny): any => {
162
+ if (_def.defaultValue) {
163
+ return _def.defaultValue();
164
+ }
165
+ const subschema = to_subschema(_def);
166
+ if (subschema) {
167
+ return to_args_schema_default(subschema);
168
+ }
169
+ };
170
+
171
+ const to_subschema = (_def: any): ZodTypeAny | undefined => {
172
+ if ('type' in _def) {
173
+ return _def.type;
174
+ } else if ('innerType' in _def) {
175
+ return _def.innerType;
176
+ } else if ('schema' in _def) {
177
+ return _def.schema;
178
+ }
179
+ return undefined;
180
+ };
@@ -0,0 +1,50 @@
1
+ import {yellow} from '@ryanatkn/belt/styletext.js';
2
+ import {z} from 'zod';
3
+
4
+ import {Task_Error, type Task} from './task.js';
5
+ import {paths} from './paths.js';
6
+ import {find_cli} from './cli.js';
7
+
8
+ export const Args = z
9
+ .object({
10
+ _: z.array(z.string(), {description: 'file patterns to test'}).default([`\\.test\\.ts$`]), // TODO maybe use uvu's default instead of being restrictive?
11
+ bail: z
12
+ .boolean({description: 'the bail option to uvu run, exit immediately on failure'})
13
+ .default(false),
14
+ cwd: z.string({description: 'the cwd option to uvu parse'}).optional(),
15
+ ignore: z
16
+ .union([z.string(), z.array(z.string())], {description: 'the ignore option to uvu parse'})
17
+ .optional(),
18
+ })
19
+ .strict();
20
+ export type Args = z.infer<typeof Args>;
21
+
22
+ export const task: Task<Args> = {
23
+ summary: 'run tests with uvu',
24
+ Args,
25
+ run: async ({args, log}): Promise<void> => {
26
+ const {_: patterns, bail, cwd, ignore} = args;
27
+
28
+ if (!find_cli('uvu')) {
29
+ log.warn(yellow('uvu is not installed, skipping tests'));
30
+ return;
31
+ }
32
+
33
+ const [{run}, {parse}] = await Promise.all([import('uvu/run'), import('uvu/parse')]);
34
+
35
+ // uvu doesn't work with esm loaders and TypeScript files,
36
+ // so we use its `parse` and `run` APIs directly instead of its CLI.
37
+ // To avoid surprises, we allow any number of patterns in the rest args,
38
+ // so we call `parse` multiple times because it supports only one.
39
+ const suites = [];
40
+ for (const pattern of patterns) {
41
+ const parsed = await parse(paths.source, pattern, {cwd, ignore}); // eslint-disable-line no-await-in-loop
42
+ suites.push(...parsed.suites);
43
+ }
44
+ await run(suites, {bail});
45
+
46
+ if (process.exitCode) {
47
+ throw new Task_Error('Tests failed.');
48
+ }
49
+ },
50
+ };
@@ -0,0 +1,52 @@
1
+ import {wait} from '@ryanatkn/belt/async.js';
2
+ import {test} from 'uvu';
3
+ import * as assert from 'uvu/assert';
4
+
5
+ import {throttle} from './throttle.js';
6
+
7
+ test('throttles calls to a function', async () => {
8
+ const results: string[] = [];
9
+ const fn = throttle(async (name: string) => {
10
+ results.push(name + '_run');
11
+ await wait();
12
+ results.push(name + '_done');
13
+ });
14
+ const promise_a = fn('a');
15
+ const promise_b = fn('b'); // discarded
16
+ const promise_c = fn('c'); // discarded
17
+ const promise_d = fn('d');
18
+ assert.ok(promise_a !== promise_b);
19
+ assert.is(promise_b, promise_c);
20
+ assert.is(promise_b, promise_d);
21
+ assert.equal(results, ['a_run']);
22
+ await promise_a;
23
+ assert.equal(results, ['a_run', 'a_done']);
24
+ await promise_b;
25
+ assert.equal(results, ['a_run', 'a_done', 'd_run', 'd_done']);
26
+ const promise_e = fn('e'); // discarded
27
+ const promise_f = fn('f');
28
+ assert.ok(promise_d !== promise_e);
29
+ assert.is(promise_e, promise_f);
30
+ assert.equal(results, ['a_run', 'a_done', 'd_run', 'd_done']); // delayed
31
+ await wait();
32
+ assert.equal(results, ['a_run', 'a_done', 'd_run', 'd_done', 'f_run']);
33
+ await promise_e;
34
+ assert.equal(results, ['a_run', 'a_done', 'd_run', 'd_done', 'f_run', 'f_done']);
35
+ const promise_g = fn('g');
36
+ assert.equal(results, ['a_run', 'a_done', 'd_run', 'd_done', 'f_run', 'f_done']); // delayed
37
+ await wait();
38
+ assert.equal(results, ['a_run', 'a_done', 'd_run', 'd_done', 'f_run', 'f_done', 'g_run']);
39
+ await promise_g;
40
+ assert.equal(results, [
41
+ 'a_run',
42
+ 'a_done',
43
+ 'd_run',
44
+ 'd_done',
45
+ 'f_run',
46
+ 'f_done',
47
+ 'g_run',
48
+ 'g_done',
49
+ ]);
50
+ });
51
+
52
+ test.run();