@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,161 @@
1
+ import {join, resolve} from 'node:path';
2
+ import {existsSync} from 'node:fs';
3
+
4
+ import {GRO_DIST_DIR, IS_THIS_GRO, paths} from './paths.js';
5
+ import {
6
+ GRO_CONFIG_PATH,
7
+ NODE_MODULES_DIRNAME,
8
+ SERVER_DIST_PATH,
9
+ SVELTEKIT_BUILD_DIRNAME,
10
+ SVELTEKIT_DIST_DIRNAME,
11
+ } from './path_constants.js';
12
+ import create_default_config from './gro.config.default.js';
13
+ import type {Create_Config_Plugins} from './plugin.js';
14
+ import type {Map_Package_Json} from './package_json.js';
15
+ import type {Path_Filter, Path_Id} from './path.js';
16
+
17
+ /**
18
+ * The config that users can extend via `gro.config.ts`.
19
+ * This is exposed to users in places like tasks and genfiles.
20
+ * @see https://github.com/ryanatkn/gro/blob/main/src/lib/docs/config.md
21
+ */
22
+ export interface Gro_Config {
23
+ /**
24
+ * @see https://github.com/ryanatkn/gro/blob/main/src/lib/docs/plugin.md
25
+ */
26
+ plugins: Create_Config_Plugins;
27
+ /**
28
+ * Maps the project's `package.json` before writing it to the filesystem.
29
+ * The `package_json` argument may be mutated, but the return value is what's used by the caller.
30
+ * Returning `null` is a no-op for the caller.
31
+ */
32
+ map_package_json: Map_Package_Json | null;
33
+ /**
34
+ * The root directories to search for tasks given implicit relative input paths.
35
+ * Defaults to `./src/lib`, then the cwd, then the Gro package dist.
36
+ */
37
+ task_root_dirs: Path_Id[];
38
+ /**
39
+ * When searching the filsystem for tasks and genfiles,
40
+ * directories and files are included if they pass all of these filters.
41
+ */
42
+ search_filters: Path_Filter[];
43
+ }
44
+
45
+ /**
46
+ * The relaxed variant of `Gro_Config` that users can provide via `gro.config.ts`.
47
+ * Superset of `Gro_Config`.
48
+ * @see https://github.com/ryanatkn/gro/blob/main/src/lib/docs/config.md
49
+ */
50
+ export interface Raw_Gro_Config {
51
+ plugins?: Create_Config_Plugins;
52
+ map_package_json?: Map_Package_Json | null;
53
+ task_root_dirs?: string[];
54
+ search_filters?: Path_Filter | Path_Filter[] | null;
55
+ }
56
+
57
+ export type Create_Gro_Config = (
58
+ base_config: Gro_Config,
59
+ ) => Raw_Gro_Config | Promise<Raw_Gro_Config>;
60
+
61
+ export const create_empty_config = (): Gro_Config => ({
62
+ plugins: () => [],
63
+ map_package_json: default_map_package_json,
64
+ task_root_dirs: [
65
+ // TODO maybe disable if no SvelteKit `lib` directory? or other detection to improve defaults
66
+ paths.lib,
67
+ IS_THIS_GRO ? null : paths.root,
68
+ IS_THIS_GRO ? null : GRO_DIST_DIR,
69
+ ].filter((v) => v !== null),
70
+ search_filters: [(id) => !DEFAULT_SEARCH_EXCLUDER.test(id)],
71
+ });
72
+
73
+ /**
74
+ * The regexp used by default to exclude directories and files
75
+ * when searching the filesystem for tasks and genfiles.
76
+ * Customize via `search_filters` in the `Gro_Config`.
77
+ * See the test cases for the exact behavior.
78
+ */
79
+ export const DEFAULT_SEARCH_EXCLUDER = new RegExp(
80
+ `(${
81
+ '(^|/)\\.[^/]+' + // exclude all `.`-prefixed directories
82
+ // TODO probably change to `pkg.name` instead of this catch-all (also `gro` below)
83
+ `|(^|/)${NODE_MODULES_DIRNAME}(?!/(@[^/]+/)?gro/${SVELTEKIT_DIST_DIRNAME})` + // exclude `node_modules` unless it's to the Gro directory
84
+ `|(^|/)${SVELTEKIT_BUILD_DIRNAME}` + // exclude the SvelteKit build directory
85
+ `|(^|/)(?<!(^|/)gro/)${SVELTEKIT_DIST_DIRNAME}` + // exclude the SvelteKit dist directory unless it's in the Gro directory
86
+ `|(^|/)${SERVER_DIST_PATH}` // exclude the Gro server plugin dist directory
87
+ })($|/)`,
88
+ 'u',
89
+ );
90
+
91
+ const default_map_package_json: Map_Package_Json = (package_json) => {
92
+ if (package_json.exports) {
93
+ package_json.exports = Object.fromEntries(
94
+ Object.entries(package_json.exports).filter(([k]) => !DEFAULT_EXPORTS_EXCLUDER.test(k)),
95
+ );
96
+ }
97
+ return package_json;
98
+ };
99
+
100
+ export const DEFAULT_EXPORTS_EXCLUDER = /(\.md|\.(test|ignore)\.|\/(test|fixtures|ignore)\/)/u;
101
+
102
+ /**
103
+ * Transforms a `Raw_Gro_Config` to the more strict `Gro_Config`.
104
+ * This allows users to provide a more relaxed config.
105
+ */
106
+ export const normalize_config = (raw_config: Raw_Gro_Config): Gro_Config => {
107
+ const empty_config = create_empty_config();
108
+ // All of the raw config properties are optional,
109
+ // so fall back to the empty values when `undefined`.
110
+ const {
111
+ plugins = empty_config.plugins,
112
+ map_package_json = empty_config.map_package_json,
113
+ task_root_dirs = empty_config.task_root_dirs,
114
+ search_filters = empty_config.search_filters,
115
+ } = raw_config;
116
+ return {
117
+ plugins,
118
+ map_package_json,
119
+ task_root_dirs: task_root_dirs.map((p) => resolve(p)),
120
+ search_filters: Array.isArray(search_filters)
121
+ ? search_filters
122
+ : search_filters
123
+ ? [search_filters]
124
+ : [],
125
+ };
126
+ };
127
+
128
+ export interface Gro_Config_Module {
129
+ readonly default: Raw_Gro_Config | Create_Gro_Config;
130
+ }
131
+
132
+ export const load_config = async (dir = paths.root): Promise<Gro_Config> => {
133
+ const default_config = normalize_config(await create_default_config(create_empty_config()));
134
+ const config_path = join(dir, GRO_CONFIG_PATH);
135
+ if (!existsSync(config_path)) {
136
+ // No user config file found, so return the default.
137
+ return default_config;
138
+ }
139
+ // Import the user's `gro.config.ts`.
140
+ const config_module = await import(config_path);
141
+ validate_config_module(config_module, config_path);
142
+ return normalize_config(
143
+ typeof config_module.default === 'function'
144
+ ? await config_module.default(default_config)
145
+ : config_module.default,
146
+ );
147
+ };
148
+
149
+ export const validate_config_module: (
150
+ config_module: any,
151
+ config_path: string,
152
+ ) => asserts config_module is Gro_Config_Module = (config_module, config_path) => {
153
+ const config = config_module.default;
154
+ if (!config) {
155
+ throw Error(`Invalid Gro config module at ${config_path}: expected a default export`);
156
+ } else if (!(typeof config === 'function' || typeof config === 'object')) {
157
+ throw Error(
158
+ `Invalid Gro config module at ${config_path}: the default export must be a function or object`,
159
+ );
160
+ }
161
+ };
@@ -0,0 +1,243 @@
1
+ import {spawn} from '@ryanatkn/belt/process.js';
2
+ import {print_error} from '@ryanatkn/belt/print.js';
3
+ import {green, red} from '@ryanatkn/belt/styletext.js';
4
+ import {z} from 'zod';
5
+ import {cp, mkdir, rm} from 'node:fs/promises';
6
+ import {join, resolve} from 'node:path';
7
+ import {existsSync, readdirSync} from 'node:fs';
8
+
9
+ import {Task_Error, type Task} from './task.js';
10
+ import {print_path} from './paths.js';
11
+ import {GRO_DIRNAME, GIT_DIRNAME, SVELTEKIT_BUILD_DIRNAME} from './path_constants.js';
12
+ import {empty_dir} from './fs.js';
13
+ import {
14
+ git_check_clean_workspace,
15
+ git_checkout,
16
+ git_local_branch_exists,
17
+ git_remote_branch_exists,
18
+ Git_Origin,
19
+ Git_Branch,
20
+ git_delete_local_branch,
21
+ git_push_to_create,
22
+ git_reset_branch_to_first_commit,
23
+ git_pull,
24
+ git_fetch,
25
+ git_check_setting_pull_rebase,
26
+ git_clone_locally,
27
+ git_current_branch_name,
28
+ } from './git.js';
29
+
30
+ // docs at ./docs/deploy.md
31
+
32
+ // terminal command for testing:
33
+ // npm run bootstrap && rm -rf .gro && clear && gro deploy --source no-git-workspace --no-build --dry
34
+
35
+ // TODO customize
36
+ const dir = process.cwd();
37
+ const INITIAL_FILE_PATH = '.gitkeep';
38
+ const DEPLOY_DIR = GRO_DIRNAME + '/deploy';
39
+ const SOURCE_BRANCH = 'main';
40
+ const TARGET_BRANCH = 'deploy';
41
+ const DANGEROUS_BRANCHES = [SOURCE_BRANCH, 'master'];
42
+
43
+ export const Args = z
44
+ .object({
45
+ source: Git_Branch.describe('git source branch to build and deploy from').default(
46
+ SOURCE_BRANCH,
47
+ ),
48
+ target: Git_Branch.describe('git target branch to deploy to').default(TARGET_BRANCH),
49
+ origin: Git_Origin.describe('git origin to deploy to').default('origin'),
50
+ deploy_dir: z.string({description: 'the deploy output directory'}).default(DEPLOY_DIR),
51
+ build_dir: z
52
+ .string({description: 'the SvelteKit build directory'})
53
+ .default(SVELTEKIT_BUILD_DIRNAME),
54
+ dry: z
55
+ .boolean({
56
+ description: 'build and prepare to deploy without actually deploying',
57
+ })
58
+ .default(false),
59
+ force: z
60
+ .boolean({description: 'caution!! destroys the target branch both locally and remotely'})
61
+ .default(false),
62
+ dangerous: z
63
+ .boolean({description: 'caution!! enables destruction of branches like main and master'})
64
+ .default(false),
65
+ reset: z
66
+ .boolean({
67
+ description: 'if true, resets the target branch back to the first commit before deploying',
68
+ })
69
+ .default(false),
70
+ build: z.boolean({description: 'dual of no-build'}).default(true),
71
+ 'no-build': z.boolean({description: 'opt out of building'}).default(false),
72
+ })
73
+ .strict();
74
+ export type Args = z.infer<typeof Args>;
75
+
76
+ export const task: Task<Args> = {
77
+ summary: 'deploy to a branch',
78
+ Args,
79
+ run: async ({args, log, invoke_task}): Promise<void> => {
80
+ const {source, target, origin, build_dir, deploy_dir, dry, force, dangerous, reset, build} =
81
+ args;
82
+
83
+ // Checks
84
+ if (!force && target !== TARGET_BRANCH) {
85
+ throw new Task_Error(
86
+ `Warning! You are deploying to a custom target branch '${target}',` +
87
+ ` instead of the default '${TARGET_BRANCH}' branch.` +
88
+ ` This is destructive to your '${target}' branch!` +
89
+ ` If you understand and are OK with deleting your branch '${target}',` +
90
+ ` both locally and remotely, pass --force to suppress this error.`,
91
+ );
92
+ }
93
+ if (!dangerous && DANGEROUS_BRANCHES.includes(target)) {
94
+ throw new Task_Error(
95
+ `Warning! You are deploying to a custom target branch '${target}'` +
96
+ ` and that appears very dangerous: it is destructive to your '${target}' branch!` +
97
+ ` If you understand and are OK with deleting your branch '${target}',` +
98
+ ` both locally and remotely, pass --dangerous to suppress this error.`,
99
+ );
100
+ }
101
+ const clean_error_message = await git_check_clean_workspace();
102
+ if (clean_error_message) {
103
+ throw new Task_Error(
104
+ 'Deploy failed because the git workspace has uncommitted changes: ' + clean_error_message,
105
+ );
106
+ }
107
+ if (!(await git_check_setting_pull_rebase())) {
108
+ throw new Task_Error(
109
+ 'Deploying currently requires `git config --global pull.rebase true`,' +
110
+ ' but this restriction could be lifted with more work',
111
+ );
112
+ }
113
+
114
+ // Fetch the source branch in the cwd if it's not there
115
+ if (!(await git_local_branch_exists(source))) {
116
+ await git_fetch(origin, source);
117
+ }
118
+
119
+ // Prepare the source branch in the cwd
120
+ await git_checkout(source);
121
+ await git_pull(origin, source);
122
+ if (await git_check_clean_workspace()) {
123
+ throw new Task_Error(
124
+ 'Deploy failed because the local source branch is out of sync with the remote one,' +
125
+ ' finish rebasing manually or reset with `git rebase --abort`',
126
+ );
127
+ }
128
+
129
+ // Prepare the target branch remotely and locally
130
+ const resolved_deploy_dir = resolve(deploy_dir);
131
+ const target_spawn_options = {cwd: resolved_deploy_dir};
132
+ const remote_target_exists = await git_remote_branch_exists(origin, target);
133
+ if (remote_target_exists) {
134
+ // Remote target branch already exists, so sync up efficiently
135
+
136
+ // First, check if the deploy dir exists, and if so, attempt to sync it.
137
+ // If anything goes wrong, delete the directory and we'll initialize it
138
+ // using the same code path as if it didn't exist in the first place.
139
+ if (existsSync(resolved_deploy_dir)) {
140
+ if (target !== (await git_current_branch_name(target_spawn_options))) {
141
+ // We're in a bad state because the target branch has changed,
142
+ // so delete the directory and continue as if it wasn't there.
143
+ await rm(resolved_deploy_dir, {recursive: true});
144
+ } else {
145
+ await spawn('git', ['reset', '--hard'], target_spawn_options); // in case it's dirty
146
+ await git_pull(origin, target, target_spawn_options);
147
+ if (await git_check_clean_workspace(target_spawn_options)) {
148
+ // We're in a bad state because the local branch lost continuity with the remote,
149
+ // so delete the directory and continue as if it wasn't there.
150
+ await rm(resolved_deploy_dir, {recursive: true});
151
+ }
152
+ }
153
+ }
154
+
155
+ // Second, initialize the deploy dir if needed.
156
+ // It may not exist, or it may have been deleted after failing to sync above.
157
+ if (!existsSync(resolved_deploy_dir)) {
158
+ const local_deploy_branch_exists = await git_local_branch_exists(target);
159
+ await git_fetch(origin, ('+' + target + ':' + target) as Git_Branch); // fetch+merge and allow non-fastforward updates with the +
160
+ await git_clone_locally(origin, target, dir, resolved_deploy_dir);
161
+ // Clean up if we created the target branch in the cwd
162
+ if (!local_deploy_branch_exists) {
163
+ await git_delete_local_branch(target);
164
+ }
165
+ }
166
+
167
+ // Local target branch is now synced with remote, but do we need to reset?
168
+ if (reset) {
169
+ await git_reset_branch_to_first_commit(origin, target, target_spawn_options);
170
+ }
171
+ } else {
172
+ // Remote target branch does not exist, so start from scratch
173
+
174
+ // Delete the deploy dir and recreate it
175
+ if (existsSync(resolved_deploy_dir)) {
176
+ await rm(resolved_deploy_dir, {recursive: true});
177
+ await mkdir(resolved_deploy_dir, {recursive: true});
178
+ }
179
+
180
+ // Delete the target branch locally in the cwd if it exists
181
+ if (await git_local_branch_exists(target)) {
182
+ await git_delete_local_branch(target);
183
+ }
184
+
185
+ // Create the target branch locally and remotely.
186
+ // This is more complex to avoid churning the cwd.
187
+ await git_clone_locally(origin, source, dir, resolved_deploy_dir);
188
+ await spawn('git', ['checkout', '--orphan', target], target_spawn_options);
189
+ // TODO there's definitely a better way to do this
190
+ await spawn('git', ['rm', '-rf', '.'], target_spawn_options);
191
+ await spawn('touch', [INITIAL_FILE_PATH], target_spawn_options);
192
+ await spawn('git', ['add', INITIAL_FILE_PATH], target_spawn_options);
193
+ await spawn('git', ['commit', '-m', 'init'], target_spawn_options);
194
+ await git_push_to_create(origin, target, target_spawn_options);
195
+ await git_delete_local_branch(source, target_spawn_options);
196
+ }
197
+
198
+ // Remove everything except .git from the deploy directory to avoid stale files
199
+ await empty_dir(resolved_deploy_dir, (path) => path !== GIT_DIRNAME);
200
+
201
+ // Build
202
+ try {
203
+ if (build) {
204
+ await invoke_task('build');
205
+ }
206
+ if (!existsSync(build_dir)) {
207
+ log.error(red('directory to deploy does not exist after building:'), build_dir);
208
+ return;
209
+ }
210
+ } catch (err) {
211
+ log.error(red('build failed'), 'but', green('no changes were made to git'), print_error(err));
212
+ if (dry) {
213
+ log.info(red('dry deploy failed'));
214
+ }
215
+ throw new Task_Error(`Deploy safely canceled due to build failure. See the error above.`);
216
+ }
217
+
218
+ // Copy the build
219
+ await Promise.all(
220
+ readdirSync(build_dir).map((path) =>
221
+ cp(join(build_dir, path), join(resolved_deploy_dir, path), {recursive: true}),
222
+ ),
223
+ );
224
+
225
+ // At this point, `dist/` is ready to be committed and deployed!
226
+ if (dry) {
227
+ log.info(green('dry deploy complete:'), 'files at', print_path(resolved_deploy_dir));
228
+ return;
229
+ }
230
+
231
+ // Commit and push
232
+ try {
233
+ await spawn('git', ['add', '.', '-f'], target_spawn_options);
234
+ await spawn('git', ['commit', '-m', 'deployment'], target_spawn_options);
235
+ await spawn('git', ['push', origin, target, '-f'], target_spawn_options); // force push because we may be resetting the branch, see the checks above to make this safer
236
+ } catch (err) {
237
+ log.error(red('updating git failed:'), print_error(err));
238
+ throw new Task_Error(`Deploy failed in a bad state: built but not pushed, see error above.`);
239
+ }
240
+
241
+ log.info(green('deployed')); // TODO log a different message if "Everything up-to-date"
242
+ },
243
+ };
@@ -0,0 +1,43 @@
1
+ import {z} from 'zod';
2
+
3
+ import type {Task} from './task.js';
4
+ import {Plugins, type Plugin_Context} from './plugin.js';
5
+ import {clean_fs} from './clean_fs.js';
6
+
7
+ export const Args = z
8
+ .object({
9
+ watch: z.boolean({description: 'dual of no-watch'}).default(true),
10
+ 'no-watch': z
11
+ .boolean({
12
+ description:
13
+ 'opt out of running a long-lived process to watch files and rebuild on changes',
14
+ })
15
+ .default(false),
16
+ sync: z.boolean({description: 'dual of no-sync'}).default(true),
17
+ 'no-sync': z.boolean({description: 'opt out of gro sync'}).default(false),
18
+ })
19
+ .strict();
20
+ export type Args = z.infer<typeof Args>;
21
+
22
+ export type DevTask_Context = Plugin_Context<Args>;
23
+
24
+ export const task: Task<Args> = {
25
+ summary: 'start SvelteKit and other dev plugins',
26
+ Args,
27
+ run: async (ctx) => {
28
+ const {args, invoke_task} = ctx;
29
+ const {watch, sync} = args;
30
+
31
+ await clean_fs({build_dev: true});
32
+
33
+ if (sync) {
34
+ await invoke_task('sync');
35
+ }
36
+
37
+ const plugins = await Plugins.create({...ctx, dev: true, watch});
38
+ await plugins.setup();
39
+ if (!watch) {
40
+ await plugins.teardown();
41
+ }
42
+ },
43
+ };
@@ -0,0 +1,63 @@
1
+ import {dirname, relative, basename} from 'node:path';
2
+ import {parse_path_parts, parse_path_segments} from '@ryanatkn/belt/path.js';
3
+ import {strip_start} from '@ryanatkn/belt/string.js';
4
+
5
+ import {type Gen, to_output_file_name} from '../gen.js';
6
+ import {paths, base_path_to_path_id} from '../paths.js';
7
+ import {search_fs} from '../search_fs.js';
8
+
9
+ // TODO look at `tasks.gen.md.ts` to refactor and generalize
10
+ // TODO show nested structure, not a flat list
11
+ // TODO work with file types beyond markdown
12
+
13
+ /**
14
+ * Renders a simple index of a possibly nested directory of files.
15
+ */
16
+ export const gen: Gen = ({origin_id}) => {
17
+ // TODO need to get this from project config or something
18
+ const root_path = parse_path_segments(paths.root).at(-1);
19
+
20
+ const origin_dir = dirname(origin_id);
21
+ const origin_base = basename(origin_id);
22
+
23
+ const base_dir = paths.source;
24
+ const relative_path = strip_start(origin_id, base_dir);
25
+ const relative_dir = dirname(relative_path);
26
+
27
+ // TODO should this be passed in the context, like `defaultOutputFileName`?
28
+ const output_file_name = to_output_file_name(origin_base);
29
+
30
+ // TODO this is GitHub-specific
31
+ const root_link = `[${root_path}](/../..)`;
32
+ const doc_files = search_fs(origin_dir);
33
+ const doc_paths: string[] = [];
34
+ for (const {path} of doc_files) {
35
+ if (path === output_file_name || !path.endsWith('.md')) {
36
+ continue;
37
+ }
38
+ doc_paths.push(path);
39
+ }
40
+
41
+ // TODO do we want to use absolute paths instead of relative paths,
42
+ // because GitHub works with them and it simplifies the code?
43
+ const is_index_file = output_file_name === 'README.md';
44
+ const path_parts = parse_path_parts(relative_dir).map((relative_path_part) => {
45
+ const segment = parse_path_segments(relative_path_part).at(-1);
46
+ return is_index_file && relative_path_part === relative_dir
47
+ ? segment
48
+ : `[${segment}](${relative(origin_dir, base_path_to_path_id(relative_path_part)) || './'})`;
49
+ });
50
+ const breadcrumbs =
51
+ '> <sub>' + [root_link, ...path_parts, output_file_name].join(' / ') + '</sub>';
52
+
53
+ // TODO render the footer with the origin_id
54
+ return `# docs
55
+
56
+ ${breadcrumbs}
57
+
58
+ ${doc_paths.reduce((docList, doc) => docList + `- [${basename(doc, '.md')}](${doc})\n`, '')}
59
+ ${breadcrumbs}
60
+
61
+ > <sub>generated by [${origin_base}](${origin_base})</sub>
62
+ `;
63
+ };
@@ -0,0 +1,20 @@
1
+ # docs
2
+
3
+ > <sub>[gro](/../..) / [lib](..) / docs / README.md</sub>
4
+
5
+ - [build](build.md)
6
+ - [config](config.md)
7
+ - [deploy](deploy.md)
8
+ - [dev](dev.md)
9
+ - [gen](gen.md)
10
+ - [gro_plugin_sveltekit_app](gro_plugin_sveltekit_app.md)
11
+ - [package_json](package_json.md)
12
+ - [plugin](plugin.md)
13
+ - [publish](publish.md)
14
+ - [task](task.md)
15
+ - [tasks](tasks.md)
16
+ - [test](test.md)
17
+
18
+ > <sub>[gro](/../..) / [lib](..) / docs / README.md</sub>
19
+
20
+ > <sub>generated by [README.gen.md.ts](README.gen.md.ts)</sub>
@@ -0,0 +1,41 @@
1
+ # build
2
+
3
+ > these docs are for production builds, for development see [dev.md](dev.md)
4
+
5
+ ## usage
6
+
7
+ The `gro build` task produces outputs for production:
8
+
9
+ ```bash
10
+ gro build
11
+ ```
12
+
13
+ This runs the configured Gro plugins, `setup -> adapt -> teardown`, in production mode.
14
+
15
+ If your project has a SvelteKit frontend,
16
+ [the default plugin](../gro_plugin_sveltekit_app.ts) calls `vite build`,
17
+ forwarding any [`-- vite [...]` args](https://vitejs.dev/config/):
18
+
19
+ ```bash
20
+ gro build -- vite --config my-config.js
21
+ ```
22
+
23
+ ## plugins
24
+
25
+ `Plugin`s are objects that customize the behavior of `gro build` and `gro dev`.
26
+ They try to defer to underlying tools as much as possible, and exist to glue everything together.
27
+ For example, the library plugin internally uses
28
+ [`svelte-package`](https://kit.svelte.dev/docs/packaging).
29
+ See [plugin.md](plugin.md) to learn more.
30
+
31
+ ## deploying and publishing
32
+
33
+ Now that we can produce builds, how do we share them with the world?
34
+
35
+ The [`gro deploy`](deploy.md) task outputs builds to a branch,
36
+ like for static publishing to GitHub pages.
37
+
38
+ The [`gro publish`](publish.md) task publishes packages to npm.
39
+
40
+ Both of these tasks call `gro build` internally,
41
+ and you can always run it manually if you're curious.