@hanseltime/template-repo-sync 2.2.1 → 2.3.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.3.0](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.2.1...v2.3.0) (2026-03-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * actually run plugin validate commands before mergin ([216ebbb](https://github.com/HanseltimeIndustries/template-repo-sync/commit/216ebbbf21f71f4480905fde336fbc0631b6aa7b))
7
+
1
8
  ## [2.2.1](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.2.0...v2.2.1) (2026-02-22)
2
9
 
3
10
 
@@ -35,6 +35,7 @@ const formatting_1 = require("./formatting");
35
35
  const commentJSON = __importStar(require("comment-json"));
36
36
  const checkout_drivers_1 = require("./checkout-drivers");
37
37
  const micromatch_1 = require("micromatch");
38
+ const load_plugin_1 = require("./load-plugin");
38
39
  exports.TEMPLATE_SYNC_CONFIG = "templatesync";
39
40
  exports.TEMPLATE_SYNC_LOCAL_CONFIG = "templatesync.local";
40
41
  async function templateSync(options) {
@@ -96,6 +97,46 @@ async function templateSync(options) {
96
97
  modified: [],
97
98
  };
98
99
  }
100
+ // Pre-load plugins and make sure the sync file is respected
101
+ // Synchronous since grpc servers take a second
102
+ const localValidateErrors = {};
103
+ const validateErrors = {};
104
+ for (const config of localTemplateSyncConfig.merge ?? []) {
105
+ const plugin = await (0, load_plugin_1.loadPlugin)(config, options.repoDir);
106
+ const errors = plugin.validate(config.options ?? {});
107
+ if (errors && errors.length > 0) {
108
+ localValidateErrors[config.plugin] = errors;
109
+ }
110
+ }
111
+ for (const config of templateSyncConfig.merge ?? []) {
112
+ const plugin = await (0, load_plugin_1.loadPlugin)(config, tempCloneDir);
113
+ const errors = plugin.validate(config.options ?? {});
114
+ if (errors && errors.length > 0) {
115
+ validateErrors[config.plugin] = errors;
116
+ }
117
+ }
118
+ let errorStr = "";
119
+ if (Object.keys(validateErrors).length > 0) {
120
+ errorStr = `${errorStr}templatesync.json plugin option errors:\n`;
121
+ for (const plugin in validateErrors) {
122
+ errorStr = `${errorStr}\tPlugin (${plugin}):\n`;
123
+ validateErrors[plugin].forEach((err) => {
124
+ errorStr = `${errorStr}\t\t${err}\n`;
125
+ });
126
+ }
127
+ }
128
+ if (Object.keys(localValidateErrors).length > 0) {
129
+ errorStr = `${errorStr}templatesync.local.json plugin option errors:\n`;
130
+ for (const plugin in localValidateErrors) {
131
+ errorStr = `${errorStr}\tPlugin (${plugin}):\n`;
132
+ localValidateErrors[plugin].forEach((err) => {
133
+ errorStr = `${errorStr}\t\t${err}\n`;
134
+ });
135
+ }
136
+ }
137
+ if (errorStr !== "") {
138
+ throw Error(errorStr);
139
+ }
99
140
  // Apply ignore filters
100
141
  filesToSync.added = filesToSync.added.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
101
142
  filesToSync.modified = filesToSync.modified.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
@@ -35,6 +35,7 @@ const formatting_1 = require("./formatting");
35
35
  const commentJSON = __importStar(require("comment-json"));
36
36
  const checkout_drivers_1 = require("./checkout-drivers");
37
37
  const micromatch_1 = require("micromatch");
38
+ const load_plugin_1 = require("./load-plugin");
38
39
  exports.TEMPLATE_SYNC_CONFIG = "templatesync";
39
40
  exports.TEMPLATE_SYNC_LOCAL_CONFIG = "templatesync.local";
40
41
  async function templateSync(options) {
@@ -96,6 +97,46 @@ async function templateSync(options) {
96
97
  modified: [],
97
98
  };
98
99
  }
100
+ // Pre-load plugins and make sure the sync file is respected
101
+ // Synchronous since grpc servers take a second
102
+ const localValidateErrors = {};
103
+ const validateErrors = {};
104
+ for (const config of localTemplateSyncConfig.merge ?? []) {
105
+ const plugin = await (0, load_plugin_1.loadPlugin)(config, options.repoDir);
106
+ const errors = plugin.validate(config.options ?? {});
107
+ if (errors && errors.length > 0) {
108
+ localValidateErrors[config.plugin] = errors;
109
+ }
110
+ }
111
+ for (const config of templateSyncConfig.merge ?? []) {
112
+ const plugin = await (0, load_plugin_1.loadPlugin)(config, tempCloneDir);
113
+ const errors = plugin.validate(config.options ?? {});
114
+ if (errors && errors.length > 0) {
115
+ validateErrors[config.plugin] = errors;
116
+ }
117
+ }
118
+ let errorStr = "";
119
+ if (Object.keys(validateErrors).length > 0) {
120
+ errorStr = `${errorStr}templatesync.json plugin option errors:\n`;
121
+ for (const plugin in validateErrors) {
122
+ errorStr = `${errorStr}\tPlugin (${plugin}):\n`;
123
+ validateErrors[plugin].forEach((err) => {
124
+ errorStr = `${errorStr}\t\t${err}\n`;
125
+ });
126
+ }
127
+ }
128
+ if (Object.keys(localValidateErrors).length > 0) {
129
+ errorStr = `${errorStr}templatesync.local.json plugin option errors:\n`;
130
+ for (const plugin in localValidateErrors) {
131
+ errorStr = `${errorStr}\tPlugin (${plugin}):\n`;
132
+ localValidateErrors[plugin].forEach((err) => {
133
+ errorStr = `${errorStr}\t\t${err}\n`;
134
+ });
135
+ }
136
+ }
137
+ if (errorStr !== "") {
138
+ throw Error(errorStr);
139
+ }
99
140
  // Apply ignore filters
100
141
  filesToSync.added = filesToSync.added.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
101
142
  filesToSync.modified = filesToSync.modified.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanseltime/template-repo-sync",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "An npm library that enables pluggable, customizable synchronization between template repos",
5
5
  "main": "lib/cjs/index.js",
6
6
  "types": "lib/cjs/index.d.ts",
@@ -5,24 +5,27 @@ import { tempDir, TEST_FIXTURES_DIR } from "./test-utils";
5
5
  import { join, resolve } from "path";
6
6
  import { existsSync, readFileSync, writeFileSync } from "fs";
7
7
 
8
- // Just return the test-fixture directory
9
- const dummyCloneDriver = async () => {
10
- return {
11
- dir: resolve(TEST_FIXTURES_DIR, "template"),
12
- remoteName: "ourRemote",
13
- };
14
- };
15
-
16
8
  const dummyCheckoutDriver = jest.fn();
9
+ const dummyCurrentRefDriver = jest.fn();
17
10
 
18
11
  const downstreamDir = resolve(TEST_FIXTURES_DIR, "downstream");
19
12
 
20
13
  describe("templateSync", () => {
21
14
  let tmpDir: string;
15
+ let templateDir: string;
16
+ let dummyCloneDriver: () => Promise<{ dir: string; remoteName: string }>;
22
17
  beforeEach(async () => {
23
18
  jest.resetAllMocks();
24
19
  tmpDir = await mkdtemp(tempDir());
20
+ templateDir = await mkdtemp(tempDir());
21
+ await copy(resolve(TEST_FIXTURES_DIR, "template"), templateDir);
25
22
  await copy(downstreamDir, tmpDir);
23
+ dummyCloneDriver = async () => {
24
+ return {
25
+ dir: templateDir,
26
+ remoteName: "ourRemote",
27
+ };
28
+ };
26
29
  });
27
30
  afterEach(async () => {
28
31
  await rm(tmpDir, {
@@ -30,6 +33,93 @@ describe("templateSync", () => {
30
33
  recursive: true,
31
34
  });
32
35
  });
36
+ // Note: for this test, we expect actions and users to handle misconfigured template syncs
37
+ it.each([
38
+ [
39
+ "local",
40
+ `templatesync.local.json plugin option errors:
41
+ \tPlugin (plugins/fail-validate-plugin.js):
42
+ \t\toh no!
43
+ \t\tnot this one too!
44
+ `,
45
+ ],
46
+ [
47
+ "template",
48
+ `templatesync.json plugin option errors:
49
+ \tPlugin (dummy-fail-plugin.js):
50
+ \t\tshucks
51
+ \t\tno good
52
+ `,
53
+ ],
54
+ [
55
+ "both",
56
+ `templatesync.json plugin option errors:
57
+ \tPlugin (dummy-fail-plugin.js):
58
+ \t\tshucks
59
+ \t\tno good
60
+ templatesync.local.json plugin option errors:
61
+ \tPlugin (plugins/fail-validate-plugin.js):
62
+ \t\toh no!
63
+ \t\tnot this one too!
64
+ `,
65
+ ],
66
+ ])("throws errors from plugin validation", async (mode, expected) => {
67
+ // Remove the local sync overrides
68
+ await rm(join(tmpDir, "templatesync.local.json"));
69
+
70
+ if (mode === "local" || mode == "both") {
71
+ writeFileSync(
72
+ join(tmpDir, "templatesync.local.json"),
73
+ JSON.stringify({
74
+ ignore: [
75
+ // Ignores the templated.ts
76
+ "**/*.ts",
77
+ // We don't have a need for this in here, but it's an example of keeping things cleaner for our custom plugins
78
+ "plugins/**",
79
+ ],
80
+ merge: [
81
+ {
82
+ glob: "package.json",
83
+ plugin: "plugins/fail-validate-plugin.js",
84
+ options: {},
85
+ },
86
+ ],
87
+ }),
88
+ );
89
+ }
90
+ if (mode === "template" || mode === "both") {
91
+ writeFileSync(
92
+ join(templateDir, "templatesync.json"),
93
+ JSON.stringify({
94
+ ignore: [
95
+ // Ignores the templated.ts
96
+ "**/*.ts",
97
+ // We don't have a need for this in here, but it's an example of keeping things cleaner for our custom plugins
98
+ "plugins/**",
99
+ ],
100
+ merge: [
101
+ {
102
+ glob: "package.json",
103
+ plugin: "dummy-fail-plugin.js",
104
+ options: {},
105
+ },
106
+ ],
107
+ }),
108
+ );
109
+ }
110
+
111
+ await expect(
112
+ async () =>
113
+ await templateSync({
114
+ tmpCloneDir: "stubbed-by-driver",
115
+ cloneDriver: dummyCloneDriver,
116
+ repoUrl: "not-important",
117
+ repoDir: tmpDir,
118
+ checkoutDriver: dummyCheckoutDriver,
119
+ currentRefDriver: dummyCurrentRefDriver,
120
+ }),
121
+ ).rejects.toThrow(expected);
122
+ });
33
123
  it("appropriately merges according to just the templatesync config file into an empty dir", async () => {
34
124
  const emptyTmpDir = await mkdtemp(tempDir());
35
125
  expect(
@@ -39,6 +129,7 @@ describe("templateSync", () => {
39
129
  repoUrl: "not-important",
40
130
  repoDir: emptyTmpDir,
41
131
  checkoutDriver: dummyCheckoutDriver,
132
+ currentRefDriver: dummyCurrentRefDriver,
42
133
  }),
43
134
  ).toEqual({
44
135
  // Expect no changes since there was no local sync file
@@ -79,6 +170,7 @@ describe("templateSync", () => {
79
170
  repoDir: emptyTmpDir,
80
171
  branch: "new-template-test",
81
172
  checkoutDriver: dummyCheckoutDriver,
173
+ currentRefDriver: dummyCurrentRefDriver,
82
174
  }),
83
175
  ).toEqual({
84
176
  // Expect no changes since there was no local sync file
@@ -123,6 +215,7 @@ describe("templateSync", () => {
123
215
  repoUrl: "not-important",
124
216
  repoDir: tmpDir,
125
217
  checkoutDriver: dummyCheckoutDriver,
218
+ currentRefDriver: dummyCurrentRefDriver,
126
219
  });
127
220
 
128
221
  expect(result.localSkipFiles).toEqual([]);
@@ -190,6 +283,7 @@ describe("templateSync", () => {
190
283
  repoUrl: "not-important",
191
284
  repoDir: tmpDir,
192
285
  checkoutDriver: dummyCheckoutDriver,
286
+ currentRefDriver: dummyCurrentRefDriver,
193
287
  });
194
288
 
195
289
  expect(result.localSkipFiles).toEqual(["src/templated.ts"]);
@@ -256,6 +350,7 @@ describe("templateSync", () => {
256
350
  repoUrl: "not-important",
257
351
  repoDir: tmpDir,
258
352
  diffDriver: mockDiffDriver,
353
+ currentRefDriver: dummyCurrentRefDriver,
259
354
  checkoutDriver: dummyCheckoutDriver,
260
355
  });
261
356
 
@@ -484,26 +579,24 @@ describe("templateSync", () => {
484
579
  afterRef: "newestSha",
485
580
  });
486
581
  });
487
- expect(dummyCheckoutDriver).not.toHaveBeenCalled();
582
+ // helper
583
+ async function fileMatchTemplate(tmpDir: string, relPath: string) {
584
+ return fileMatch(tmpDir, relPath, "template");
585
+ }
586
+
587
+ async function fileMatchDownstream(tmpDir: string, relPath: string) {
588
+ return fileMatch(tmpDir, relPath, "downstream");
589
+ }
590
+
591
+ async function fileMatch(
592
+ tmpDir: string,
593
+ relPath: string,
594
+ source: "downstream" | "template",
595
+ ) {
596
+ const dir =
597
+ source === "downstream" ? downstreamDir : (await dummyCloneDriver()).dir;
598
+ expect((await readFile(resolve(tmpDir, relPath))).toString()).toEqual(
599
+ (await readFile(resolve(dir, relPath))).toString(),
600
+ );
601
+ }
488
602
  });
489
-
490
- // helper
491
- async function fileMatchTemplate(tmpDir: string, relPath: string) {
492
- return fileMatch(tmpDir, relPath, "template");
493
- }
494
-
495
- async function fileMatchDownstream(tmpDir: string, relPath: string) {
496
- return fileMatch(tmpDir, relPath, "downstream");
497
- }
498
-
499
- async function fileMatch(
500
- tmpDir: string,
501
- relPath: string,
502
- source: "downstream" | "template",
503
- ) {
504
- const dir =
505
- source === "downstream" ? downstreamDir : (await dummyCloneDriver()).dir;
506
- expect((await readFile(resolve(tmpDir, relPath))).toString()).toEqual(
507
- (await readFile(resolve(dir, relPath))).toString(),
508
- );
509
- }
@@ -13,6 +13,7 @@ import { inferJSONIndent } from "./formatting";
13
13
  import * as commentJSON from "comment-json";
14
14
  import { TemplateCheckoutDriverFn, gitCheckout } from "./checkout-drivers";
15
15
  import { some } from "micromatch";
16
+ import { loadPlugin } from "./load-plugin";
16
17
 
17
18
  export interface TemplateSyncOptions {
18
19
  /**
@@ -166,6 +167,53 @@ export async function templateSync(
166
167
  };
167
168
  }
168
169
 
170
+ // Pre-load plugins and make sure the sync file is respected
171
+ // Synchronous since grpc servers take a second
172
+ const localValidateErrors: {
173
+ [k: string]: string[];
174
+ } = {};
175
+ const validateErrors: {
176
+ [k: string]: string[];
177
+ } = {};
178
+ for (const config of localTemplateSyncConfig.merge ?? []) {
179
+ const plugin = await loadPlugin(config, options.repoDir);
180
+ const errors = plugin.validate(config.options ?? {});
181
+ if (errors && errors.length > 0) {
182
+ localValidateErrors[config.plugin] = errors;
183
+ }
184
+ }
185
+ for (const config of templateSyncConfig.merge ?? []) {
186
+ const plugin = await loadPlugin(config, tempCloneDir);
187
+ const errors = plugin.validate(config.options ?? {});
188
+ if (errors && errors.length > 0) {
189
+ validateErrors[config.plugin] = errors;
190
+ }
191
+ }
192
+
193
+ let errorStr = "";
194
+ if (Object.keys(validateErrors).length > 0) {
195
+ errorStr = `${errorStr}templatesync.json plugin option errors:\n`;
196
+ for (const plugin in validateErrors) {
197
+ errorStr = `${errorStr}\tPlugin (${plugin}):\n`;
198
+ validateErrors[plugin].forEach((err) => {
199
+ errorStr = `${errorStr}\t\t${err}\n`;
200
+ });
201
+ }
202
+ }
203
+ if (Object.keys(localValidateErrors).length > 0) {
204
+ errorStr = `${errorStr}templatesync.local.json plugin option errors:\n`;
205
+ for (const plugin in localValidateErrors) {
206
+ errorStr = `${errorStr}\tPlugin (${plugin}):\n`;
207
+ localValidateErrors[plugin].forEach((err) => {
208
+ errorStr = `${errorStr}\t\t${err}\n`;
209
+ });
210
+ }
211
+ }
212
+
213
+ if (errorStr !== "") {
214
+ throw Error(errorStr);
215
+ }
216
+
169
217
  // Apply ignore filters
170
218
  filesToSync.added = filesToSync.added.filter(
171
219
  (f) => !some(f, templateSyncConfig.ignore),
@@ -8,4 +8,7 @@ module.exports = {
8
8
  4,
9
9
  );
10
10
  },
11
+ validate: () => {
12
+ return [];
13
+ },
11
14
  };
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ merge: () => {
3
+ return JSON.stringify(
4
+ {
5
+ downstream: true,
6
+ },
7
+ null,
8
+ 4,
9
+ );
10
+ },
11
+ validate: () => {
12
+ return ["oh no!", "not this one too!"];
13
+ },
14
+ };
@@ -1,5 +1,5 @@
1
1
  {
2
- "ignore": ["src/!(templated).ts", "custom-bin/**"],
2
+ "ignore": ["src/!(templated).ts", "custom-bin/**", "dummy-fail-plugin.js"],
3
3
  "merge": [
4
4
  {
5
5
  "glob": "package.json",
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ merge: () => {
3
+ return JSON.stringify({ tested: true }, null, 4);
4
+ },
5
+ validate: () => {
6
+ return ["shucks", "no good"];
7
+ },
8
+ };
@@ -1,5 +1,5 @@
1
1
  {
2
- "ignore": ["src/!(templated).ts", "custom-bin/**"],
2
+ "ignore": ["src/!(templated).ts", "custom-bin/**", "dummy-fail-plugin.js"],
3
3
  "merge": [
4
4
  {
5
5
  "glob": "package.json",