Skip to content

Commit 4624d9c

Browse files
committedJul 7, 2023
feat: add overwrite false property to added files
By default template-oss allows for written files to be configured on a per-repo basis. This is helpful for different repos to create their own templated files and apply those. With this change, a repo can now set overwrite: false to a templated file and have those updates be applied after the default template-oss changes are made.
1 parent 449066e commit 4624d9c

File tree

7 files changed

+165
-37
lines changed

7 files changed

+165
-37
lines changed
 

‎lib/apply/apply-files.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const run = async (dir, files, options) => {
99
await rmEach(dir, rm, options, (f) => fs.rm(f))
1010

1111
log.verbose('apply-files', 'add', add)
12-
await parseEach(dir, add, options, (p) => p.applyWrite())
12+
await parseEach(dir, add, options, {}, (p) => p.applyWrite())
1313
}
1414

1515
module.exports = [{

‎lib/check/check-apply.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const run = async (type, dir, files, options) => {
1212
const { add: addFiles, rm: rmFiles } = files
1313

1414
const rm = await rmEach(dir, rmFiles, options, (f) => rel(f))
15-
const [add, update] = partition(await parseEach(dir, addFiles, options, async (p) => {
15+
const parseOpts = { allowMultipleSources: false }
16+
const [add, update] = partition(await parseEach(dir, addFiles, options, parseOpts, async (p) => {
1617
const diff = await p.applyDiff()
1718
const target = rel(p.target)
1819
if (diff === null) {

‎lib/config.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ const semver = require('semver')
44
const parseCIVersions = require('./util/parse-ci-versions.js')
55
const getGitUrl = require('./util/get-git-url.js')
66
const gitignore = require('./util/gitignore.js')
7-
const { withArrays } = require('./util/merge.js')
8-
const { FILE_KEYS, parseConfig: parseFiles, getAddedFiles } = require('./util/files.js')
7+
const { mergeWithArrays } = require('./util/merge.js')
8+
const { FILE_KEYS, parseConfig: parseFiles, getAddedFiles, mergeFiles } = require('./util/files.js')
99

1010
const CONFIG_KEY = 'templateOSS'
1111
const getPkgConfig = (pkg) => pkg[CONFIG_KEY] || {}
@@ -14,7 +14,7 @@ const { name: NAME, version: LATEST_VERSION } = require('../package.json')
1414
const MERGE_KEYS = [...FILE_KEYS, 'defaultContent', 'content']
1515
const DEFAULT_CONTENT = require.resolve(NAME)
1616

17-
const merge = withArrays('branches', 'distPaths', 'allowPaths', 'ignorePaths')
17+
const merge = mergeWithArrays('branches', 'distPaths', 'allowPaths', 'ignorePaths')
1818

1919
const makePosix = (v) => v.split(win32.sep).join(posix.sep)
2020
const deglob = (v) => makePosix(v).replace(/[/*]+$/, '')
@@ -120,7 +120,7 @@ const getFullConfig = async ({
120120
// Files get merged in from the default content (that template-oss provides) as well
121121
// as any content paths provided from the root or the workspace
122122
const fileDirs = uniq([useDefault && defaultDir, rootDir, pkgDir].filter(Boolean))
123-
const files = merge(useDefault && defaultFiles, rootFiles, pkgFiles)
123+
const files = mergeFiles(useDefault && defaultFiles, rootFiles, pkgFiles)
124124
const repoFiles = isRoot ? files.rootRepo : files.workspaceRepo
125125
const moduleFiles = isRoot ? files.rootModule : files.workspaceModule
126126

‎lib/util/files.js

+54-18
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,62 @@
11
const { join } = require('path')
2-
const { defaultsDeep } = require('lodash')
3-
const merge = require('./merge.js')
2+
const { defaultsDeep, omit } = require('lodash')
43
const deepMapValues = require('just-deep-map-values')
54
const { glob } = require('glob')
5+
const { mergeWithCustomizers, customizers } = require('./merge.js')
66
const Parser = require('./parser.js')
77
const template = require('./template.js')
88

9+
const ADD_KEY = 'add'
10+
const RM_KEY = 'rm'
911
const FILE_KEYS = ['rootRepo', 'rootModule', 'workspaceRepo', 'workspaceModule']
1012

1113
const globify = pattern => pattern.split('\\').join('/')
1214

13-
const fileEntries = (dir, files, options) => Object.entries(files)
14-
// remove any false values
15-
.filter(([_, v]) => v !== false)
16-
// target paths need to be joinsed with dir and templated
17-
.map(([k, source]) => {
18-
const target = join(dir, template(k, options))
19-
return [target, source]
20-
})
15+
const mergeFiles = mergeWithCustomizers((value, srcValue, key, target, source, stack) => {
16+
// This will merge all files except if the src file has overwrite:false. Then
17+
// the files will be turned into an array so they can be applied on top of
18+
// each other in the parser.
19+
if (
20+
stack[0] === ADD_KEY &&
21+
FILE_KEYS.includes(stack[1]) &&
22+
value?.file &&
23+
srcValue?.overwrite === false
24+
) {
25+
return [value, omit(srcValue, 'overwrite')]
26+
}
27+
}, customizers.overwriteArrays)
28+
29+
const fileEntries = (dir, files, options, { allowMultipleSources = true } = {}) => {
30+
const results = []
31+
32+
for (const [key, source] of Object.entries(files)) {
33+
// remove any false values first since that means those targets are skipped
34+
if (source === false) {
35+
continue
36+
}
37+
38+
// target paths need to be joinsed with dir and templated
39+
const target = join(dir, template(key, options))
40+
41+
if (Array.isArray(source)) {
42+
// When turning an object of files into all its entries, we allow
43+
// multiples when applying changes, but not when checking for changes
44+
// since earlier files would always return as needing an update. So we
45+
// either allow multiples and return the array or only return the last
46+
// source file in the array.
47+
const sources = allowMultipleSources ? source : source.slice(-1)
48+
results.push(...sources.map(s => [target, s]))
49+
} else {
50+
results.push([target, source])
51+
}
52+
}
53+
54+
return results
55+
}
2156

2257
// given an obj of files, return the full target/source paths and associated parser
23-
const getParsers = (dir, files, options) => {
24-
const parsers = fileEntries(dir, files, options).map(([target, source]) => {
58+
const getParsers = (dir, files, options, parseOptions) => {
59+
const parsers = fileEntries(dir, files, options, parseOptions).map(([target, source]) => {
2560
const { file, parser, filter, clean: shouldClean } = source
2661

2762
if (typeof filter === 'function' && !filter(options)) {
@@ -62,17 +97,17 @@ const rmEach = async (dir, files, options, fn) => {
6297
return res.filter(Boolean)
6398
}
6499

65-
const parseEach = async (dir, files, options, fn) => {
100+
const parseEach = async (dir, files, options, parseOptions, fn) => {
66101
const res = []
67-
for (const parser of getParsers(dir, files, options)) {
102+
for (const parser of getParsers(dir, files, options, parseOptions)) {
68103
res.push(await fn(parser))
69104
}
70105
return res.filter(Boolean)
71106
}
72107

73108
const parseConfig = (files, dir, overrides) => {
74109
const normalizeFiles = (v) => deepMapValues(v, (value, key) => {
75-
if (key === 'rm' && Array.isArray(value)) {
110+
if (key === RM_KEY && Array.isArray(value)) {
76111
return value.reduce((acc, k) => {
77112
acc[k] = true
78113
return acc
@@ -88,21 +123,22 @@ const parseConfig = (files, dir, overrides) => {
88123
return value
89124
})
90125

91-
const merged = merge(normalizeFiles(files), normalizeFiles(overrides))
126+
const merged = mergeFiles(normalizeFiles(files), normalizeFiles(overrides))
92127
const withDefaults = defaultsDeep(merged, FILE_KEYS.reduce((acc, k) => {
93-
acc[k] = { add: {}, rm: {} }
128+
acc[k] = { [ADD_KEY]: {}, [RM_KEY]: {} }
94129
return acc
95130
}, {}))
96131

97132
return withDefaults
98133
}
99134

100-
const getAddedFiles = (files) => files ? Object.keys(files.add || {}) : []
135+
const getAddedFiles = (files) => files ? Object.keys(files[ADD_KEY] || {}) : []
101136

102137
module.exports = {
103138
rmEach,
104139
parseEach,
105140
FILE_KEYS,
106141
parseConfig,
107142
getAddedFiles,
143+
mergeFiles,
108144
}

‎lib/util/merge.js

+63-12
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,72 @@
1-
const { mergeWith } = require('lodash')
1+
const { mergeWith: _mergeWith } = require('lodash')
22

3-
const merge = (...objects) => mergeWith({}, ...objects, (value, srcValue, key) => {
4-
if (Array.isArray(srcValue)) {
5-
// Dont merge arrays, last array wins
6-
return srcValue
7-
}
8-
})
3+
// Adapted from https://github.com/lodash/lodash/issues/3901#issuecomment-517983996
4+
// Allows us to keep track of the current key during each merge so a customizer
5+
// can make different merges based on the parent keys.
6+
const mergeWith = (...args) => {
7+
const customizer = args.pop()
8+
const objects = args
9+
const sourceStack = []
10+
const keyStack = []
11+
return _mergeWith({}, ...objects, (value, srcValue, key, target, source) => {
12+
let currentKeys
13+
while (true) {
14+
if (!sourceStack.length) {
15+
sourceStack.push(source)
16+
keyStack.push([])
17+
}
18+
if (source === sourceStack[sourceStack.length - 1]) {
19+
currentKeys = keyStack[keyStack.length - 1].concat(key)
20+
sourceStack.push(srcValue)
21+
keyStack.push(currentKeys)
22+
break
23+
}
24+
sourceStack.pop()
25+
keyStack.pop()
26+
}
27+
// Remove the last key since that is the current one and reverse the whole
28+
// array so that the first entry is the parent, 2nd grandparent, etc
29+
return customizer(value, srcValue, key, target, source, currentKeys.slice(0, -1).reverse())
30+
})
31+
}
32+
33+
// Create a merge function that will run a set of customizer functions
34+
const mergeWithCustomizers = (...customizers) => {
35+
return (...objects) => mergeWith({}, ...objects, (...args) => {
36+
for (const customizer of customizers) {
37+
const result = customizer(...args)
38+
// undefined means the customizer will defer to the next one
39+
// the default behavior of undefined in lodash is to merge
40+
if (result !== undefined) {
41+
return result
42+
}
43+
}
44+
})
45+
}
946

10-
const mergeWithArrays = (...keys) =>
11-
(...objects) => mergeWith({}, ...objects, (value, srcValue, key) => {
47+
const customizers = {
48+
// Dont merge arrays, last array wins
49+
overwriteArrays: (value, srcValue) => {
50+
if (Array.isArray(srcValue)) {
51+
return srcValue
52+
}
53+
},
54+
// Merge arrays if their key matches one of the passed in keys
55+
mergeArrays: (...keys) => (value, srcValue, key) => {
1256
if (Array.isArray(srcValue)) {
1357
if (keys.includes(key)) {
1458
return (Array.isArray(value) ? value : []).concat(srcValue)
1559
}
1660
return srcValue
1761
}
18-
})
62+
},
63+
}
1964

20-
module.exports = merge
21-
module.exports.withArrays = mergeWithArrays
65+
module.exports = {
66+
// default merge is to overwrite arrays
67+
merge: mergeWithCustomizers(customizers.overwriteArrays),
68+
mergeWithArrays: (...keys) => mergeWithCustomizers(customizers.mergeArrays(...keys)),
69+
mergeWithCustomizers,
70+
mergeWith,
71+
customizers,
72+
}

‎lib/util/parser.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const ini = require('ini')
99
const { minimatch } = require('minimatch')
1010
const template = require('./template.js')
1111
const jsonDiff = require('./json-diff')
12-
const merge = require('./merge.js')
12+
const { merge } = require('./merge.js')
1313

1414
const setFirst = (first, rest) => ({ ...first, ...rest })
1515

‎test/apply/overwrite-false.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const t = require('tap')
2+
const setup = require('../setup.js')
3+
4+
t.test('json merge', async (t) => {
5+
const s = await setup(t, {
6+
ok: true,
7+
package: {
8+
templateOSS: {
9+
content: 'content',
10+
},
11+
},
12+
testdir: {
13+
content: {
14+
'index.js': `module.exports=${JSON.stringify({
15+
rootModule: {
16+
add: {
17+
'package.json': {
18+
file: 'more-package.json',
19+
overwrite: false,
20+
},
21+
},
22+
},
23+
})}`,
24+
'more-package.json': JSON.stringify({
25+
scripts: {
26+
test: 'tap test/',
27+
},
28+
}),
29+
},
30+
},
31+
})
32+
33+
await s.apply()
34+
35+
const pkg = await s.readJson('package.json')
36+
t.equal(pkg.scripts.test, 'tap test/')
37+
t.equal(pkg.scripts.snap, 'tap')
38+
39+
t.strictSame(await s.check(), [])
40+
})

0 commit comments

Comments
 (0)
Please sign in to comment.