@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,206 @@
1
+ import {z} from 'zod';
2
+ import {spawn} from '@ryanatkn/belt/process.js';
3
+ import {red, blue} from '@ryanatkn/belt/styletext.js';
4
+ import type {WrittenConfig} from '@changesets/types';
5
+ import {readFile, writeFile} from 'node:fs/promises';
6
+ import {join} from 'node:path';
7
+ import {existsSync, readdirSync} from 'node:fs';
8
+
9
+ import {Task_Error, type Task} from './task.js';
10
+ import {find_cli, spawn_cli} from './cli.js';
11
+ import {Git_Origin, git_check_fully_staged_workspace, git_push_to_create} from './git.js';
12
+ import {has_sveltekit_library} from './sveltekit_helpers.js';
13
+ import {
14
+ CHANGESET_CLI,
15
+ CHANGESET_DIR,
16
+ Changeset_Access,
17
+ Changeset_Bump,
18
+ CHANGESET_PUBLIC_ACCESS,
19
+ CHANGESET_RESTRICTED_ACCESS,
20
+ } from './changeset_helpers.js';
21
+ import {load_package_json} from './package_json.js';
22
+
23
+ export const Args = z
24
+ .object({
25
+ /**
26
+ * The optional rest args get joined with a space to form the `message`.
27
+ */
28
+ _: z.array(z.string(), {description: 'the message for the changeset and commit'}).default([]),
29
+ minor: z.boolean({description: 'bump the minor version'}).default(false),
30
+ major: z.boolean({description: 'bump the major version'}).default(false),
31
+ dir: z.string({description: 'changeset dir'}).default(CHANGESET_DIR),
32
+ access: Changeset_Access.describe(
33
+ "changeset 'access' config value, the default depends on package.json#private",
34
+ ).optional(),
35
+ changelog: z
36
+ .string({description: 'changeset "changelog" config value'})
37
+ .default('@changesets/changelog-git'),
38
+ install: z.boolean({description: 'dual of no-install'}).default(true),
39
+ 'no-install': z
40
+ .boolean({description: 'opt out of npm installing the changelog package'})
41
+ .default(false),
42
+ origin: Git_Origin.describe('git origin to deploy to').default('origin'),
43
+ changeset_cli: z.string({description: 'the changeset CLI to use'}).default(CHANGESET_CLI),
44
+ })
45
+ .strict();
46
+ export type Args = z.infer<typeof Args>;
47
+
48
+ /**
49
+ * Calls the `changeset` CLI with some simple automations.
50
+ * This API is designed for convenient manual usage, not clarity or normality.
51
+ *
52
+ * Usage:
53
+ * - gro changeset some commit message
54
+ * - gro changeset some commit message --minor
55
+ * - gro changeset "some commit message" --minor
56
+ */
57
+ export const task: Task<Args> = {
58
+ summary: 'call changeset with gro patterns',
59
+ Args,
60
+ run: async (ctx): Promise<void> => {
61
+ const {
62
+ invoke_task,
63
+ args: {_, minor, major, dir, access: access_arg, changelog, install, origin, changeset_cli},
64
+ log,
65
+ sveltekit_config,
66
+ } = ctx;
67
+
68
+ const message = _.join(' ');
69
+
70
+ if (!message && (minor || major)) throw new Task_Error('cannot bump version without a message');
71
+ if (minor && major) throw new Task_Error('cannot bump both minor and major');
72
+
73
+ const bump: Changeset_Bump = minor ? 'minor' : major ? 'major' : 'patch';
74
+
75
+ const found_changeset_cli = find_cli(changeset_cli);
76
+ if (!found_changeset_cli) {
77
+ throw new Task_Error(
78
+ 'changeset command not found: install @changesets/cli locally or globally',
79
+ );
80
+ }
81
+
82
+ const package_json = load_package_json();
83
+
84
+ const has_sveltekit_library_result = has_sveltekit_library(package_json, sveltekit_config);
85
+ if (!has_sveltekit_library_result.ok) {
86
+ throw new Task_Error(
87
+ 'Failed to find SvelteKit library: ' + has_sveltekit_library_result.message,
88
+ );
89
+ }
90
+
91
+ const path = join(dir, 'config.json');
92
+
93
+ const inited = existsSync(path);
94
+
95
+ if (!inited) {
96
+ await spawn_cli(found_changeset_cli, ['init'], log);
97
+
98
+ const access =
99
+ access_arg ?? package_json.private ? CHANGESET_RESTRICTED_ACCESS : CHANGESET_PUBLIC_ACCESS;
100
+
101
+ const access_color = access === CHANGESET_RESTRICTED_ACCESS ? blue : red;
102
+ log.info('initing changeset with ' + access_color(access) + ' access');
103
+ if (access !== CHANGESET_RESTRICTED_ACCESS) {
104
+ await update_changeset_config(path, (config) => {
105
+ const updated = {...config};
106
+ updated.access = access;
107
+ updated.changelog = changelog;
108
+ return updated;
109
+ });
110
+ }
111
+
112
+ await spawn('git', ['add', dir]);
113
+
114
+ if (install) {
115
+ await spawn('npm', ['i', '-D', changelog]);
116
+ }
117
+ }
118
+
119
+ // TODO small problem here where generated files don't get committed
120
+ await invoke_task('sync'); // after the `npm i` above, and in all cases
121
+
122
+ if (message) {
123
+ // TODO see the helper below, simplify this to CLI flags when support is added to Changesets
124
+ const changeset_adder = create_changeset_adder(package_json.name, dir, message, bump);
125
+ await spawn_cli(found_changeset_cli, ['add', '--empty'], log);
126
+ await changeset_adder();
127
+ if (!(await git_check_fully_staged_workspace())) {
128
+ await spawn('git', ['commit', '-m', message]);
129
+ await git_push_to_create(origin);
130
+ }
131
+ } else {
132
+ await spawn_cli(found_changeset_cli, [], log);
133
+ await spawn('git', ['add', dir]);
134
+ }
135
+ },
136
+ };
137
+
138
+ /**
139
+ * TODO ideally this wouldn't exist and we'd use CLI flags, but they doesn't exist yet
140
+ * @see https://github.com/changesets/changesets/pull/1121
141
+ */
142
+ const create_changeset_adder = (
143
+ repo_name: string,
144
+ dir: string,
145
+ message: string,
146
+ bump: Changeset_Bump,
147
+ ) => {
148
+ const filenames_before = readdirSync(dir);
149
+ return async () => {
150
+ const filenames_after = readdirSync(dir);
151
+ const filenames_added = filenames_after.filter((p) => !filenames_before.includes(p));
152
+ if (!filenames_added.length) {
153
+ throw Error('expected to find a new changeset file');
154
+ }
155
+ if (filenames_added.length !== 1) {
156
+ throw Error('expected to find exactly one new changeset file');
157
+ }
158
+ const path = join(dir, filenames_added[0]);
159
+ const contents = create_new_changeset(repo_name, message, bump);
160
+ await writeFile(path, contents, 'utf8');
161
+ await spawn('git', ['add', path]);
162
+ };
163
+ };
164
+
165
+ const create_new_changeset = (
166
+ repo_name: string,
167
+ message: string,
168
+ bump: Changeset_Bump,
169
+ ): string => `---
170
+ "${repo_name}": ${bump}
171
+ ---
172
+
173
+ ${message}
174
+ `;
175
+
176
+ type Changeset_Callback = (config: WrittenConfig) => WrittenConfig | Promise<WrittenConfig>;
177
+
178
+ type Update_Written_Config = (path: string, cb: Changeset_Callback) => Promise<boolean>;
179
+
180
+ // TODO refactor all of this with zod and package_json helpers - util file helper? JSON parse pluggable
181
+
182
+ const update_changeset_config: Update_Written_Config = async (path, cb) => {
183
+ const config_contents = await load_changeset_config_contents(path);
184
+ const config = parse_changeset_config(config_contents);
185
+
186
+ const updated = await cb(config);
187
+
188
+ const serialized = serialize_changeset_config(updated);
189
+
190
+ if (serialized === config_contents) {
191
+ return false;
192
+ }
193
+
194
+ await write_changeset_config(path, serialized);
195
+ return true;
196
+ };
197
+
198
+ const load_changeset_config_contents = (path: string): Promise<string> => readFile(path, 'utf8');
199
+
200
+ const write_changeset_config = (path: string, serialized: string): Promise<void> =>
201
+ writeFile(path, serialized);
202
+
203
+ const serialize_changeset_config = (config: WrittenConfig): string =>
204
+ JSON.stringify(config, null, '\t') + '\n';
205
+
206
+ const parse_changeset_config = (contents: string): WrittenConfig => JSON.parse(contents);
@@ -0,0 +1,13 @@
1
+ import {z} from 'zod';
2
+
3
+ export const CHANGESET_RESTRICTED_ACCESS = 'restricted';
4
+ export const CHANGESET_PUBLIC_ACCESS = 'public';
5
+
6
+ export const Changeset_Access = z.enum([CHANGESET_RESTRICTED_ACCESS, CHANGESET_PUBLIC_ACCESS]);
7
+
8
+ export const CHANGESET_CLI = 'changeset';
9
+
10
+ export const CHANGESET_DIR = '.changeset';
11
+
12
+ export const Changeset_Bump = z.enum(['patch', 'minor', 'major']);
13
+ export type Changeset_Bump = z.infer<typeof Changeset_Bump>;
@@ -0,0 +1,90 @@
1
+ import {z} from 'zod';
2
+ import {spawn} from '@ryanatkn/belt/process.js';
3
+ import {red} from '@ryanatkn/belt/styletext.js';
4
+
5
+ import {Task_Error, type Task} from './task.js';
6
+ import {git_check_clean_workspace} from './git.js';
7
+ import {sync_package_json} from './package_json.js';
8
+
9
+ export const Args = z
10
+ .object({
11
+ typecheck: z.boolean({description: 'dual of no-typecheck'}).default(true),
12
+ 'no-typecheck': z.boolean({description: 'opt out of typechecking'}).default(false),
13
+ test: z.boolean({description: 'dual of no-test'}).default(true),
14
+ 'no-test': z.boolean({description: 'opt out of running tests'}).default(false),
15
+ gen: z.boolean({description: 'dual of no-gen'}).default(true),
16
+ 'no-gen': z.boolean({description: 'opt out of gen check'}).default(false),
17
+ format: z.boolean({description: 'dual of no-format'}).default(true),
18
+ 'no-format': z.boolean({description: 'opt out of format check'}).default(false),
19
+ package_json: z.boolean({description: 'dual of no-package_json'}).default(true),
20
+ 'no-package_json': z.boolean({description: 'opt out of package.json check'}).default(false),
21
+ lint: z.boolean({description: 'dual of no-lint'}).default(true),
22
+ 'no-lint': z.boolean({description: 'opt out of linting'}).default(false),
23
+ workspace: z
24
+ .boolean({description: 'ensure a clean git workspace, useful for CI'})
25
+ .default(false),
26
+ })
27
+ .strict();
28
+ export type Args = z.infer<typeof Args>;
29
+
30
+ export const task: Task<Args> = {
31
+ summary: 'check that everything is ready to commit',
32
+ Args,
33
+ run: async ({args, invoke_task, log, config}) => {
34
+ const {typecheck, test, gen, format, package_json, lint, workspace} = args;
35
+
36
+ // When checking the workspace, which was added for CI,
37
+ // don't sync, because the check will fail with misleading errors.
38
+ // For example it would cause the gen check to be a false negative,
39
+ // and then the workspace check would fail with the new files.
40
+ const sync = !workspace;
41
+ if (sync) {
42
+ await invoke_task('sync');
43
+ }
44
+
45
+ if (typecheck) {
46
+ await invoke_task('typecheck');
47
+ }
48
+
49
+ if (test) {
50
+ await invoke_task('test');
51
+ }
52
+
53
+ if (gen) {
54
+ await invoke_task('gen', {check: true});
55
+ }
56
+
57
+ if (package_json && config.map_package_json) {
58
+ const {changed} = await sync_package_json(config.map_package_json, log, true);
59
+ if (changed) {
60
+ throw new Task_Error('package.json is out of date, run `gro sync` to update it');
61
+ } else {
62
+ log.info('check passed for package.json');
63
+ }
64
+ }
65
+
66
+ if (format) {
67
+ await invoke_task('format', {check: true});
68
+ }
69
+
70
+ // Run the linter last to surface every other kind of problem first.
71
+ // It's not the ideal order when the linter would catch errors that cause failing tests,
72
+ // but it's better for most usage.
73
+ if (lint) {
74
+ await invoke_task('lint');
75
+ }
76
+
77
+ if (workspace) {
78
+ const error_message = await git_check_clean_workspace();
79
+ if (error_message) {
80
+ log.error(red('git status'));
81
+ await spawn('git', ['status']);
82
+ throw new Task_Error(
83
+ 'Failed check for git_check_clean_workspace:' +
84
+ error_message +
85
+ ' - do you need to run `gro sync` or commit some files?',
86
+ );
87
+ }
88
+ }
89
+ },
90
+ };
@@ -0,0 +1,46 @@
1
+ import {spawn} from '@ryanatkn/belt/process.js';
2
+ import {z} from 'zod';
3
+
4
+ import type {Task} from './task.js';
5
+ import {clean_fs} from './clean_fs.js';
6
+ import {Git_Origin} from './git.js';
7
+
8
+ export const Args = z
9
+ .object({
10
+ build_dev: z.boolean({description: 'delete the Gro build dev directory'}).default(false),
11
+ build_dist: z.boolean({description: 'delete the Gro build dist directory'}).default(false),
12
+ sveltekit: z
13
+ .boolean({description: 'delete the SvelteKit directory and Vite cache'})
14
+ .default(false),
15
+ nodemodules: z.boolean({description: 'delete the node_modules directory'}).default(false),
16
+ git: z
17
+ .boolean({
18
+ description:
19
+ 'run "git remote prune" to delete local branches referencing nonexistent remote branches',
20
+ })
21
+ .default(false),
22
+ git_origin: Git_Origin.describe('the origin to "git remote prune"').default('origin'),
23
+ })
24
+ .strict();
25
+ export type Args = z.infer<typeof Args>;
26
+
27
+ export const task: Task<Args> = {
28
+ summary: 'remove temporary dev and build files, and optionally prune git branches',
29
+ Args,
30
+ run: async ({args}): Promise<void> => {
31
+ const {build_dev, build_dist, sveltekit, nodemodules, git, git_origin} = args;
32
+
33
+ await clean_fs({
34
+ build: !build_dev && !build_dist,
35
+ build_dev,
36
+ build_dist,
37
+ sveltekit,
38
+ nodemodules,
39
+ });
40
+
41
+ // lop off stale git branches
42
+ if (git) {
43
+ await spawn('git', ['remote', 'prune', git_origin]);
44
+ }
45
+ },
46
+ };
@@ -0,0 +1,54 @@
1
+ import {rm} from 'node:fs/promises';
2
+ import {readdirSync, type RmOptions} from 'node:fs';
3
+
4
+ import {paths} from './paths.js';
5
+ import {
6
+ NODE_MODULES_DIRNAME,
7
+ GRO_DIST_PREFIX,
8
+ SVELTEKIT_DEV_DIRNAME,
9
+ SVELTEKIT_BUILD_DIRNAME,
10
+ SVELTEKIT_VITE_CACHE_PATH,
11
+ SVELTEKIT_DIST_DIRNAME,
12
+ } from './path_constants.js';
13
+
14
+ export const clean_fs = async (
15
+ {
16
+ build = false,
17
+ build_dev = false,
18
+ build_dist = false,
19
+ sveltekit = false,
20
+ nodemodules = false,
21
+ }: {
22
+ build?: boolean;
23
+ build_dev?: boolean;
24
+ build_dist?: boolean;
25
+ sveltekit?: boolean;
26
+ nodemodules?: boolean;
27
+ },
28
+ rm_options: RmOptions = {force: true, recursive: true},
29
+ ): Promise<void> => {
30
+ const promises: Array<Promise<void>> = [];
31
+
32
+ if (build) {
33
+ promises.push(rm(paths.build, rm_options));
34
+ } else if (build_dev) {
35
+ promises.push(rm(paths.build_dev, rm_options));
36
+ }
37
+ if (build || build_dist) {
38
+ const paths = readdirSync('.').filter((p) => p.startsWith(GRO_DIST_PREFIX));
39
+ for (const path of paths) {
40
+ promises.push(rm(path, rm_options));
41
+ }
42
+ }
43
+ if (sveltekit) {
44
+ promises.push(rm(SVELTEKIT_DEV_DIRNAME, rm_options));
45
+ promises.push(rm(SVELTEKIT_BUILD_DIRNAME, rm_options));
46
+ promises.push(rm(SVELTEKIT_DIST_DIRNAME, rm_options));
47
+ promises.push(rm(SVELTEKIT_VITE_CACHE_PATH, rm_options));
48
+ }
49
+ if (nodemodules) {
50
+ promises.push(rm(NODE_MODULES_DIRNAME, rm_options));
51
+ }
52
+
53
+ await Promise.all(promises);
54
+ };
package/src/lib/cli.ts ADDED
@@ -0,0 +1,97 @@
1
+ import {spawnSync, type SpawnOptions} from 'node:child_process';
2
+ import {
3
+ spawn,
4
+ spawn_process,
5
+ type Spawn_Result,
6
+ type Spawned_Process,
7
+ } from '@ryanatkn/belt/process.js';
8
+ import {join} from 'node:path';
9
+ import {existsSync} from 'node:fs';
10
+ import {fileURLToPath} from 'node:url';
11
+ import type {Logger} from '@ryanatkn/belt/log.js';
12
+
13
+ import {NODE_MODULES_DIRNAME} from './path_constants.js';
14
+ import type {Path_Id} from './path.js';
15
+ import {print_command_args} from './args.js';
16
+
17
+ // TODO maybe upstream to Belt?
18
+
19
+ export type Cli =
20
+ | {kind: 'local'; name: string; id: Path_Id}
21
+ | {kind: 'global'; name: string; id: Path_Id};
22
+
23
+ /**
24
+ * Searches the filesystem for the CLI `name`, first local to the cwd and then globally.
25
+ * @returns `null` if not found locally or globally
26
+ */
27
+ export const find_cli = (
28
+ name: string,
29
+ cwd: string | URL = process.cwd(),
30
+ options?: SpawnOptions | undefined,
31
+ ): Cli | null => {
32
+ const final_cwd = typeof cwd === 'string' ? cwd : fileURLToPath(cwd);
33
+ const local_id = join(final_cwd, NODE_MODULES_DIRNAME, `.bin/${name}`);
34
+ if (existsSync(local_id)) {
35
+ return {name, id: local_id, kind: 'local'};
36
+ }
37
+ const {stdout} = spawnSync('which', [name], options);
38
+ const global_id = stdout.toString().trim();
39
+ if (!global_id) return null;
40
+ return {name, id: global_id, kind: 'global'};
41
+ };
42
+
43
+ /**
44
+ * Spawns a CLI if available using Belt's `spawn`.
45
+ * If a string is provided for `name_or_cli`, it checks first local to the cwd and then globally.
46
+ * @returns `undefined` if no CLI is found, or the spawn result
47
+ */
48
+ export const spawn_cli = async (
49
+ name_or_cli: string | Cli,
50
+ args: string[] = [],
51
+ log?: Logger,
52
+ options?: SpawnOptions | undefined,
53
+ ): Promise<Spawn_Result | undefined> => {
54
+ const cli = resolve_cli(name_or_cli, args, options?.cwd, log, options);
55
+ if (!cli) return;
56
+ return spawn(cli.id, args, options);
57
+ };
58
+
59
+ /**
60
+ * Spawns a CLI if available using Belt's `spawn_process`.
61
+ * If a string is provided for `name_or_cli`, it checks first local to the cwd and then globally.
62
+ * @returns `undefined` if no CLI is found, or the spawn result
63
+ */
64
+ export const spawn_cli_process = (
65
+ name_or_cli: string | Cli,
66
+ args: string[] = [],
67
+ log?: Logger,
68
+ options?: SpawnOptions | undefined,
69
+ ): Spawned_Process | undefined => {
70
+ const cli = resolve_cli(name_or_cli, args, options?.cwd, log, options);
71
+ if (!cli) return;
72
+ return spawn_process(cli.id, args, options);
73
+ };
74
+
75
+ export const resolve_cli = (
76
+ name_or_cli: string | Cli,
77
+ args: string[] = [],
78
+ cwd: string | URL | undefined,
79
+ log?: Logger,
80
+ options?: SpawnOptions | undefined,
81
+ ): Cli | undefined => {
82
+ let final_cli;
83
+ if (typeof name_or_cli === 'string') {
84
+ const found = find_cli(name_or_cli, cwd, options);
85
+ if (!found) return;
86
+ final_cli = found;
87
+ } else {
88
+ final_cli = name_or_cli;
89
+ }
90
+ if (log) {
91
+ log.info(print_command_args([final_cli.name].concat(args)));
92
+ }
93
+ return final_cli;
94
+ };
95
+
96
+ export const to_cli_name = (cli: string | Cli): string =>
97
+ typeof cli === 'string' ? cli : cli.name;
@@ -0,0 +1,33 @@
1
+ import {spawn} from '@ryanatkn/belt/process.js';
2
+ import {z} from 'zod';
3
+
4
+ import type {Task} from './task.js';
5
+ import {Git_Origin, git_current_branch_name, git_push} from './git.js';
6
+
7
+ export const Args = z
8
+ .object({
9
+ _: z
10
+ .array(z.string(), {
11
+ description: 'the git commit message, the same as git commit -m or --message',
12
+ })
13
+ .default([]),
14
+ origin: Git_Origin.describe('git origin to commit to').default('origin'),
15
+ })
16
+ .strict();
17
+ export type Args = z.infer<typeof Args>;
18
+
19
+ export const task: Task<Args> = {
20
+ summary: 'commit and push to a new branch',
21
+ Args,
22
+ run: async ({args}): Promise<void> => {
23
+ const {
24
+ _: [message],
25
+ origin,
26
+ } = args;
27
+
28
+ const branch = await git_current_branch_name();
29
+
30
+ await spawn('git', ['commit', '-a', '-m', message]);
31
+ await git_push(origin, branch, undefined, true);
32
+ },
33
+ };
@@ -0,0 +1,71 @@
1
+ import {test} from 'uvu';
2
+ import * as assert from 'uvu/assert';
3
+
4
+ import {DEFAULT_SEARCH_EXCLUDER, load_config} from './config.js';
5
+
6
+ test('load_config', async () => {
7
+ const config = await load_config();
8
+ assert.ok(config);
9
+ });
10
+
11
+ test('DEFAULT_SEARCH_EXCLUDER', () => {
12
+ const assert_includes = (path: string, exclude: boolean) => {
13
+ const m = `should ${exclude ? 'exclude' : 'include '}: ${path}`;
14
+ const b = (v: boolean) => (exclude ? !v : v);
15
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`a/${path}/c`)), m);
16
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`a/${path}/c/d.js`)), m);
17
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`a/${path}/c/d.e.js`)), m);
18
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`a/${path}/`)), m);
19
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`a/${path}`)), m);
20
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/a/${path}/c`)), m);
21
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/a/${path}/c/d.js`)), m);
22
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/a/${path}/c/d.e.js`)), m);
23
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/a/${path}/`)), m);
24
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/a/${path}`)), m);
25
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/${path}/a`)), m);
26
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/${path}/a/b.js`)), m);
27
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/${path}/a/b.e.js`)), m);
28
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/${path}/`)), m);
29
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`/${path}`)), m);
30
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`./${path}/a`)), m);
31
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`./${path}/a/b.js`)), m);
32
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`./${path}/a/b.c.js`)), m);
33
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`./${path}/`)), m);
34
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`./${path}`)), m);
35
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`${path}/a`)), m);
36
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`${path}/a/b.js`)), m);
37
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`${path}/a/b.c.js`)), m);
38
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(`${path}/`)), m);
39
+ assert.ok(b(DEFAULT_SEARCH_EXCLUDER.test(path)), m);
40
+ };
41
+
42
+ assert_includes('node_modules', false);
43
+ assert_includes('dist', false);
44
+ assert_includes('build', false);
45
+ assert_includes('.git', false);
46
+ assert_includes('.gro', false);
47
+ assert_includes('.svelte-kit', false);
48
+
49
+ assert_includes('a', true);
50
+ assert_includes('nodemodules', true);
51
+
52
+ // Special exception for `gro/dist/`, but not `gro/build/` etc because they're not usecases.
53
+ assert_includes('gro/build', false);
54
+ assert_includes('gro/buildE', true);
55
+ assert_includes('groE/build', false);
56
+ assert_includes('gro/dist', true);
57
+ assert_includes('node_modules/gro/dist', true);
58
+ assert_includes('node_modules/@someuser/gro/dist', true);
59
+ assert_includes('node_modules/@someuser/foo/gro/dist', false);
60
+ assert_includes('gro/distE', true);
61
+ assert_includes('groE/dist', false);
62
+ assert_includes('Egro/dist', false);
63
+ assert_includes('Ebuild', true);
64
+ assert_includes('buildE', true);
65
+ assert_includes('grobuild', true);
66
+ assert_includes('distE', true);
67
+ assert_includes('Edist', true);
68
+ assert_includes('grodist', true);
69
+ });
70
+
71
+ test.run();