@ryanatkn/gro 0.170.0 → 0.171.0

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 (42) hide show
  1. package/dist/build.task.d.ts +6 -1
  2. package/dist/build.task.d.ts.map +1 -1
  3. package/dist/build.task.js +86 -5
  4. package/dist/build_cache.d.ts +100 -0
  5. package/dist/build_cache.d.ts.map +1 -0
  6. package/dist/build_cache.js +289 -0
  7. package/dist/deploy.task.d.ts.map +1 -1
  8. package/dist/deploy.task.js +13 -10
  9. package/dist/esbuild_plugin_svelte.js +1 -1
  10. package/dist/gen.d.ts.map +1 -1
  11. package/dist/gro_config.d.ts +30 -1
  12. package/dist/gro_config.d.ts.map +1 -1
  13. package/dist/gro_config.js +28 -4
  14. package/dist/hash.d.ts +1 -1
  15. package/dist/hash.d.ts.map +1 -1
  16. package/dist/hash.js +1 -2
  17. package/dist/invoke_task.d.ts.map +1 -1
  18. package/dist/invoke_task.js +2 -1
  19. package/dist/package.d.ts.map +1 -1
  20. package/dist/package.js +23 -14
  21. package/dist/package_json.js +1 -1
  22. package/package.json +3 -3
  23. package/src/lib/build.task.ts +110 -6
  24. package/src/lib/build_cache.ts +362 -0
  25. package/src/lib/changelog.ts +1 -1
  26. package/src/lib/changeset.task.ts +1 -1
  27. package/src/lib/commit.task.ts +1 -1
  28. package/src/lib/deploy.task.ts +14 -10
  29. package/src/lib/esbuild_plugin_svelte.ts +1 -1
  30. package/src/lib/gen.ts +2 -1
  31. package/src/lib/gro_config.ts +62 -3
  32. package/src/lib/hash.ts +2 -4
  33. package/src/lib/invoke_task.ts +5 -2
  34. package/src/lib/package.ts +23 -14
  35. package/src/lib/package_json.ts +2 -2
  36. package/src/lib/parse_exports_context.ts +2 -2
  37. package/src/lib/parse_imports.ts +1 -1
  38. package/src/lib/upgrade.task.ts +1 -1
  39. package/dist/test_helpers.d.ts +0 -22
  40. package/dist/test_helpers.d.ts.map +0 -1
  41. package/dist/test_helpers.js +0 -123
  42. package/src/lib/test_helpers.ts +0 -161
@@ -2,6 +2,11 @@ import type { Path_Filter, Path_Id } from '@ryanatkn/belt/path.js';
2
2
  import type { Create_Config_Plugins } from './plugin.ts';
3
3
  import type { Map_Package_Json } from './package_json.ts';
4
4
  import type { Parsed_Svelte_Config } from './svelte_config.ts';
5
+ /**
6
+ * SHA-256 hash of empty string, used for configs without build_cache_config.
7
+ * This ensures consistent cache behavior when no custom config is provided.
8
+ */
9
+ export declare const EMPTY_BUILD_CACHE_CONFIG_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
5
10
  /**
6
11
  * The config that users can extend via `gro.config.ts`.
7
12
  * This is exposed to users in places like tasks and genfiles.
@@ -38,6 +43,13 @@ export interface Gro_Config extends Raw_Gro_Config {
38
43
  pm_cli: string;
39
44
  /** @default SVELTE_CONFIG_FILENAME */
40
45
  svelte_config_filename?: string;
46
+ /**
47
+ * SHA-256 hash of the user's `build_cache_config` from `gro.config.ts`.
48
+ * This is computed during config normalization and the raw value is immediately deleted.
49
+ * If no `build_cache_config` was provided, this is the hash of an empty string.
50
+ * @see Raw_Gro_Config.build_cache_config
51
+ */
52
+ build_cache_config_hash: string;
41
53
  }
42
54
  /**
43
55
  * The relaxed variant of `Gro_Config` that users can provide via `gro.config.ts`.
@@ -51,6 +63,22 @@ export interface Raw_Gro_Config {
51
63
  search_filters?: Path_Filter | Array<Path_Filter> | null;
52
64
  js_cli?: string;
53
65
  pm_cli?: string;
66
+ /**
67
+ * Optional object defining custom build inputs for cache invalidation.
68
+ * This value is hashed during config normalization and used to detect
69
+ * when builds need to be regenerated due to non-source changes.
70
+ *
71
+ * Use cases:
72
+ * - Environment variables baked into build: `{api_url: process.env.PUBLIC_API_URL}`
73
+ * - External data files: `{data: fs.readFileSync('data.json', 'utf-8')}`
74
+ * - Build feature flags: `{enable_analytics: true}`
75
+ *
76
+ * Can be a static object or an async function that returns an object.
77
+ *
78
+ * IMPORTANT: It's safe to include secrets here because they are hashed and `delete`d
79
+ * during config normalization. The raw value is never logged or persisted.
80
+ */
81
+ build_cache_config?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
54
82
  }
55
83
  export type Create_Gro_Config = (base_config: Gro_Config, svelte_config?: Parsed_Svelte_Config) => Raw_Gro_Config | Promise<Raw_Gro_Config>;
56
84
  export declare const create_empty_gro_config: () => Gro_Config;
@@ -65,8 +93,9 @@ export declare const EXPORTS_EXCLUDER_DEFAULT: RegExp;
65
93
  /**
66
94
  * Transforms a `Raw_Gro_Config` to the more strict `Gro_Config`.
67
95
  * This allows users to provide a more relaxed config.
96
+ * Hashes the `build_cache_config` and deletes the raw value for security.
68
97
  */
69
- export declare const cook_gro_config: (raw_config: Raw_Gro_Config) => Gro_Config;
98
+ export declare const cook_gro_config: (raw_config: Raw_Gro_Config) => Promise<Gro_Config>;
70
99
  export interface Gro_Config_Module {
71
100
  readonly default: Raw_Gro_Config | Create_Gro_Config;
72
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"gro_config.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/gro_config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAE,OAAO,EAAC,MAAM,wBAAwB,CAAC;AAajE,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,WAAW,UAAW,SAAQ,cAAc;IACjD;;OAEG;IACH,OAAO,EAAE,qBAAqB,CAAC;IAC/B;;;;OAIG;IACH,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/B;;;OAGG;IACH,cAAc,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,gBAAgB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,cAAc,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,iBAAiB,GAAG,CAC/B,WAAW,EAAE,UAAU,EACvB,aAAa,CAAC,EAAE,oBAAoB,KAChC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;AAE9C,eAAO,MAAM,uBAAuB,QAAO,UAYzC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAUnC,CAAC;AAEF,eAAO,MAAM,wBAAwB,QAAwD,CAAC;AAE9F;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,cAAc,KAAG,UA0B5D,CAAC;AAEF,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,iBAAiB,CAAC;CACrD;AAED,eAAO,MAAM,eAAe,GAAU,YAAgB,KAAG,OAAO,CAAC,UAAU,CAmB1E,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,CACxC,aAAa,EAAE,GAAG,EAClB,WAAW,EAAE,MAAM,KACf,OAAO,CAAC,aAAa,IAAI,iBAS7B,CAAC"}
1
+ {"version":3,"file":"gro_config.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/gro_config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAE,OAAO,EAAC,MAAM,wBAAwB,CAAC;AAcjE,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAG7D;;;GAGG;AACH,eAAO,MAAM,6BAA6B,qEACyB,CAAC;AAEpE;;;;GAIG;AACH,MAAM,WAAW,UAAW,SAAQ,cAAc;IACjD;;OAEG;IACH,OAAO,EAAE,qBAAqB,CAAC;IAC/B;;;;OAIG;IACH,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/B;;;OAGG;IACH,cAAc,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,gBAAgB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,cAAc,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;OAcG;IACH,kBAAkB,CAAC,EAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACvB,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACtE;AAED,MAAM,MAAM,iBAAiB,GAAG,CAC/B,WAAW,EAAE,UAAU,EACvB,aAAa,CAAC,EAAE,oBAAoB,KAChC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;AAE9C,eAAO,MAAM,uBAAuB,QAAO,UAazC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAUnC,CAAC;AAEF,eAAO,MAAM,wBAAwB,QAAwD,CAAC;AAE9F;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAU,YAAY,cAAc,KAAG,OAAO,CAAC,UAAU,CA+CpF,CAAC;AAEF,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,iBAAiB,CAAC;CACrD;AAED,eAAO,MAAM,eAAe,GAAU,YAAgB,KAAG,OAAO,CAAC,UAAU,CAqB1E,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,CACxC,aAAa,EAAE,GAAG,EAClB,WAAW,EAAE,MAAM,KACf,OAAO,CAAC,aAAa,IAAI,iBAS7B,CAAC"}
@@ -9,9 +9,16 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { join, resolve } from 'node:path';
10
10
  import { existsSync } from 'node:fs';
11
11
  import { identity } from '@ryanatkn/belt/function.js';
12
+ import { json_stringify_deterministic } from '@ryanatkn/belt/json.js';
12
13
  import { GRO_DIST_DIR, IS_THIS_GRO, paths } from "./paths.js";
13
14
  import { GRO_CONFIG_FILENAME, JS_CLI_DEFAULT, NODE_MODULES_DIRNAME, PM_CLI_DEFAULT, SERVER_DIST_PATH, SVELTEKIT_BUILD_DIRNAME, SVELTEKIT_DIST_DIRNAME, } from "./constants.js";
14
15
  import create_default_config from "./gro.config.default.js";
16
+ import { to_hash } from "./hash.js";
17
+ /**
18
+ * SHA-256 hash of empty string, used for configs without build_cache_config.
19
+ * This ensures consistent cache behavior when no custom config is provided.
20
+ */
21
+ export const EMPTY_BUILD_CACHE_CONFIG_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
15
22
  export const create_empty_gro_config = () => ({
16
23
  plugins: () => [],
17
24
  map_package_json: identity,
@@ -24,6 +31,7 @@ export const create_empty_gro_config = () => ({
24
31
  search_filters: [(id) => !SEARCH_EXCLUDER_DEFAULT.test(id)],
25
32
  js_cli: JS_CLI_DEFAULT,
26
33
  pm_cli: PM_CLI_DEFAULT,
34
+ build_cache_config_hash: EMPTY_BUILD_CACHE_CONFIG_HASH,
27
35
  });
28
36
  /**
29
37
  * The regexp used by default to exclude directories and files
@@ -42,12 +50,27 @@ export const EXPORTS_EXCLUDER_DEFAULT = /(\.md|\.(test|ignore)\.|\/(test|fixture
42
50
  /**
43
51
  * Transforms a `Raw_Gro_Config` to the more strict `Gro_Config`.
44
52
  * This allows users to provide a more relaxed config.
53
+ * Hashes the `build_cache_config` and deletes the raw value for security.
45
54
  */
46
- export const cook_gro_config = (raw_config) => {
55
+ export const cook_gro_config = async (raw_config) => {
47
56
  const empty_config = create_empty_gro_config();
48
57
  // All of the raw config properties are optional,
49
58
  // so fall back to the empty values when `undefined`.
50
- const { plugins = empty_config.plugins, map_package_json = empty_config.map_package_json, task_root_dirs = empty_config.task_root_dirs, search_filters = empty_config.search_filters, js_cli = empty_config.js_cli, pm_cli = empty_config.pm_cli, } = raw_config;
59
+ const { plugins = empty_config.plugins, map_package_json = empty_config.map_package_json, task_root_dirs = empty_config.task_root_dirs, search_filters = empty_config.search_filters, js_cli = empty_config.js_cli, pm_cli = empty_config.pm_cli, build_cache_config, } = raw_config;
60
+ // Hash build_cache_config and delete the raw value
61
+ // IMPORTANT: Raw value may contain secrets - hash it and delete immediately
62
+ let build_cache_config_hash;
63
+ if (!build_cache_config) {
64
+ build_cache_config_hash = EMPTY_BUILD_CACHE_CONFIG_HASH;
65
+ }
66
+ else {
67
+ // Resolve if it's a function
68
+ const resolved = typeof build_cache_config === 'function' ? await build_cache_config() : build_cache_config;
69
+ // Hash the JSON representation with deterministic key ordering
70
+ build_cache_config_hash = await to_hash(new TextEncoder().encode(json_stringify_deterministic(resolved)));
71
+ }
72
+ // Delete the raw value to ensure it doesn't persist in memory
73
+ delete raw_config.build_cache_config;
51
74
  return {
52
75
  plugins,
53
76
  map_package_json,
@@ -59,10 +82,11 @@ export const cook_gro_config = (raw_config) => {
59
82
  : [],
60
83
  js_cli,
61
84
  pm_cli,
85
+ build_cache_config_hash,
62
86
  };
63
87
  };
64
88
  export const load_gro_config = async (dir = paths.root) => {
65
- const default_config = cook_gro_config(await create_default_config(create_empty_gro_config()));
89
+ const default_config = await cook_gro_config(await create_default_config(create_empty_gro_config()));
66
90
  const config_path = join(dir, GRO_CONFIG_FILENAME);
67
91
  if (!existsSync(config_path)) {
68
92
  // No user config file found, so return the default.
@@ -71,7 +95,7 @@ export const load_gro_config = async (dir = paths.root) => {
71
95
  // Import the user's `gro.config.ts`.
72
96
  const config_module = await import(__rewriteRelativeImportExtension(config_path, true));
73
97
  validate_gro_config_module(config_module, config_path);
74
- return cook_gro_config(typeof config_module.default === 'function'
98
+ return await cook_gro_config(typeof config_module.default === 'function'
75
99
  ? await config_module.default(default_config)
76
100
  : config_module.default);
77
101
  };
package/dist/hash.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
3
3
  */
4
- export declare const to_hash: (data: Buffer, algorithm?: "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512") => Promise<string>;
4
+ export declare const to_hash: (data: BufferSource, algorithm?: "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512") => Promise<string>;
5
5
  //# sourceMappingURL=hash.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hash.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,eAAO,MAAM,OAAO,GACnB,MAAM,MAAM,EACZ,YAAW,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAqB,KAChE,OAAO,CAAC,MAAM,CAQhB,CAAC"}
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hash.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,OAAO,GACnB,MAAM,YAAY,EAClB,YAAW,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAqB,KAChE,OAAO,CAAC,MAAM,CAQhB,CAAC"}
package/dist/hash.js CHANGED
@@ -1,5 +1,4 @@
1
- import { webcrypto } from 'node:crypto';
2
- const { subtle } = webcrypto;
1
+ const { subtle } = globalThis.crypto;
3
2
  /**
4
3
  * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
5
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"invoke_task.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/invoke_task.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,OAAO,EAAC,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAoB,KAAK,IAAI,EAAC,MAAM,WAAW,CAAC;AAEvD,OAAO,EAAgB,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAI9D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,WAAW,GACvB,WAAW,cAAc,EACzB,MAAM,IAAI,GAAG,SAAS,EACtB,QAAQ,UAAU,EAClB,gBAAgB,KAAK,EACrB,kBAAkB,OAAO,GAAG,IAAI,KAC9B,OAAO,CAAC,IAAI,CAwFd,CAAC"}
1
+ {"version":3,"file":"invoke_task.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/invoke_task.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,OAAO,EAAC,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAoB,KAAK,IAAI,EAAC,MAAM,WAAW,CAAC;AAEvD,OAAO,EAAgB,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAI9D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAC,KAAK,EAAC,MAAM,YAAY,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,WAAW,GACvB,WAAW,cAAc,EACzB,MAAM,IAAI,GAAG,SAAS,EACtB,QAAQ,UAAU,EAClB,gBAAgB,KAAK,EACrB,kBAAkB,OAAO,GAAG,IAAI,KAC9B,OAAO,CAAC,IAAI,CA2Fd,CAAC"}
@@ -77,7 +77,8 @@ export const invoke_task = async (task_name, args, config, initial_filer, initia
77
77
  throw new Silent_Error();
78
78
  }
79
79
  const loaded_tasks = loaded.value;
80
- if (resolved_input_files.length > 1 || resolved_input_files[0].resolved_input_path.is_directory) {
80
+ if (resolved_input_files.length > 1 ||
81
+ resolved_input_files[0].resolved_input_path.is_directory) {
81
82
  // The input path matches a directory. Log the tasks but don't run them.
82
83
  log_tasks(log, loaded_tasks);
83
84
  await finish();
@@ -1 +1 @@
1
- {"version":3,"file":"package.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/package.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,4BAA4B,CAAC;AAEzD,eAAO,MAAM,YAAY,EAAE,YAkGnB,CAAC;AAET,eAAO,MAAM,QAAQ,EAAE,QA8vBf,CAAC"}
1
+ {"version":3,"file":"package.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/package.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,4BAA4B,CAAC;AAEzD,eAAO,MAAM,YAAY,EAAE,YAkGnB,CAAC;AAET,eAAO,MAAM,QAAQ,EAAE,QAuwBf,CAAC"}
package/dist/package.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // generated by src/lib/package.gen.ts
2
2
  export const package_json = {
3
3
  name: '@ryanatkn/gro',
4
- version: '0.170.0',
4
+ version: '0.171.0',
5
5
  description: 'task runner and toolkit extending SvelteKit',
6
6
  motto: 'generate, run, optimize',
7
7
  glyph: '🌰',
@@ -51,7 +51,7 @@ export const package_json = {
51
51
  zod: '^4.1.12',
52
52
  },
53
53
  peerDependencies: {
54
- '@ryanatkn/belt': '^0.35.1',
54
+ '@ryanatkn/belt': '^0.36.0',
55
55
  '@sveltejs/kit': '^2',
56
56
  esbuild: '^0.25',
57
57
  svelte: '^5',
@@ -78,7 +78,7 @@ export const package_json = {
78
78
  'svelte-check': '^4.3.3',
79
79
  typescript: '^5.9.3',
80
80
  'typescript-eslint': '^8.42.0',
81
- vitest: '^4.0.0',
81
+ vitest: '^4.0.3',
82
82
  },
83
83
  prettier: {
84
84
  plugins: ['prettier-plugin-svelte'],
@@ -99,7 +99,7 @@ export const package_json = {
99
99
  };
100
100
  export const src_json = {
101
101
  name: '@ryanatkn/gro',
102
- version: '0.170.0',
102
+ version: '0.171.0',
103
103
  modules: {
104
104
  '.': {
105
105
  path: 'index.ts',
@@ -132,10 +132,28 @@ export const src_json = {
132
132
  { name: 'print_command_args', kind: 'function' },
133
133
  ],
134
134
  },
135
+ './build_cache.js': {
136
+ path: 'build_cache.ts',
137
+ declarations: [
138
+ { name: 'BUILD_CACHE_METADATA_FILENAME', kind: 'variable' },
139
+ { name: 'BUILD_CACHE_VERSION', kind: 'variable' },
140
+ { name: 'Build_Output_Entry', kind: 'variable' },
141
+ { name: 'Build_Cache_Metadata', kind: 'variable' },
142
+ { name: 'compute_build_cache_key', kind: 'function' },
143
+ { name: 'load_build_cache_metadata', kind: 'function' },
144
+ { name: 'save_build_cache_metadata', kind: 'function' },
145
+ { name: 'validate_build_cache', kind: 'function' },
146
+ { name: 'is_build_cache_valid', kind: 'function' },
147
+ { name: 'collect_build_outputs', kind: 'function' },
148
+ { name: 'discover_build_output_dirs', kind: 'function' },
149
+ { name: 'create_build_cache_metadata', kind: 'function' },
150
+ ],
151
+ },
135
152
  './build.task.js': {
136
153
  path: 'build.task.ts',
137
154
  declarations: [
138
155
  { name: 'Args', kind: 'variable' },
156
+ { name: 'GIT_SHORT_HASH_LENGTH', kind: 'variable' },
139
157
  { name: 'task', kind: 'variable' },
140
158
  ],
141
159
  },
@@ -408,6 +426,7 @@ export const src_json = {
408
426
  './gro_config.js': {
409
427
  path: 'gro_config.ts',
410
428
  declarations: [
429
+ { name: 'EMPTY_BUILD_CACHE_CONFIG_HASH', kind: 'variable' },
411
430
  { name: 'Gro_Config', kind: 'type' },
412
431
  { name: 'Raw_Gro_Config', kind: 'type' },
413
432
  { name: 'Create_Gro_Config', kind: 'type' },
@@ -820,16 +839,6 @@ export const src_json = {
820
839
  { name: 'validate_task_module', kind: 'function' },
821
840
  ],
822
841
  },
823
- './test_helpers.js': {
824
- path: 'test_helpers.ts',
825
- declarations: [
826
- { name: 'TEST_TIMEOUT_MD', kind: 'variable' },
827
- { name: 'SOME_PUBLIC_ENV_VAR_NAME', kind: 'variable' },
828
- { name: 'SOME_PUBLIC_ENV_VAR_VALUE', kind: 'variable' },
829
- { name: 'init_test_env', kind: 'function' },
830
- { name: 'create_ts_test_env', kind: 'function' },
831
- ],
832
- },
833
842
  './test.task.js': {
834
843
  path: 'test.task.ts',
835
844
  declarations: [
@@ -136,7 +136,7 @@ export const parse_repo_url = (package_json) => {
136
136
  return;
137
137
  }
138
138
  const [, owner, repo] = parsed_repo_url;
139
- return { owner, repo };
139
+ return { owner: owner, repo: repo };
140
140
  };
141
141
  /**
142
142
  * Parses a `Package_Json` object but preserves the order of the original keys.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryanatkn/gro",
3
- "version": "0.170.0",
3
+ "version": "0.171.0",
4
4
  "description": "task runner and toolkit extending SvelteKit",
5
5
  "motto": "generate, run, optimize",
6
6
  "glyph": "🌰",
@@ -61,7 +61,7 @@
61
61
  "zod": "^4.1.12"
62
62
  },
63
63
  "peerDependencies": {
64
- "@ryanatkn/belt": "^0.35.1",
64
+ "@ryanatkn/belt": "^0.36.0",
65
65
  "@sveltejs/kit": "^2",
66
66
  "esbuild": "^0.25",
67
67
  "svelte": "^5",
@@ -97,7 +97,7 @@
97
97
  "svelte-check": "^4.3.3",
98
98
  "typescript": "^5.9.3",
99
99
  "typescript-eslint": "^8.42.0",
100
- "vitest": "^4.0.0"
100
+ "vitest": "^4.0.3"
101
101
  },
102
102
  "prettier": {
103
103
  "plugins": [
@@ -1,8 +1,19 @@
1
1
  import {z} from 'zod';
2
+ import {styleText as st} from 'node:util';
3
+ import {git_check_clean_workspace, git_current_commit_hash} from '@ryanatkn/belt/git.js';
4
+ import {rmSync, existsSync} from 'node:fs';
5
+ import {join} from 'node:path';
2
6
 
3
- import type {Task} from './task.ts';
7
+ import {Task_Error, type Task} from './task.ts';
4
8
  import {Plugins} from './plugin.ts';
5
9
  import {clean_fs} from './clean_fs.ts';
10
+ import {
11
+ is_build_cache_valid,
12
+ create_build_cache_metadata,
13
+ save_build_cache_metadata,
14
+ discover_build_output_dirs,
15
+ } from './build_cache.ts';
16
+ import {paths} from './paths.ts';
6
17
 
7
18
  export const Args = z.strictObject({
8
19
  sync: z.boolean().meta({description: 'dual of no-sync'}).default(true),
@@ -12,29 +23,122 @@ export const Args = z.strictObject({
12
23
  .boolean()
13
24
  .meta({description: 'opt out of installing packages before building'})
14
25
  .default(false),
26
+ force_build: z
27
+ .boolean()
28
+ .meta({description: 'force a fresh build, ignoring the cache'})
29
+ .default(false),
15
30
  });
16
31
  export type Args = z.infer<typeof Args>;
17
32
 
33
+ /**
34
+ * Length of git commit hash when displayed in logs (standard git convention).
35
+ */
36
+ export const GIT_SHORT_HASH_LENGTH = 7;
37
+
38
+ /**
39
+ * Formats a git commit hash for display in logs.
40
+ * Returns '[none]' if hash is null (e.g., not in a git repo).
41
+ */
42
+ const format_commit_hash = (hash: string | null): string =>
43
+ hash?.slice(0, GIT_SHORT_HASH_LENGTH) ?? '[none]';
44
+
18
45
  export const task: Task<Args> = {
19
46
  summary: 'build the project',
20
47
  Args,
21
48
  run: async (ctx): Promise<void> => {
22
- const {args, invoke_task, log} = ctx;
23
- const {sync, install} = args;
49
+ const {args, invoke_task, log, config} = ctx;
50
+ const {sync, install, force_build} = args;
24
51
 
25
52
  if (sync || install) {
26
53
  if (!sync) log.warn('sync is false but install is true, so ignoring the sync option');
27
54
  await invoke_task('sync', {install});
28
55
  }
29
56
 
30
- // TODO possibly detect if the git workspace is clean, and ask for confirmation if not,
31
- // because we're not doing things like `gro gen` here because that's a dev/CI concern
57
+ // Batch git calls upfront for performance (spawning processes is expensive)
58
+ const [workspace_status, initial_commit] = await Promise.all([
59
+ git_check_clean_workspace(),
60
+ git_current_commit_hash(),
61
+ ]);
62
+ const workspace_dirty = !!workspace_status;
32
63
 
33
- await clean_fs({build_dist: true});
64
+ // Discover build output directories once to avoid redundant filesystem scans
65
+ let build_dirs: Array<string> | undefined;
66
+
67
+ // Check build cache unless force_build is set or workspace is dirty
68
+ if (!workspace_dirty && !force_build) {
69
+ const cache_valid = await is_build_cache_valid(config, log, initial_commit);
70
+ if (cache_valid) {
71
+ log.info(
72
+ st('cyan', 'skipping build, cache is valid'),
73
+ st('dim', '(use --force_build to rebuild)'),
74
+ );
75
+ return;
76
+ }
77
+ } else if (workspace_dirty) {
78
+ // IMPORTANT: When workspace is dirty, we delete cache AND all outputs to prevent stale state.
79
+ // Rationale: Uncommitted changes could be reverted, leaving cached outputs from reverted code.
80
+ // This conservative approach prioritizes safety over convenience during development.
81
+ const cache_path = join(paths.build, 'build.json');
82
+ if (existsSync(cache_path)) {
83
+ rmSync(cache_path, {force: true});
84
+ }
85
+
86
+ // Delete all build output directories
87
+ build_dirs = discover_build_output_dirs();
88
+ for (const dir of build_dirs) {
89
+ rmSync(dir, {recursive: true, force: true});
90
+ }
91
+
92
+ log.info(st('yellow', 'workspace has uncommitted changes - skipping build cache'));
93
+ // Skip clean_fs - already manually cleaned cache and all build outputs above
94
+ } else {
95
+ log.info(st('yellow', 'forcing fresh build, ignoring cache'));
96
+ }
97
+
98
+ // Clean build outputs (skip if workspace was dirty - already cleaned manually above)
99
+ if (!workspace_dirty) {
100
+ await clean_fs({build_dist: true});
101
+ }
34
102
 
35
103
  const plugins = await Plugins.create({...ctx, dev: false, watch: false});
36
104
  await plugins.setup();
37
105
  await plugins.adapt();
38
106
  await plugins.teardown();
107
+
108
+ // Verify workspace didn't become dirty during build
109
+ const final_workspace_status = await git_check_clean_workspace();
110
+ if (final_workspace_status !== workspace_status) {
111
+ // Workspace state changed during build - this indicates a problem
112
+ throw new Task_Error(
113
+ 'Build process modified tracked files or created untracked files.\n\n' +
114
+ 'Git status after build:\n' +
115
+ final_workspace_status +
116
+ '\n\n' +
117
+ 'Builds should only write to output directories (build/, dist/, etc.).\n' +
118
+ 'This usually indicates a plugin or build step is incorrectly modifying source files.',
119
+ );
120
+ }
121
+
122
+ // Save build cache metadata after successful build (only if workspace is clean)
123
+ if (!workspace_dirty) {
124
+ // Race condition protection: verify git commit didn't change during build
125
+ const current_commit = await git_current_commit_hash();
126
+
127
+ if (current_commit !== initial_commit) {
128
+ log.warn(
129
+ st('yellow', 'git commit changed during build'),
130
+ st(
131
+ 'dim',
132
+ `(${format_commit_hash(initial_commit)} → ${format_commit_hash(current_commit)})`,
133
+ ),
134
+ '- cache not saved',
135
+ );
136
+ } else {
137
+ // Commit is stable - safe to save cache
138
+ const metadata = await create_build_cache_metadata(config, log, initial_commit, build_dirs);
139
+ save_build_cache_metadata(metadata, log);
140
+ log.debug('Build cache metadata saved');
141
+ }
142
+ }
39
143
  },
40
144
  };