@hanseltime/template-repo-sync 1.0.1

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 (119) hide show
  1. package/.eslintrc.js +10 -0
  2. package/.github/CODEOWNERS +6 -0
  3. package/.github/workflows/pr-checks.yaml +12 -0
  4. package/.github/workflows/release.yaml +36 -0
  5. package/.github/workflows/test-flow.yaml +58 -0
  6. package/.husky/commit-msg +4 -0
  7. package/.prettierignore +1 -0
  8. package/.prettierrc +1 -0
  9. package/CHANGELOG.md +27 -0
  10. package/README.md +93 -0
  11. package/action.yml +13 -0
  12. package/commitlint.config.js +3 -0
  13. package/jest.config.js +19 -0
  14. package/lib/cjs/clone-drivers/git-clone.d.ts +1 -0
  15. package/lib/cjs/clone-drivers/git-clone.js +14 -0
  16. package/lib/cjs/clone-drivers/index.d.ts +2 -0
  17. package/lib/cjs/clone-drivers/index.js +18 -0
  18. package/lib/cjs/clone-drivers/types.d.ts +5 -0
  19. package/lib/cjs/clone-drivers/types.js +2 -0
  20. package/lib/cjs/diff-drivers/git-diff.d.ts +1 -0
  21. package/lib/cjs/diff-drivers/git-diff.js +13 -0
  22. package/lib/cjs/diff-drivers/index.d.ts +2 -0
  23. package/lib/cjs/diff-drivers/index.js +18 -0
  24. package/lib/cjs/diff-drivers/types.d.ts +5 -0
  25. package/lib/cjs/diff-drivers/types.js +2 -0
  26. package/lib/cjs/formatting/index.d.ts +2 -0
  27. package/lib/cjs/formatting/index.js +18 -0
  28. package/lib/cjs/formatting/infer-json-indent.d.ts +1 -0
  29. package/lib/cjs/formatting/infer-json-indent.js +18 -0
  30. package/lib/cjs/formatting/sync-results-to-md.d.ts +2 -0
  31. package/lib/cjs/formatting/sync-results-to-md.js +40 -0
  32. package/lib/cjs/index.d.ts +3 -0
  33. package/lib/cjs/index.js +19 -0
  34. package/lib/cjs/load-plugin.d.ts +2 -0
  35. package/lib/cjs/load-plugin.js +63 -0
  36. package/lib/cjs/match.d.ts +10 -0
  37. package/lib/cjs/match.js +45 -0
  38. package/lib/cjs/merge-file.d.ts +29 -0
  39. package/lib/cjs/merge-file.js +99 -0
  40. package/lib/cjs/plugins/index.d.ts +4 -0
  41. package/lib/cjs/plugins/index.js +10 -0
  42. package/lib/cjs/plugins/json-merge.d.ts +3 -0
  43. package/lib/cjs/plugins/json-merge.js +185 -0
  44. package/lib/cjs/template-sync.d.ts +40 -0
  45. package/lib/cjs/template-sync.js +56 -0
  46. package/lib/cjs/test-utils/index.d.ts +2 -0
  47. package/lib/cjs/test-utils/index.js +10 -0
  48. package/lib/cjs/types.d.ts +113 -0
  49. package/lib/cjs/types.js +2 -0
  50. package/lib/esm/clone-drivers/git-clone.js +14 -0
  51. package/lib/esm/clone-drivers/index.js +18 -0
  52. package/lib/esm/clone-drivers/types.js +2 -0
  53. package/lib/esm/diff-drivers/git-diff.js +13 -0
  54. package/lib/esm/diff-drivers/index.js +18 -0
  55. package/lib/esm/diff-drivers/types.js +2 -0
  56. package/lib/esm/formatting/index.js +18 -0
  57. package/lib/esm/formatting/infer-json-indent.js +18 -0
  58. package/lib/esm/formatting/sync-results-to-md.js +40 -0
  59. package/lib/esm/index.js +19 -0
  60. package/lib/esm/load-plugin.js +40 -0
  61. package/lib/esm/match.js +45 -0
  62. package/lib/esm/merge-file.js +99 -0
  63. package/lib/esm/plugins/index.js +10 -0
  64. package/lib/esm/plugins/json-merge.js +185 -0
  65. package/lib/esm/template-sync.js +56 -0
  66. package/lib/esm/test-utils/index.js +10 -0
  67. package/lib/esm/types.js +2 -0
  68. package/package.json +60 -0
  69. package/release.config.js +34 -0
  70. package/src/clone-drivers/git-clone.ts +16 -0
  71. package/src/clone-drivers/index.ts +2 -0
  72. package/src/clone-drivers/types.ts +8 -0
  73. package/src/diff-drivers/git-diff.ts +10 -0
  74. package/src/diff-drivers/index.ts +2 -0
  75. package/src/diff-drivers/types.ts +8 -0
  76. package/src/formatting/__snapshots__/sync-results-to-md.spec.ts.snap +22 -0
  77. package/src/formatting/index.ts +2 -0
  78. package/src/formatting/infer-json-indent.spec.ts +49 -0
  79. package/src/formatting/infer-json-indent.ts +16 -0
  80. package/src/formatting/sync-results-to-md.spec.ts +25 -0
  81. package/src/formatting/sync-results-to-md.ts +46 -0
  82. package/src/index.ts +3 -0
  83. package/src/load-plugin.ts +42 -0
  84. package/src/match.spec.ts +68 -0
  85. package/src/match.ts +52 -0
  86. package/src/merge-file.spec.ts +432 -0
  87. package/src/merge-file.ts +150 -0
  88. package/src/plugins/index.ts +11 -0
  89. package/src/plugins/json-merge.spec.ts +350 -0
  90. package/src/plugins/json-merge.ts +205 -0
  91. package/src/template-sync.spec.ts +216 -0
  92. package/src/template-sync.ts +113 -0
  93. package/src/test-utils/index.ts +13 -0
  94. package/src/types.ts +124 -0
  95. package/templatesync.local.json +15 -0
  96. package/test-fixtures/downstream/README.md +3 -0
  97. package/test-fixtures/downstream/package.json +18 -0
  98. package/test-fixtures/downstream/plugins/custom-plugin.js +11 -0
  99. package/test-fixtures/downstream/src/index.js +2 -0
  100. package/test-fixtures/downstream/src/index.ts +1 -0
  101. package/test-fixtures/downstream/src/templated.js +2 -0
  102. package/test-fixtures/downstream/src/templated.ts +1 -0
  103. package/test-fixtures/downstream/templatesync.json +19 -0
  104. package/test-fixtures/downstream/templatesync.local.json +14 -0
  105. package/test-fixtures/dummy-plugin.js +8 -0
  106. package/test-fixtures/glob-test/folder1/something.js +1 -0
  107. package/test-fixtures/glob-test/folder1/something.ts +0 -0
  108. package/test-fixtures/glob-test/toplevel.js +0 -0
  109. package/test-fixtures/glob-test/toplevel.txt +0 -0
  110. package/test-fixtures/template/custom-bin/something.txt +1 -0
  111. package/test-fixtures/template/package.json +17 -0
  112. package/test-fixtures/template/src/index.js +2 -0
  113. package/test-fixtures/template/src/index.ts +1 -0
  114. package/test-fixtures/template/src/templated.js +2 -0
  115. package/test-fixtures/template/src/templated.ts +1 -0
  116. package/test-fixtures/template/templatesync.json +19 -0
  117. package/tsconfig.cjs.json +12 -0
  118. package/tsconfig.esm.json +10 -0
  119. package/tsconfig.json +22 -0
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inferJSONIndent = void 0;
4
+ const spacingRegex = /[{[]\n?(?<spacing>\s+)["tf\d]/;
5
+ function inferJSONIndent(rawJSON) {
6
+ const match = spacingRegex.exec(rawJSON);
7
+ if (!match?.groups?.spacing) {
8
+ // eslint-disable-next-line no-console
9
+ console.warn(`Could not find json indentation for json string: ${rawJSON.slice(40)} ... \nDefaulting to 4 spaces`);
10
+ // Four spaces
11
+ return " ";
12
+ }
13
+ const spacing = match.groups.spacing;
14
+ // Handle the case where there were multiple newlines before a value
15
+ const lastNewLine = spacing.lastIndexOf("\n");
16
+ return match?.groups.spacing.slice(lastNewLine >= 0 ? lastNewLine + 1 : 0);
17
+ }
18
+ exports.inferJSONIndent = inferJSONIndent;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.syncResultsToMd = void 0;
4
+ const template_sync_1 = require("../template-sync");
5
+ function syncResultsToMd(result) {
6
+ return `# ${template_sync_1.TEMPLATE_SYNC_LOCAL_CONFIG}
7
+
8
+ ## Stopped the following files from syncing:
9
+
10
+ ${result.localSkipFiles.reduce((s, file) => {
11
+ return `${s}* ${file}\n`;
12
+ }, "")}
13
+
14
+ ## Changed the following files from what they would have synced:
15
+
16
+ ${Object.keys(result.localFileChanges).reduce((s, file) => {
17
+ return `${s}${file}
18
+ \`\`\`diff
19
+ ${result.localFileChanges[file].reduce((diffS, change) => {
20
+ return `${diffS}\n${makeChangeIntoDiffLines(change)}`;
21
+ }, "")}
22
+ \`\`\``;
23
+ }, "")}
24
+ `;
25
+ }
26
+ exports.syncResultsToMd = syncResultsToMd;
27
+ function makeChangeIntoDiffLines(change, nonchangeMax = 3) {
28
+ const operator = change.added ? "+" : change.removed ? "-" : "";
29
+ if (!operator &&
30
+ nonchangeMax > 0 &&
31
+ change.count &&
32
+ change.count > nonchangeMax) {
33
+ const lines = change.value.split("\n");
34
+ const partial = lines.slice(lines.length - nonchangeMax);
35
+ return `...\n${partial.join("\n")}\n`;
36
+ }
37
+ return change.value.split("\n").reduce((s, line) => {
38
+ return `${s}${operator}${line}\n`;
39
+ }, "");
40
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./template-sync"), exports);
19
+ __exportStar(require("./formatting"), exports);
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadPlugin = void 0;
4
+ const plugins_1 = require("./plugins");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ async function loadPlugin(mergeConfig, forExt, configDir) {
8
+ let handler;
9
+ if (mergeConfig.plugin) {
10
+ // First check if this is a loal .js file
11
+ const localPath = (0, path_1.resolve)(configDir, mergeConfig.plugin);
12
+ const importPath = (0, fs_1.existsSync)(localPath) ? localPath : mergeConfig.plugin;
13
+ try {
14
+ // Sad workaround for testing since dynamic import segfaults
15
+ if (process.env.JEST_WORKER_ID !== undefined) {
16
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
17
+ handler = require(importPath);
18
+ }
19
+ else {
20
+ handler = (await import(importPath));
21
+ }
22
+ if (!handler.merge) {
23
+ handler = handler
24
+ .default;
25
+ }
26
+ }
27
+ catch (err) {
28
+ console.error(err);
29
+ throw err;
30
+ }
31
+ }
32
+ else {
33
+ if (!plugins_1.defaultExtensionMap[forExt]) {
34
+ throw new Error(`No default merge function supplied for ${forExt}. Cannot have mere config without custom plugin supplied.`);
35
+ }
36
+ handler = plugins_1.defaultExtensionMap[forExt];
37
+ }
38
+ return handler;
39
+ }
40
+ exports.loadPlugin = loadPlugin;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAllFilesInDir = exports.invertMatchPatterns = void 0;
4
+ const fs_1 = require("fs");
5
+ const micromatch_1 = require("micromatch");
6
+ const path_1 = require("path");
7
+ function invertMatchPatterns(patterns) {
8
+ return patterns.map((pattern) => {
9
+ if (pattern.startsWith("!")) {
10
+ return pattern.slice(1);
11
+ }
12
+ return `!${pattern}`;
13
+ });
14
+ }
15
+ exports.invertMatchPatterns = invertMatchPatterns;
16
+ /**
17
+ * Gets all the files in the directory recursively while avoiding any micromatch patterns
18
+ * that would match for ignoring
19
+ *
20
+ * @param dir
21
+ * @param ignorePatterns
22
+ * @returns
23
+ */
24
+ function getAllFilesInDir(dir, ignorePatterns) {
25
+ return recurseDirsForFiles(dir, ignorePatterns);
26
+ }
27
+ exports.getAllFilesInDir = getAllFilesInDir;
28
+ function recurseDirsForFiles(dirpath, ignorePatterns, relativeRoot = "") {
29
+ return (0, fs_1.readdirSync)(dirpath, {
30
+ withFileTypes: true,
31
+ }).reduce((files, f) => {
32
+ const relPath = (0, path_1.join)(relativeRoot, f.name);
33
+ if ((0, micromatch_1.some)(relPath, ignorePatterns)) {
34
+ return files;
35
+ }
36
+ // Ensure we aren't ignoring these folders explicitly
37
+ if (f.isDirectory()) {
38
+ files.push(...recurseDirsForFiles((0, path_1.join)(dirpath, f.name), ignorePatterns, relPath));
39
+ }
40
+ else {
41
+ files.push(relPath);
42
+ }
43
+ return files;
44
+ }, []);
45
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeFile = void 0;
4
+ const micromatch_1 = require("micromatch");
5
+ const path_1 = require("path");
6
+ const fs_1 = require("fs");
7
+ const promises_1 = require("fs/promises");
8
+ const load_plugin_1 = require("./load-plugin");
9
+ const diff_1 = require("diff");
10
+ const fs_extra_1 = require("fs-extra");
11
+ /**
12
+ * Applies the merge to a file according to the context information.
13
+ *
14
+ * Returns true if merged and false if skipped
15
+ * @param relPath
16
+ * @param context
17
+ * @returns
18
+ */
19
+ async function mergeFile(relPath, context) {
20
+ const { localTemplateSyncConfig, templateSyncConfig, tempCloneDir, cwd } = context;
21
+ if ((0, micromatch_1.some)(relPath, localTemplateSyncConfig.ignore)) {
22
+ return {
23
+ ignoredDueToLocal: true,
24
+ };
25
+ }
26
+ const ext = (0, path_1.extname)(relPath);
27
+ const filePath = (0, path_1.join)(cwd, relPath);
28
+ const templatePath = (0, path_1.join)(tempCloneDir, relPath);
29
+ const mergeConfig = templateSyncConfig.merge?.[ext];
30
+ const localMergeConfig = localTemplateSyncConfig.merge?.[ext];
31
+ // Either write the merge or write
32
+ let fileContents;
33
+ const localChanges = [];
34
+ if ((0, fs_1.existsSync)(filePath) && (mergeConfig || localMergeConfig)) {
35
+ const originalCurrentFile = (await (0, promises_1.readFile)(filePath)).toString();
36
+ if (mergeConfig) {
37
+ // Apply the template's most recent merges
38
+ const handler = await (0, load_plugin_1.loadPlugin)(mergeConfig, ext, tempCloneDir);
39
+ const mergeOptions = mergeConfig.rules.find((rule) => {
40
+ return (0, micromatch_1.isMatch)(relPath, rule.glob);
41
+ });
42
+ if (mergeOptions) {
43
+ fileContents = await safeMerge(handler, mergeConfig.plugin ?? `default for ${ext}`, originalCurrentFile, (await (0, promises_1.readFile)(templatePath)).toString(), {
44
+ relFilePath: relPath,
45
+ mergeArguments: mergeOptions.options,
46
+ isLocalOptions: true,
47
+ });
48
+ }
49
+ else {
50
+ // Apply overwrite if we didn't set up merge
51
+ fileContents = (await (0, promises_1.readFile)(templatePath)).toString();
52
+ }
53
+ }
54
+ else {
55
+ // Apply overwrite if we didn't set up merge
56
+ fileContents = (await (0, promises_1.readFile)(templatePath)).toString();
57
+ }
58
+ // We apply the localMerge Config to the fileContent output by the template merge
59
+ if (localMergeConfig) {
60
+ const handler = await (0, load_plugin_1.loadPlugin)(localMergeConfig, ext, cwd);
61
+ const mergeOptions = localMergeConfig.rules.find((rule) => {
62
+ return (0, micromatch_1.isMatch)(relPath, rule.glob);
63
+ });
64
+ if (mergeOptions) {
65
+ const localContents = await safeMerge(handler, localMergeConfig.plugin ?? `default for ${ext}`, originalCurrentFile, fileContents, {
66
+ relFilePath: relPath,
67
+ mergeArguments: mergeOptions.options,
68
+ isLocalOptions: true,
69
+ });
70
+ localChanges.push(...(0, diff_1.diffLines)(fileContents, localContents));
71
+ fileContents = localContents;
72
+ }
73
+ }
74
+ }
75
+ else {
76
+ // Just perform simple overwrite
77
+ fileContents = (await (0, promises_1.readFile)(templatePath)).toString();
78
+ }
79
+ await (0, fs_extra_1.outputFile)(filePath, fileContents);
80
+ return {
81
+ ignoredDueToLocal: false,
82
+ localChanges,
83
+ };
84
+ }
85
+ exports.mergeFile = mergeFile;
86
+ /**
87
+ * Simple helper function to ensure that we don't let bad plugins corrupt the call flow
88
+ * @param plugin
89
+ */
90
+ async function safeMerge(plugin, pluginPath, current, fromTemplate, context) {
91
+ const ret = await plugin.merge(current, fromTemplate, context);
92
+ if (typeof ret !== "string") {
93
+ throw new Error(`Plugin ${pluginPath} did not return string for merge function! This is not allowed!`);
94
+ }
95
+ if (!ret) {
96
+ throw new Error(`Plugin ${pluginPath} should not make a merge be an empty string!`);
97
+ }
98
+ return ret;
99
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultExtensionMap = void 0;
4
+ const json_merge_1 = require("./json-merge");
5
+ exports.defaultExtensionMap = {
6
+ ".json": {
7
+ merge: json_merge_1.merge,
8
+ validate: json_merge_1.validate,
9
+ },
10
+ };
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.merge = exports.validate = void 0;
7
+ const lodash_merge_1 = __importDefault(require("lodash.merge"));
8
+ const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep"));
9
+ const jsonpath_1 = __importDefault(require("jsonpath"));
10
+ const infer_json_indent_1 = require("../formatting/infer-json-indent");
11
+ function stringOptionError(value) {
12
+ if (value === "overwrite" ||
13
+ value === "merge-template" ||
14
+ value === "merge-current") {
15
+ return;
16
+ }
17
+ return `${value} must be one of type overwrite, merge-template, or merge-current`;
18
+ }
19
+ function validate(options) {
20
+ const errors = [];
21
+ // check for flat options
22
+ if (typeof options === "string") {
23
+ const errMsg = stringOptionError(options);
24
+ if (errMsg) {
25
+ errors.push(errMsg);
26
+ }
27
+ return errors;
28
+ }
29
+ else {
30
+ if (typeof options !== "object") {
31
+ errors.push(`Options must be an object and not ${typeof options}`);
32
+ return errors;
33
+ }
34
+ if (Array.isArray(options)) {
35
+ errors.push(`Options must be an object and not Array`);
36
+ return errors;
37
+ }
38
+ if (options === null) {
39
+ errors.push("Options cannot be null");
40
+ return errors;
41
+ }
42
+ }
43
+ const optionsCast = options;
44
+ const optionKeys = Object.keys(optionsCast);
45
+ optionKeys.forEach((k) => {
46
+ if (k === "paths") {
47
+ optionsCast.paths.forEach((pathObj) => {
48
+ const [jsonPath, options] = pathObj;
49
+ if (jsonPath.startsWith("$.")) {
50
+ try {
51
+ jsonpath_1.default.parse(jsonPath);
52
+ }
53
+ catch (err) {
54
+ errors.push(`Invalid jsonpath key: ${err}`);
55
+ return;
56
+ }
57
+ const errMsg = stringOptionError(options);
58
+ if (errMsg) {
59
+ errors.push(`jsonpath ${jsonPath}: ${errMsg}`);
60
+ }
61
+ }
62
+ else {
63
+ errors.push(`Unrecognized jsonpath syntax: ${jsonPath}`);
64
+ }
65
+ });
66
+ }
67
+ else {
68
+ if (k === "ignoreNewProperties" || k === "missingIsDelete") {
69
+ return;
70
+ }
71
+ errors.push(`Unrecognized key: ${k}`);
72
+ }
73
+ });
74
+ return errors;
75
+ }
76
+ exports.validate = validate;
77
+ async function merge(current, fromTemplateRepo, context) {
78
+ if (context.mergeArguments === "overwrite") {
79
+ return fromTemplateRepo;
80
+ }
81
+ const currentJson = JSON.parse(current);
82
+ const fromTemplateJson = JSON.parse(fromTemplateRepo);
83
+ if (context.mergeArguments === "merge-current") {
84
+ // Performs Lodash Merge with current as the override
85
+ return JSON.stringify((0, lodash_merge_1.default)(fromTemplateJson, currentJson), null, 4);
86
+ }
87
+ if (context.mergeArguments === "merge-template") {
88
+ // Performs Lodash Merge with current as the override
89
+ return JSON.stringify((0, lodash_merge_1.default)(currentJson, fromTemplateJson), null, 4);
90
+ }
91
+ const { missingIsDelete, ignoreNewProperties, paths } = context.mergeArguments;
92
+ const returnJson = (0, lodash_clonedeep_1.default)(currentJson);
93
+ paths.forEach((p) => {
94
+ const [jPath, overrideType] = p;
95
+ const fromTemplatePaths = new Map();
96
+ if (overrideType === "merge-template") {
97
+ // We want to make sure there aren't extra paths from the template that didn't get added
98
+ jsonpath_1.default.nodes(fromTemplateJson, jPath).forEach((n) => {
99
+ fromTemplatePaths.set(jsonpath_1.default.stringify(n.path), n.path);
100
+ });
101
+ }
102
+ jsonpath_1.default.nodes(currentJson, jPath).forEach(({ path, value }) => {
103
+ // This solves for wildcard operators
104
+ const fullPath = jsonpath_1.default.stringify(path);
105
+ // track the paths in the template we've walked
106
+ fromTemplatePaths.delete(fullPath);
107
+ jsonpath_1.default.apply(returnJson, fullPath, () => {
108
+ const templateValue = jsonpath_1.default.value(fromTemplateJson, fullPath);
109
+ if (overrideType === "merge-template") {
110
+ if (templateValue === undefined) {
111
+ if (missingIsDelete) {
112
+ return templateValue;
113
+ }
114
+ }
115
+ else {
116
+ return applyValueMerge(value, templateValue);
117
+ }
118
+ }
119
+ else if (overrideType === "merge-current") {
120
+ return applyValueMerge(templateValue, value);
121
+ }
122
+ else if (overrideType === "overwrite") {
123
+ if (templateValue !== undefined || missingIsDelete) {
124
+ return templateValue;
125
+ }
126
+ }
127
+ else {
128
+ throw new Error(`Unexpected JsonPath merge value ${overrideType}`);
129
+ }
130
+ return value;
131
+ });
132
+ });
133
+ if (!ignoreNewProperties) {
134
+ for (const fromTemplatePath of fromTemplatePaths.values()) {
135
+ applyPerPath(fromTemplatePath, returnJson, fromTemplateJson);
136
+ }
137
+ }
138
+ });
139
+ if (!ignoreNewProperties) {
140
+ Object.keys(fromTemplateJson).forEach((key) => {
141
+ if (!returnJson[key]) {
142
+ returnJson[key] = fromTemplateJson[key];
143
+ }
144
+ });
145
+ }
146
+ return JSON.stringify(returnJson, null, (0, infer_json_indent_1.inferJSONIndent)(current));
147
+ }
148
+ exports.merge = merge;
149
+ /**
150
+ * A merge from a template to template perspective is either melding objects
151
+ * or completely overwriting primitive types
152
+ */
153
+ function applyValueMerge(base, toMerge) {
154
+ if (typeof base === "object" && typeof toMerge === "object") {
155
+ return (0, lodash_merge_1.default)(base, toMerge);
156
+ }
157
+ else {
158
+ return toMerge;
159
+ }
160
+ }
161
+ /**
162
+ * Given a nodePath on the "map" tree, this will go to the first missing element
163
+ * and copy that node onto the object
164
+ *
165
+ * @param nodePath
166
+ * @param onto
167
+ * @param map
168
+ * @param forIdx
169
+ */
170
+ function applyPerPath(nodePath,
171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
+ onto,
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ map, forIdx = 0) {
175
+ if (nodePath[forIdx] === "$") {
176
+ applyPerPath(nodePath, onto, map, forIdx + 1);
177
+ return;
178
+ }
179
+ const selector = nodePath[forIdx];
180
+ if (onto[selector]) {
181
+ applyPerPath(nodePath, onto[selector], map[selector], forIdx + 1);
182
+ return;
183
+ }
184
+ onto[selector] = map[selector];
185
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.templateSync = exports.TEMPLATE_SYNC_LOCAL_CONFIG = exports.TEMPLATE_SYNC_CONFIG = void 0;
4
+ const path_1 = require("path");
5
+ const fs_1 = require("fs");
6
+ const match_1 = require("./match");
7
+ const merge_file_1 = require("./merge-file");
8
+ const git_clone_1 = require("./clone-drivers/git-clone");
9
+ const diff_drivers_1 = require("./diff-drivers");
10
+ exports.TEMPLATE_SYNC_CONFIG = "templatesync";
11
+ exports.TEMPLATE_SYNC_LOCAL_CONFIG = "templatesync.local";
12
+ async function templateSync(options) {
13
+ const cloneDriver = options.cloneDriver ?? git_clone_1.gitClone;
14
+ const diffDriver = options.diffDriver ?? diff_drivers_1.gitDiff;
15
+ const tempCloneDir = await cloneDriver(options.tmpCloneDir, options.repoUrl);
16
+ // Get the clone Config
17
+ const cloneConfigPath = (0, path_1.join)(tempCloneDir, `${exports.TEMPLATE_SYNC_CONFIG}.json`);
18
+ const templateSyncConfig = (0, fs_1.existsSync)(cloneConfigPath)
19
+ ? JSON.parse((0, fs_1.readFileSync)(cloneConfigPath).toString())
20
+ : {};
21
+ const localConfigPath = (0, path_1.join)(options.repoDir, `${exports.TEMPLATE_SYNC_LOCAL_CONFIG}.json`);
22
+ const localTemplateSyncConfig = (0, fs_1.existsSync)(localConfigPath)
23
+ ? JSON.parse((0, fs_1.readFileSync)(localConfigPath).toString())
24
+ : {};
25
+ let filesToSync;
26
+ if (localTemplateSyncConfig.afterRef) {
27
+ filesToSync = await diffDriver(tempCloneDir, localTemplateSyncConfig.afterRef);
28
+ }
29
+ else {
30
+ filesToSync = (0, match_1.getAllFilesInDir)(tempCloneDir, [
31
+ ...templateSyncConfig.ignore,
32
+ ".git/**",
33
+ ]);
34
+ }
35
+ const localSkipFiles = [];
36
+ const localFileChanges = {};
37
+ await Promise.all(filesToSync.map(async (f) => {
38
+ const result = await (0, merge_file_1.mergeFile)(f, {
39
+ localTemplateSyncConfig,
40
+ templateSyncConfig,
41
+ tempCloneDir,
42
+ cwd: options.repoDir,
43
+ });
44
+ if (result.ignoredDueToLocal) {
45
+ localSkipFiles.push(f);
46
+ }
47
+ else if (result?.localChanges && result.localChanges.length > 0) {
48
+ localFileChanges[f] = result.localChanges;
49
+ }
50
+ }));
51
+ return {
52
+ localSkipFiles,
53
+ localFileChanges,
54
+ };
55
+ }
56
+ exports.templateSync = templateSync;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tempDir = exports.TEST_FIXTURES_DIR = void 0;
4
+ const path_1 = require("path");
5
+ const os_1 = require("os");
6
+ exports.TEST_FIXTURES_DIR = (0, path_1.resolve)(__dirname, "..", "..", "test-fixtures");
7
+ function tempDir() {
8
+ return process.env.RUNNER_TEMP ?? (0, os_1.tmpdir)();
9
+ }
10
+ exports.tempDir = tempDir;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@hanseltime/template-repo-sync",
3
+ "version": "1.0.1",
4
+ "description": "An npm library that enables pluggable, customizable synchronization between template repos",
5
+ "main": "lib/cjs/index.js",
6
+ "types": "lib/cjs/index.d.ts",
7
+ "module": "lib/esm/index.js",
8
+ "scripts": {
9
+ "format": "npx prettier . --write",
10
+ "lint": "npx eslint .",
11
+ "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest",
12
+ "run-script": "node --loader ts-node/esm",
13
+ "build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
14
+ "prepare": "husky",
15
+ "release": "npx semantic-release"
16
+ },
17
+ "keywords": [],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "@actions/core": "^1.10.1",
22
+ "@actions/github": "^6.0.0",
23
+ "diff": "^5.1.0",
24
+ "fs-extra": "^11.2.0",
25
+ "jsonpath": "^1.1.1",
26
+ "lodash.clonedeep": "^4.5.0",
27
+ "lodash.merge": "^4.6.2",
28
+ "micromatch": "^4.0.5"
29
+ },
30
+ "devDependencies": {
31
+ "@commitlint/config-angular": "^19.0.3",
32
+ "@semantic-release/changelog": "^6.0.3",
33
+ "@semantic-release/git": "^10.0.1",
34
+ "@types/diff": "^5.0.9",
35
+ "@types/fs-extra": "^11.0.4",
36
+ "@types/jest": "^29.5.11",
37
+ "@types/jsonpath": "^0.2.4",
38
+ "@types/lodash.clonedeep": "^4.5.9",
39
+ "@types/lodash.merge": "^4.6.9",
40
+ "@types/micromatch": "^4.0.6",
41
+ "@types/node": "^20.11.7",
42
+ "colors": "^1.4.0",
43
+ "commitlint": "^19.0.3",
44
+ "eslint": "^8.57.0",
45
+ "husky": "^9.0.11",
46
+ "jest": "^29.7.0",
47
+ "prettier": "3.2.5",
48
+ "semantic-release": "^23.0.2",
49
+ "ts-jest": "^29.1.2",
50
+ "ts-node": "^10.9.2",
51
+ "typescript": "^5.3.3",
52
+ "typescript-eslint": "^7.1.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=20"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }
@@ -0,0 +1,34 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ const { readFileSync } = require("fs");
3
+ const { join } = require("path");
4
+
5
+ const packageJson = JSON.parse(
6
+ readFileSync(join(__dirname, "package.json")).toString(),
7
+ );
8
+
9
+ // Abbreviate for Git Commit readability
10
+ const fullName = packageJson.name;
11
+ const scopeLimiterIdx = fullName.lastIndexOf("/");
12
+ const abbreviatedName = fullName.substring(
13
+ scopeLimiterIdx >= 0 ? scopeLimiterIdx + 1 : 0,
14
+ );
15
+
16
+ module.exports = {
17
+ branches: ["main", { name: "alpha", prerelease: true }],
18
+ plugins: [
19
+ "@semantic-release/commit-analyzer",
20
+ "@semantic-release/release-notes-generator",
21
+ "@semantic-release/changelog",
22
+ "@semantic-release/npm",
23
+ [
24
+ "@semantic-release/git",
25
+ {
26
+ assets: ["CHANGELOG.md", "package.json"],
27
+ message: `docs(release): ${abbreviatedName} $\{nextRelease.version} [skip ci]\n\n$\{nextRelease.notes}`,
28
+ },
29
+ ],
30
+ // This creates a release on github - you can decide if you want to mirror the files in package.json
31
+ "@semantic-release/github",
32
+ ],
33
+ ci: true,
34
+ };
@@ -0,0 +1,16 @@
1
+ import { execSync } from "child_process";
2
+ import { resolve } from "path";
3
+
4
+ const CLONE_DIR = "cloned_repo";
5
+
6
+ export async function gitClone(
7
+ tmpDir: string,
8
+ repoUrl: string,
9
+ ): Promise<string> {
10
+ execSync(`git clone ${repoUrl} ${CLONE_DIR}`, {
11
+ cwd: tmpDir,
12
+ env: process.env,
13
+ });
14
+
15
+ return resolve(tmpDir, CLONE_DIR);
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export * from "./git-clone";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A function that clones the template repo into the provided tmpDir
3
+ * and then returns the relative path within that directory to the template root
4
+ */
5
+ export type TemplateCloneDriverFn = (
6
+ tmpDir: string,
7
+ repoUrl: string,
8
+ ) => Promise<string>;
@@ -0,0 +1,10 @@
1
+ import { execSync } from "child_process";
2
+
3
+ export async function gitDiff(gitDir: string, afterRef: string) {
4
+ const diffFilesStr = execSync(`git diff ${afterRef}.. --name-only`, {
5
+ cwd: gitDir,
6
+ })
7
+ .toString()
8
+ .trim();
9
+ return diffFilesStr.split("\n");
10
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export * from "./git-diff";