Skip to content

Commit

Permalink
feat: add overwrite: false property to added files
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lukekarrys committed Jul 6, 2023
1 parent 83f3699 commit 38df407
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 46 deletions.
30 changes: 18 additions & 12 deletions lib/check/check-apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ const localeCompare = require('@isaacs/string-locale-compare')('en')

const solution = 'npx template-oss-apply --force'

const checkEach = async (parser) => {
const diff = await parser.applyDiff()
if (diff === null) {
// needs to be added
return parser.target
} else if (diff === true) {
// its ok, no diff, this is filtered out
return null
}
return { file: parser.target, diff, parser }
}

const run = async (type, dir, files, options) => {
const res = []
const rel = (f) => relative(options.root, f)
const { add: addFiles, rm: rmFiles } = files

const rm = await rmEach(dir, rmFiles, options, (f) => rel(f))
const [add, update] = partition(await parseEach(dir, addFiles, options, async (p) => {
const diff = await p.applyDiff()
const target = rel(p.target)
if (diff === null) {
// needs to be added
return target
} else if (diff === true) {
// its ok, no diff, this is filtered out
return null
}
return { file: target, diff }
}), (d) => typeof d === 'string')
const [add, update] = await parseEach(dir, addFiles, options, { allowMultiple: false }, checkEach)
.then(checks => checks.map(c => typeof c === 'string' ? rel(c) : {
...c,
file: rel(c.file),
}))
.then((checks) => partition(checks, (d) => typeof d === 'string'))

log.verbose('check-apply', 'rm', rm)
if (rm.length) {
Expand Down
4 changes: 2 additions & 2 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const parseCIVersions = require('./util/parse-ci-versions.js')
const getGitUrl = require('./util/get-git-url.js')
const gitignore = require('./util/gitignore.js')
const { withArrays } = require('./util/merge.js')
const { FILE_KEYS, parseConfig: parseFiles, getAddedFiles } = require('./util/files.js')
const { FILE_KEYS, parseConfig: parseFiles, getAddedFiles, mergeFiles } = require('./util/files.js')

const CONFIG_KEY = 'templateOSS'
const getPkgConfig = (pkg) => pkg[CONFIG_KEY] || {}
Expand Down Expand Up @@ -120,7 +120,7 @@ const getFullConfig = async ({
// Files get merged in from the default content (that template-oss provides) as well
// as any content paths provided from the root or the workspace
const fileDirs = uniq([useDefault && defaultDir, rootDir, pkgDir].filter(Boolean))
const files = merge(useDefault && defaultFiles, rootFiles, pkgFiles)
const files = mergeFiles(useDefault && defaultFiles, rootFiles, pkgFiles)
const repoFiles = isRoot ? files.rootRepo : files.workspaceRepo
const moduleFiles = isRoot ? files.rootModule : files.workspaceModule

Expand Down
44 changes: 32 additions & 12 deletions lib/util/files.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
const { join } = require('path')
const { defaultsDeep } = require('lodash')
const merge = require('./merge.js')
const { defaultsDeep, omit } = require('lodash')
const deepMapValues = require('just-deep-map-values')
const { glob } = require('glob')
const { withCustomizers, customizers } = require('./merge.js')
const Parser = require('./parser.js')
const template = require('./template.js')

const FILE_KEYS = ['rootRepo', 'rootModule', 'workspaceRepo', 'workspaceModule']

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

const fileEntries = (dir, files, options) => Object.entries(files)
const mergeFiles = withCustomizers((value, srcValue, _, __, ___, stack) => {
if (
stack[stack.length - 2] === 'add' &&
FILE_KEYS.includes(stack[stack.length - 3]) &&
value?.file &&
srcValue?.overwrite === false
) {
return [value, omit(srcValue, 'overwrite')]
}
}, customizers.overwriteArrays)

const fileEntries = (dir, files, options, { allowMultiple = true } = {}) => Object.entries(files)
// remove any false values
.filter(([_, v]) => v !== false)
// target paths need to be joinsed with dir and templated
.map(([k, source]) => {
.reduce((acc, [k, source]) => {
const target = join(dir, template(k, options))
return [target, source]
})
if (Array.isArray(source)) {
const sources = allowMultiple ? source : source.slice(-1)
acc.push(...sources.map(s => [target, s]))
} else {
acc.push([target, source])
}
return acc
}, [])

// given an obj of files, return the full target/source paths and associated parser
const getParsers = (dir, files, options) => {
const parsers = fileEntries(dir, files, options).map(([target, source]) => {
const getParsers = (dir, files, options, fileOptions) => {
const parsers = fileEntries(dir, files, options, fileOptions).map(([target, source]) => {
const { file, parser, filter, clean: shouldClean } = source

if (typeof filter === 'function' && !filter(options)) {
Expand All @@ -35,7 +52,7 @@ const getParsers = (dir, files, options) => {
return new (parser(Parser.Parsers))(target, file, options, { clean })
}

return new (Parser(file))(target, file, options, { clean })
return new (Parser(target))(target, file, options, { clean })
})

return parsers.filter(Boolean)
Expand All @@ -62,9 +79,11 @@ const rmEach = async (dir, files, options, fn) => {
return res.filter(Boolean)
}

const parseEach = async (dir, files, options, fn) => {
const parseEach = async (dir, files, options, ...fnArgs) => {
const res = []
for (const parser of getParsers(dir, files, options)) {
const fn = fnArgs.pop()
const fileOptions = fnArgs[0] ?? {}
for (const parser of getParsers(dir, files, options, fileOptions)) {
res.push(await fn(parser))
}
return res.filter(Boolean)
Expand All @@ -88,7 +107,7 @@ const parseConfig = (files, dir, overrides) => {
return value
})

const merged = merge(normalizeFiles(files), normalizeFiles(overrides))
const merged = mergeFiles(normalizeFiles(files), normalizeFiles(overrides))
const withDefaults = defaultsDeep(merged, FILE_KEYS.reduce((acc, k) => {
acc[k] = { add: {}, rm: {} }
return acc
Expand All @@ -105,4 +124,5 @@ module.exports = {
FILE_KEYS,
parseConfig,
getAddedFiles,
mergeFiles,
}
54 changes: 43 additions & 11 deletions lib/util/merge.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
const { mergeWith } = require('lodash')

const merge = (...objects) => mergeWith({}, ...objects, (value, srcValue, key) => {
if (Array.isArray(srcValue)) {
// Dont merge arrays, last array wins
return srcValue
}
})
const mergeWithCustomizers = (...customizers) => {
const sourceStack = []
const pathStack = []
return (...objects) => mergeWith({}, ...objects, (value, srcValue, key, target, source) => {
let currentPath
while (true) {
if (!sourceStack.length) {
sourceStack.push(source)
pathStack.push([])
}
const prevSource = sourceStack[sourceStack.length - 1]
const prevPath = pathStack[pathStack.length - 1]
if (source === prevSource) {
currentPath = prevPath.concat(key)
sourceStack.push(srcValue)
pathStack.push(currentPath)
break
}
sourceStack.pop()
pathStack.pop()
}
for (const cusomtizer of customizers) {
const result = cusomtizer(value, srcValue, key, target, source, currentPath)
if (result !== undefined) {
return result
}
}
})
}

const mergeWithArrays = (...keys) =>
(...objects) => mergeWith({}, ...objects, (value, srcValue, key) => {
const customizers = {
overwriteArrays: (value, srcValue) => {
if (Array.isArray(srcValue)) {
// Dont merge arrays, last array wins
return srcValue
}
},
mergeArrays: (...keys) => (value, srcValue, key) => {
if (Array.isArray(srcValue)) {
if (keys.includes(key)) {
return (Array.isArray(value) ? value : []).concat(srcValue)
}
return srcValue
}
})
},
}

module.exports = merge
module.exports.withArrays = mergeWithArrays
module.exports = mergeWithCustomizers(customizers.overwriteArrays)
module.exports.withArrays = (...keys) => mergeWithCustomizers(customizers.mergeArrays(...keys))
module.exports.withCustomizers = mergeWithCustomizers
module.exports.customizers = customizers
18 changes: 9 additions & 9 deletions lib/util/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,17 @@ class Base {
}

class Gitignore extends Base {
static types = ['codeowners', 'gitignore']
static types = ['codeowners', '.gitignore']
comment = (c) => `# ${c}`
}

class Js extends Base {
static types = ['js']
static types = ['.js']
comment = (c) => `/* ${c} */`
}

class Ini extends Base {
static types = ['ini']
static types = ['.ini']
comment = (c) => `; ${c}`

toString (s) {
Expand All @@ -202,17 +202,17 @@ class Ini extends Base {
}

class IniMerge extends Ini {
static types = ['npmrc']
static types = ['.npmrc']
merge = (t, s) => merge(t, s)
}

class Markdown extends Base {
static types = ['md']
static types = ['.md']
comment = (c) => `<!-- ${c} -->`
}

class Yml extends Base {
static types = ['yml']
static types = ['.yml']
comment = (c) => ` ${c}`

toString (s) {
Expand Down Expand Up @@ -274,7 +274,7 @@ class YmlMerge extends Yml {
}

class Json extends Base {
static types = ['json']
static types = ['.json']
// its a json comment! not really but we do add a special key
// to json objects
comment = (c) => ({ [`//${this.options.config.__NAME__}`]: c })
Expand Down Expand Up @@ -306,7 +306,7 @@ class JsonMerge extends Json {
}

class PackageJson extends JsonMerge {
static types = ['pkg.json']
static types = ['package.json']

async prepare (s, t) {
// merge new source with current pkg content
Expand Down Expand Up @@ -352,7 +352,7 @@ const parserLookup = Object.values(Parsers)

const getParser = (file) => {
const base = basename(file).toLowerCase()
const ext = extname(file).slice(1).toLowerCase()
const ext = extname(file).toLowerCase()

return parserLookup.find((p) => p.types.includes(base))
|| parserLookup.find((p) => p.types.includes(ext))
Expand Down
40 changes: 40 additions & 0 deletions test/apply/overwrite-false.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const t = require('tap')
const setup = require('../setup.js')

t.test('json merge', async (t) => {
const s = await setup(t, {
ok: true,
package: {
templateOSS: {
content: 'content',
},
},
testdir: {
content: {
'index.js': `module.exports=${JSON.stringify({
rootModule: {
add: {
'package.json': {
file: 'more-package.json',
overwrite: false,
},
},
},
})}`,
'more-package.json': JSON.stringify({
scripts: {
test: 'tap test/',
},
}),
},
},
})

await s.apply()

const pkg = await s.readJson('package.json')
t.equal(pkg.scripts.test, 'tap test/')
t.equal(pkg.scripts.snap, 'tap')

t.strictSame(await s.check(), [])
})

0 comments on commit 38df407

Please sign in to comment.