@hanseltime/template-repo-sync 2.2.0 → 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 +14 -0
- package/lib/cjs/template-sync.js +67 -3
- package/lib/esm/template-sync.js +67 -3
- package/package.json +1 -1
- package/src/template-sync.spec.ts +210 -31
- package/src/template-sync.ts +75 -6
- 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,17 @@
|
|
|
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
|
+
|
|
8
|
+
## [2.2.1](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.2.0...v2.2.1) (2026-02-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* template sync afterRef updates ([21dd335](https://github.com/HanseltimeIndustries/template-repo-sync/commit/21dd335045034d132ad5f6ab7944a81c82b96981))
|
|
14
|
+
|
|
1
15
|
# [2.2.0](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.1.2...v2.2.0) (2026-02-21)
|
|
2
16
|
|
|
3
17
|
|
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) {
|
|
@@ -61,12 +62,29 @@ async function templateSync(options) {
|
|
|
61
62
|
const templateSyncConfig = (0, fs_1.existsSync)(cloneConfigPath)
|
|
62
63
|
? commentJSON.parse((0, fs_1.readFileSync)(cloneConfigPath).toString())
|
|
63
64
|
: { ignore: [] };
|
|
64
|
-
const
|
|
65
|
+
const localConfigFile = `${exports.TEMPLATE_SYNC_LOCAL_CONFIG}.json`;
|
|
66
|
+
const localConfigPath = (0, path_1.join)(options.repoDir, localConfigFile);
|
|
65
67
|
const localTemplateSyncConfig = (0, fs_1.existsSync)(localConfigPath)
|
|
66
68
|
? commentJSON.parse((0, fs_1.readFileSync)(localConfigPath).toString())
|
|
67
69
|
: { ignore: [] };
|
|
68
70
|
let filesToSync;
|
|
71
|
+
const ref = await currentRefDriver({
|
|
72
|
+
rootDir: tempCloneDir,
|
|
73
|
+
});
|
|
69
74
|
if (localTemplateSyncConfig.afterRef) {
|
|
75
|
+
if (ref === localTemplateSyncConfig.afterRef) {
|
|
76
|
+
// short circuit if the refs match
|
|
77
|
+
return {
|
|
78
|
+
localSkipFiles: [],
|
|
79
|
+
localFileChanges: {},
|
|
80
|
+
modifiedFiles: {
|
|
81
|
+
added: [],
|
|
82
|
+
modified: [],
|
|
83
|
+
deleted: [],
|
|
84
|
+
total: 0,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
70
88
|
filesToSync = await diffDriver(tempCloneDir, localTemplateSyncConfig.afterRef);
|
|
71
89
|
}
|
|
72
90
|
else {
|
|
@@ -79,6 +97,46 @@ async function templateSync(options) {
|
|
|
79
97
|
modified: [],
|
|
80
98
|
};
|
|
81
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
|
+
}
|
|
82
140
|
// Apply ignore filters
|
|
83
141
|
filesToSync.added = filesToSync.added.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
|
|
84
142
|
filesToSync.modified = filesToSync.modified.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
|
|
@@ -114,7 +172,6 @@ async function templateSync(options) {
|
|
|
114
172
|
added: actualAdded,
|
|
115
173
|
modified: actualModified,
|
|
116
174
|
deleted: actualDeleted,
|
|
117
|
-
total: actualAdded.length + actualModified.length + actualDeleted.length,
|
|
118
175
|
};
|
|
119
176
|
// apply after ref
|
|
120
177
|
if (options.updateAfterRef) {
|
|
@@ -126,15 +183,22 @@ async function templateSync(options) {
|
|
|
126
183
|
const config = commentJSON.parse(configStr);
|
|
127
184
|
config.afterRef = ref;
|
|
128
185
|
(0, fs_1.writeFileSync)(localConfigPath, commentJSON.stringify(config, null, (0, formatting_1.inferJSONIndent)(configStr)));
|
|
186
|
+
modifiedFiles.modified.push(localConfigFile);
|
|
129
187
|
}
|
|
130
188
|
else {
|
|
131
189
|
(0, fs_1.writeFileSync)(localConfigPath, commentJSON.stringify({ afterRef: ref }, null, 4));
|
|
190
|
+
modifiedFiles.added.push(localConfigFile);
|
|
132
191
|
}
|
|
133
192
|
}
|
|
134
193
|
return {
|
|
135
194
|
localSkipFiles: Array.from(localSkipFiles),
|
|
136
195
|
localFileChanges,
|
|
137
|
-
modifiedFiles:
|
|
196
|
+
modifiedFiles: {
|
|
197
|
+
...modifiedFiles,
|
|
198
|
+
total: modifiedFiles.added.length +
|
|
199
|
+
modifiedFiles.deleted.length +
|
|
200
|
+
modifiedFiles.modified.length,
|
|
201
|
+
},
|
|
138
202
|
};
|
|
139
203
|
}
|
|
140
204
|
exports.templateSync = templateSync;
|
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) {
|
|
@@ -61,12 +62,29 @@ async function templateSync(options) {
|
|
|
61
62
|
const templateSyncConfig = (0, fs_1.existsSync)(cloneConfigPath)
|
|
62
63
|
? commentJSON.parse((0, fs_1.readFileSync)(cloneConfigPath).toString())
|
|
63
64
|
: { ignore: [] };
|
|
64
|
-
const
|
|
65
|
+
const localConfigFile = `${exports.TEMPLATE_SYNC_LOCAL_CONFIG}.json`;
|
|
66
|
+
const localConfigPath = (0, path_1.join)(options.repoDir, localConfigFile);
|
|
65
67
|
const localTemplateSyncConfig = (0, fs_1.existsSync)(localConfigPath)
|
|
66
68
|
? commentJSON.parse((0, fs_1.readFileSync)(localConfigPath).toString())
|
|
67
69
|
: { ignore: [] };
|
|
68
70
|
let filesToSync;
|
|
71
|
+
const ref = await currentRefDriver({
|
|
72
|
+
rootDir: tempCloneDir,
|
|
73
|
+
});
|
|
69
74
|
if (localTemplateSyncConfig.afterRef) {
|
|
75
|
+
if (ref === localTemplateSyncConfig.afterRef) {
|
|
76
|
+
// short circuit if the refs match
|
|
77
|
+
return {
|
|
78
|
+
localSkipFiles: [],
|
|
79
|
+
localFileChanges: {},
|
|
80
|
+
modifiedFiles: {
|
|
81
|
+
added: [],
|
|
82
|
+
modified: [],
|
|
83
|
+
deleted: [],
|
|
84
|
+
total: 0,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
70
88
|
filesToSync = await diffDriver(tempCloneDir, localTemplateSyncConfig.afterRef);
|
|
71
89
|
}
|
|
72
90
|
else {
|
|
@@ -79,6 +97,46 @@ async function templateSync(options) {
|
|
|
79
97
|
modified: [],
|
|
80
98
|
};
|
|
81
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
|
+
}
|
|
82
140
|
// Apply ignore filters
|
|
83
141
|
filesToSync.added = filesToSync.added.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
|
|
84
142
|
filesToSync.modified = filesToSync.modified.filter((f) => !(0, micromatch_1.some)(f, templateSyncConfig.ignore));
|
|
@@ -114,7 +172,6 @@ async function templateSync(options) {
|
|
|
114
172
|
added: actualAdded,
|
|
115
173
|
modified: actualModified,
|
|
116
174
|
deleted: actualDeleted,
|
|
117
|
-
total: actualAdded.length + actualModified.length + actualDeleted.length,
|
|
118
175
|
};
|
|
119
176
|
// apply after ref
|
|
120
177
|
if (options.updateAfterRef) {
|
|
@@ -126,15 +183,22 @@ async function templateSync(options) {
|
|
|
126
183
|
const config = commentJSON.parse(configStr);
|
|
127
184
|
config.afterRef = ref;
|
|
128
185
|
(0, fs_1.writeFileSync)(localConfigPath, commentJSON.stringify(config, null, (0, formatting_1.inferJSONIndent)(configStr)));
|
|
186
|
+
modifiedFiles.modified.push(localConfigFile);
|
|
129
187
|
}
|
|
130
188
|
else {
|
|
131
189
|
(0, fs_1.writeFileSync)(localConfigPath, commentJSON.stringify({ afterRef: ref }, null, 4));
|
|
190
|
+
modifiedFiles.added.push(localConfigFile);
|
|
132
191
|
}
|
|
133
192
|
}
|
|
134
193
|
return {
|
|
135
194
|
localSkipFiles: Array.from(localSkipFiles),
|
|
136
195
|
localFileChanges,
|
|
137
|
-
modifiedFiles:
|
|
196
|
+
modifiedFiles: {
|
|
197
|
+
...modifiedFiles,
|
|
198
|
+
total: modifiedFiles.added.length +
|
|
199
|
+
modifiedFiles.deleted.length +
|
|
200
|
+
modifiedFiles.modified.length,
|
|
201
|
+
},
|
|
138
202
|
};
|
|
139
203
|
}
|
|
140
204
|
exports.templateSync = templateSync;
|
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
|
|
|
@@ -292,7 +387,7 @@ describe("templateSync", () => {
|
|
|
292
387
|
// We will only update the templated.ts
|
|
293
388
|
const mockDiffDriver = jest.fn().mockImplementation(async () => ({
|
|
294
389
|
added: ["src/templated.ts"],
|
|
295
|
-
modified: ["src/index.ts"], // Add index.ts so we make sure it is still ignored -
|
|
390
|
+
modified: ["src/index.ts"], // Add index.ts so we make sure it is still ignored - see test-fixtures/template/templatesync.json ignores
|
|
296
391
|
deleted: [],
|
|
297
392
|
}));
|
|
298
393
|
const mockCurrentRefDriver = jest
|
|
@@ -311,6 +406,12 @@ describe("templateSync", () => {
|
|
|
311
406
|
|
|
312
407
|
// since there was no override for this file, no changes from the local file
|
|
313
408
|
expect(result.localFileChanges).toEqual(expect.objectContaining({}));
|
|
409
|
+
expect(result.modifiedFiles).toEqual({
|
|
410
|
+
added: ["src/templated.ts"],
|
|
411
|
+
modified: ["templatesync.local.json"], // Add index.ts so we make sure it is still ignored - due to a bug
|
|
412
|
+
deleted: [],
|
|
413
|
+
total: 2,
|
|
414
|
+
});
|
|
314
415
|
|
|
315
416
|
// Verify the files
|
|
316
417
|
await fileMatchTemplate(tmpDir, "templatesync.json");
|
|
@@ -332,6 +433,73 @@ describe("templateSync", () => {
|
|
|
332
433
|
});
|
|
333
434
|
expect(dummyCheckoutDriver).not.toHaveBeenCalled();
|
|
334
435
|
});
|
|
436
|
+
it("Does not update the local templatesync if updateAfterRef is true and the ref is the same", async () => {
|
|
437
|
+
// Remove the local sync overrides
|
|
438
|
+
await rm(join(tmpDir, "templatesync.local.json"));
|
|
439
|
+
|
|
440
|
+
const mockLocalConfig = {
|
|
441
|
+
afterRef: "dummySha",
|
|
442
|
+
ignore: [
|
|
443
|
+
// We don't have a need for this in here, but it's an example of keeping things cleaner for our custom plugins
|
|
444
|
+
"plugins/**",
|
|
445
|
+
],
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
writeFileSync(
|
|
449
|
+
join(tmpDir, "templatesync.local.json"),
|
|
450
|
+
JSON.stringify(mockLocalConfig),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// We will only update the templated.ts
|
|
454
|
+
const mockDiffDriver = jest.fn().mockImplementation(async () => ({
|
|
455
|
+
added: ["src/templated.ts"],
|
|
456
|
+
modified: ["src/index.ts"], // Add index.ts so we make sure it is still ignored - see test-fixtures/template/templatesync.json ignores
|
|
457
|
+
deleted: [],
|
|
458
|
+
}));
|
|
459
|
+
const mockCurrentRefDriver = jest
|
|
460
|
+
.fn()
|
|
461
|
+
.mockImplementation(async () => "dummySha");
|
|
462
|
+
const result = await templateSync({
|
|
463
|
+
tmpCloneDir: "stubbed-by-driver",
|
|
464
|
+
cloneDriver: dummyCloneDriver,
|
|
465
|
+
repoUrl: "not-important",
|
|
466
|
+
repoDir: tmpDir,
|
|
467
|
+
updateAfterRef: true,
|
|
468
|
+
diffDriver: mockDiffDriver,
|
|
469
|
+
currentRefDriver: mockCurrentRefDriver,
|
|
470
|
+
checkoutDriver: dummyCheckoutDriver,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Nothing shoudl be reported as changing
|
|
474
|
+
expect(result).toEqual({
|
|
475
|
+
localFileChanges: {},
|
|
476
|
+
localSkipFiles: [],
|
|
477
|
+
modifiedFiles: {
|
|
478
|
+
added: [],
|
|
479
|
+
modified: [],
|
|
480
|
+
deleted: [],
|
|
481
|
+
total: 0,
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
// Verify the files
|
|
485
|
+
await fileMatchDownstream(tmpDir, "templatesync.json");
|
|
486
|
+
await fileMatchDownstream(tmpDir, "src/templated.ts");
|
|
487
|
+
|
|
488
|
+
// Expect the none of the diff files to work
|
|
489
|
+
await fileMatchDownstream(tmpDir, "src/index.ts");
|
|
490
|
+
await fileMatchDownstream(tmpDir, "plugins/custom-plugin.js");
|
|
491
|
+
await fileMatchDownstream(tmpDir, "package.json");
|
|
492
|
+
|
|
493
|
+
// Ensure we have updated the local template field
|
|
494
|
+
expect(
|
|
495
|
+
JSON.parse(
|
|
496
|
+
(await readFile(join(tmpDir, "templatesync.local.json"))).toString(),
|
|
497
|
+
),
|
|
498
|
+
).toEqual({
|
|
499
|
+
...mockLocalConfig,
|
|
500
|
+
});
|
|
501
|
+
expect(dummyCheckoutDriver).not.toHaveBeenCalled();
|
|
502
|
+
});
|
|
335
503
|
it("creates the local templatesync with the current ref if updateAfterRef is true and no local template exists", async () => {
|
|
336
504
|
// Remove the local sync overrides
|
|
337
505
|
await rm(join(tmpDir, "templatesync.local.json"));
|
|
@@ -356,6 +524,19 @@ describe("templateSync", () => {
|
|
|
356
524
|
|
|
357
525
|
// since there was no override for this file, no changes from the local file
|
|
358
526
|
expect(result.localFileChanges).toEqual(expect.objectContaining({}));
|
|
527
|
+
expect(result.modifiedFiles).toEqual({
|
|
528
|
+
added: [
|
|
529
|
+
"package.json",
|
|
530
|
+
"src/index.js",
|
|
531
|
+
"src/templated.js",
|
|
532
|
+
"src/templated.ts",
|
|
533
|
+
"templatesync.json",
|
|
534
|
+
"templatesync.local.json",
|
|
535
|
+
],
|
|
536
|
+
deleted: [],
|
|
537
|
+
modified: [],
|
|
538
|
+
total: 6,
|
|
539
|
+
});
|
|
359
540
|
|
|
360
541
|
// Verify the files
|
|
361
542
|
await fileMatchTemplate(tmpDir, "templatesync.json");
|
|
@@ -398,26 +579,24 @@ describe("templateSync", () => {
|
|
|
398
579
|
afterRef: "newestSha",
|
|
399
580
|
});
|
|
400
581
|
});
|
|
401
|
-
|
|
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
|
+
}
|
|
402
602
|
});
|
|
403
|
-
|
|
404
|
-
// helper
|
|
405
|
-
async function fileMatchTemplate(tmpDir: string, relPath: string) {
|
|
406
|
-
return fileMatch(tmpDir, relPath, "template");
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function fileMatchDownstream(tmpDir: string, relPath: string) {
|
|
410
|
-
return fileMatch(tmpDir, relPath, "downstream");
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function fileMatch(
|
|
414
|
-
tmpDir: string,
|
|
415
|
-
relPath: string,
|
|
416
|
-
source: "downstream" | "template",
|
|
417
|
-
) {
|
|
418
|
-
const dir =
|
|
419
|
-
source === "downstream" ? downstreamDir : (await dummyCloneDriver()).dir;
|
|
420
|
-
expect((await readFile(resolve(tmpDir, relPath))).toString()).toEqual(
|
|
421
|
-
(await readFile(resolve(dir, relPath))).toString(),
|
|
422
|
-
);
|
|
423
|
-
}
|
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
|
/**
|
|
@@ -125,10 +126,8 @@ export async function templateSync(
|
|
|
125
126
|
) as unknown as Config)
|
|
126
127
|
: { ignore: [] };
|
|
127
128
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
`${TEMPLATE_SYNC_LOCAL_CONFIG}.json`,
|
|
131
|
-
);
|
|
129
|
+
const localConfigFile = `${TEMPLATE_SYNC_LOCAL_CONFIG}.json`;
|
|
130
|
+
const localConfigPath = join(options.repoDir, localConfigFile);
|
|
132
131
|
const localTemplateSyncConfig: LocalConfig = existsSync(localConfigPath)
|
|
133
132
|
? (commentJSON.parse(
|
|
134
133
|
readFileSync(localConfigPath).toString(),
|
|
@@ -136,7 +135,23 @@ export async function templateSync(
|
|
|
136
135
|
: { ignore: [] };
|
|
137
136
|
|
|
138
137
|
let filesToSync: DiffResult;
|
|
138
|
+
const ref = await currentRefDriver({
|
|
139
|
+
rootDir: tempCloneDir,
|
|
140
|
+
});
|
|
139
141
|
if (localTemplateSyncConfig.afterRef) {
|
|
142
|
+
if (ref === localTemplateSyncConfig.afterRef) {
|
|
143
|
+
// short circuit if the refs match
|
|
144
|
+
return {
|
|
145
|
+
localSkipFiles: [],
|
|
146
|
+
localFileChanges: {},
|
|
147
|
+
modifiedFiles: {
|
|
148
|
+
added: [],
|
|
149
|
+
modified: [],
|
|
150
|
+
deleted: [],
|
|
151
|
+
total: 0,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
140
155
|
filesToSync = await diffDriver(
|
|
141
156
|
tempCloneDir,
|
|
142
157
|
localTemplateSyncConfig.afterRef,
|
|
@@ -152,6 +167,53 @@ export async function templateSync(
|
|
|
152
167
|
};
|
|
153
168
|
}
|
|
154
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
|
+
|
|
155
217
|
// Apply ignore filters
|
|
156
218
|
filesToSync.added = filesToSync.added.filter(
|
|
157
219
|
(f) => !some(f, templateSyncConfig.ignore),
|
|
@@ -202,7 +264,6 @@ export async function templateSync(
|
|
|
202
264
|
added: actualAdded,
|
|
203
265
|
modified: actualModified,
|
|
204
266
|
deleted: actualDeleted,
|
|
205
|
-
total: actualAdded.length + actualModified.length + actualDeleted.length,
|
|
206
267
|
};
|
|
207
268
|
|
|
208
269
|
// apply after ref
|
|
@@ -219,17 +280,25 @@ export async function templateSync(
|
|
|
219
280
|
localConfigPath,
|
|
220
281
|
commentJSON.stringify(config, null, inferJSONIndent(configStr)),
|
|
221
282
|
);
|
|
283
|
+
modifiedFiles.modified.push(localConfigFile);
|
|
222
284
|
} else {
|
|
223
285
|
writeFileSync(
|
|
224
286
|
localConfigPath,
|
|
225
287
|
commentJSON.stringify({ afterRef: ref }, null, 4),
|
|
226
288
|
);
|
|
289
|
+
modifiedFiles.added.push(localConfigFile);
|
|
227
290
|
}
|
|
228
291
|
}
|
|
229
292
|
|
|
230
293
|
return {
|
|
231
294
|
localSkipFiles: Array.from(localSkipFiles),
|
|
232
295
|
localFileChanges,
|
|
233
|
-
modifiedFiles:
|
|
296
|
+
modifiedFiles: {
|
|
297
|
+
...modifiedFiles,
|
|
298
|
+
total:
|
|
299
|
+
modifiedFiles.added.length +
|
|
300
|
+
modifiedFiles.deleted.length +
|
|
301
|
+
modifiedFiles.modified.length,
|
|
302
|
+
},
|
|
234
303
|
};
|
|
235
304
|
}
|