@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 +7 -0
- package/lib/cjs/template-sync.js +41 -0
- package/lib/esm/template-sync.js +41 -0
- package/package.json +1 -1
- package/src/template-sync.spec.ts +123 -30
- package/src/template-sync.ts +48 -0
- package/test-fixtures/downstream/plugins/custom-plugin.js +3 -0
- package/test-fixtures/downstream/plugins/fail-validate-plugin.js +14 -0
- package/test-fixtures/downstream/templatesync.json +1 -1
- package/test-fixtures/template/dummy-fail-plugin.js +8 -0
- package/test-fixtures/template/templatesync.json +1 -1
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
|
|
package/lib/cjs/template-sync.js
CHANGED
|
@@ -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/lib/esm/template-sync.js
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
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
|
-
}
|
package/src/template-sync.ts
CHANGED
|
@@ -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),
|