@npmcli/template-oss 4.1.2 → 4.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.
Files changed (44) hide show
  1. package/bin/release-please.js +42 -28
  2. package/lib/apply/apply-files.js +1 -1
  3. package/lib/apply/index.js +1 -1
  4. package/lib/check/check-apply.js +5 -4
  5. package/lib/check/index.js +1 -1
  6. package/lib/config.js +178 -121
  7. package/lib/content/_job-matrix.yml +29 -0
  8. package/lib/content/_job.yml +8 -0
  9. package/lib/content/_on-ci.yml +30 -0
  10. package/lib/content/_step-checks.yml +24 -0
  11. package/lib/content/_step-deps.yml +2 -0
  12. package/lib/content/_step-git.yml +12 -0
  13. package/lib/content/_step-lint.yml +4 -0
  14. package/lib/content/{setup-node.yml → _step-node.yml} +12 -9
  15. package/lib/content/_step-test.yml +4 -0
  16. package/lib/content/_steps-setup.yml +6 -0
  17. package/lib/content/audit.yml +3 -6
  18. package/lib/content/ci-release.yml +31 -0
  19. package/lib/content/ci.yml +6 -54
  20. package/lib/content/codeql-analysis.yml +10 -17
  21. package/lib/content/commitlintrc.js +1 -1
  22. package/lib/content/dependabot.yml +2 -2
  23. package/lib/content/eslintrc.js +7 -0
  24. package/lib/content/gitignore +1 -14
  25. package/lib/content/index.js +62 -27
  26. package/lib/content/npmrc +1 -1
  27. package/lib/content/pkg.json +34 -14
  28. package/lib/content/post-dependabot.yml +55 -16
  29. package/lib/content/pull-request.yml +11 -13
  30. package/lib/content/release-please-config.json +5 -5
  31. package/lib/content/release-please-manifest.json +1 -1
  32. package/lib/content/release.yml +125 -0
  33. package/lib/index.js +27 -30
  34. package/lib/release-please/index.js +26 -5
  35. package/lib/util/files.js +71 -27
  36. package/lib/util/gitignore.js +34 -0
  37. package/lib/util/merge.js +21 -0
  38. package/lib/util/parser.js +76 -18
  39. package/lib/util/template.js +30 -21
  40. package/package.json +7 -2
  41. package/lib/content/release-please.yml +0 -73
  42. package/lib/content/release-test.yml +0 -46
  43. package/lib/content/setup-deps.yml +0 -1
  44. package/lib/content/setup-git.yml +0 -11
@@ -0,0 +1,125 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ {{#each branches}}
7
+ - {{ . }}
8
+ {{/each}}
9
+ {{#each releaseBranches }}
10
+ - {{ . }}
11
+ {{/each}}
12
+
13
+ permissions:
14
+ contents: write
15
+ pull-requests: write
16
+ checks: write
17
+
18
+ jobs:
19
+ release:
20
+ outputs:
21
+ pr: $\{{ steps.release.outputs.pr }}
22
+ releases: $\{{ steps.release.outputs.releases }}
23
+ release-flags: $\{{ steps.release.outputs.release-flags }}
24
+ branch: $\{{ steps.release.outputs.pr-branch }}
25
+ pr-number: $\{{ steps.release.outputs.pr-number }}
26
+ comment-id: $\{{ steps.pr-comment.outputs.result }}
27
+ check-id: $\{{ steps.check.outputs.check_id }}
28
+ {{> job jobName="Release" }}
29
+ - name: Release Please
30
+ id: release
31
+ env:
32
+ GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
33
+ run: |
34
+ {{ rootNpxPath }} --offline template-oss-release-please $\{{ github.ref_name }}
35
+ - name: Post Pull Request Comment
36
+ if: steps.release.outputs.pr-number
37
+ uses: actions/github-script@v6
38
+ id: pr-comment
39
+ env:
40
+ PR_NUMBER: $\{{ steps.release.outputs.pr-number }}
41
+ with:
42
+ script: |
43
+ const repo = { owner: context.repo.owner, repo: context.repo.repo }
44
+ const issue = { ...repo, issue_number: process.env.PR_NUMBER }
45
+
46
+ const { data: workflow } = await github.rest.actions.getWorkflowRun({ ...repo, run_id: context.runId })
47
+
48
+ let body = '## Release Manager\n\n'
49
+
50
+ const comments = await github.paginate(github.rest.issues.listComments, issue)
51
+ let commentId = comments?.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id
52
+
53
+ body += `- Release workflow run: ${workflow.html_url}`
54
+ if (commentId) {
55
+ await github.rest.issues.updateComment({ ...repo, comment_id: commentId, body })
56
+ } else {
57
+ const { data: comment } = await github.rest.issues.createComment({ ...issue, body })
58
+ commentId = comment?.id
59
+ }
60
+
61
+ return commentId
62
+ {{> stepChecks jobCheck=(obj name="Release" sha="${{ steps.release.outputs.pr-sha }}" if="steps.release.outputs.pr-number") }}
63
+
64
+ update:
65
+ needs: release
66
+ outputs:
67
+ sha: $\{{ steps.commit.outputs.sha }}
68
+ check-id: $\{{ steps.check.outputs.check_id }}
69
+ {{> job
70
+ jobName="Update - Release"
71
+ jobIf="needs.release.outputs.pr"
72
+ jobCheckout=(obj ref="${{ needs.release.outputs.branch }}" fetch-depth=0)
73
+ }}
74
+ - name: Run Post Pull Request Actions
75
+ env:
76
+ RELEASE_PR_NUMBER: $\{{ needs.release.outputs.pr-number }}
77
+ RELEASE_COMMENT_ID: $\{{ needs.release.outputs.comment-id }}
78
+ GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
79
+ run: |
80
+ {{ rootNpmPath }} run rp-pull-request --ignore-scripts {{ allFlags }}
81
+ - name: Commit
82
+ id: commit
83
+ env:
84
+ GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
85
+ run: |
86
+ git commit --all --amend --no-edit || true
87
+ git push --force-with-lease
88
+ echo "::set-output name=sha::$(git rev-parse HEAD)"
89
+ {{> stepChecks jobCheck=(obj sha="${{ steps.commit.outputs.sha }}" name="Release" )}}
90
+ {{> stepChecks jobCheck=(obj id="${{ needs.release.outputs.check-id }}" )}}
91
+
92
+ ci:
93
+ name: CI - Release
94
+ needs: [release, update]
95
+ if: needs.release.outputs.pr
96
+ uses: ./.github/workflows/ci-release.yml
97
+ with:
98
+ ref: $\{{ needs.release.outputs.branch }}
99
+ check-sha: $\{{ needs.update.outputs.sha }}
100
+
101
+ post-ci:
102
+ needs: [release, update, ci]
103
+ {{> job jobName="Post CI - Release" jobIf="needs.release.outputs.pr && always()" jobSkipSetup=true }}
104
+ - name: Get Needs Result
105
+ id: needs-result
106
+ run: |
107
+ result=""
108
+ if [[ "$\{{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
109
+ result="failure"
110
+ elif [[ "$\{{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
111
+ result="cancelled"
112
+ else
113
+ result="success"
114
+ fi
115
+ echo "::set-output name=result::$result"
116
+ {{> stepChecks jobCheck=(obj id="${{ needs.update.outputs.check-id }}" status="${{ steps.needs-result.outputs.result }}") }}
117
+
118
+ post-release:
119
+ needs: release
120
+ {{> job jobName="Post Release - Release" jobIf="needs.release.outputs.releases" }}
121
+ - name: Run Post Release Actions
122
+ env:
123
+ RELEASES: $\{{ needs.release.outputs.releases }}
124
+ run: |
125
+ {{ rootNpmPath }} run rp-release --ignore-scripts --if-present $\{{ join(fromJSON(needs.release.outputs.release-flags), ' ') }}
package/lib/index.js CHANGED
@@ -1,37 +1,42 @@
1
1
  const log = require('proc-log')
2
- const { defaults } = require('lodash')
2
+ const { resolve } = require('path')
3
3
  const getConfig = require('./config.js')
4
4
  const PackageJson = require('@npmcli/package-json')
5
5
  const mapWorkspaces = require('@npmcli/map-workspaces')
6
6
 
7
- const getPkg = async (path, baseConfig) => {
7
+ const getPkg = async (path) => {
8
8
  log.verbose('get-pkg', path)
9
9
 
10
- const pkg = (await PackageJson.load(path)).content
11
- const pkgConfig = getConfig.getPkgConfig(pkg)
10
+ const pkgJson = (await PackageJson.load(path)).content
11
+ const pkgConfig = getConfig.getPkgConfig(pkgJson)
12
12
  log.verbose('get-pkg', pkgConfig)
13
13
 
14
- return { pkg, path, config: { ...baseConfig, ...pkgConfig } }
14
+ if (pkgConfig.content) {
15
+ pkgConfig.content = resolve(path, pkgConfig.content)
16
+ }
17
+
18
+ return {
19
+ pkgJson,
20
+ path,
21
+ config: pkgConfig,
22
+ }
15
23
  }
16
24
 
17
25
  const getWsPkgs = async (root, rootPkg) => {
18
26
  const wsPkgs = []
19
27
 
20
- // workspaces are only used to filter paths and control changes to workspaces
21
- // so dont pass it along with the rest of the config
22
- const { workspaces, ...baseConfig } = rootPkg.config
23
-
24
28
  // Include all by default
29
+ const { workspaces } = rootPkg.config
25
30
  const include = (name) => Array.isArray(workspaces) ? workspaces.includes(name) : true
26
31
 
27
32
  // Look through all workspaces on the root pkg
28
- const rootWorkspaces = await mapWorkspaces({ pkg: rootPkg.pkg, cwd: root })
33
+ const rootWorkspaces = await mapWorkspaces({ pkg: rootPkg.pkgJson, cwd: root })
29
34
 
30
35
  for (const [wsName, wsPath] of rootWorkspaces.entries()) {
31
36
  if (include(wsName)) {
32
37
  // A workspace can control its own workspaceRepo and workspaceModule settings
33
38
  // which are true by default on the root config
34
- wsPkgs.push(await getPkg(wsPath, baseConfig))
39
+ wsPkgs.push(await getPkg(wsPath))
35
40
  }
36
41
  }
37
42
 
@@ -45,41 +50,33 @@ const getPkgs = async (root) => {
45
50
  log.verbose('get-pkgs', 'root', root)
46
51
 
47
52
  const rootPkg = await getPkg(root)
48
- const pkgs = [rootPkg]
49
-
50
- defaults(rootPkg.config, {
51
- rootRepo: true,
52
- rootModule: true,
53
- workspaceRepo: true,
54
- workspaceModule: true,
55
- workspaces: null,
56
- })
57
53
 
58
54
  const ws = await getWsPkgs(root, rootPkg)
59
55
 
60
56
  return {
61
- pkgs: pkgs.concat(ws.pkgs),
57
+ rootPkg,
58
+ pkgs: [rootPkg].concat(ws.pkgs),
62
59
  workspaces: ws.paths,
63
60
  }
64
61
  }
65
62
 
66
- const runAll = async (root, content, checks) => {
63
+ const runAll = async (root, checks) => {
67
64
  const results = []
68
- const { pkgs, workspaces } = await getPkgs(root)
65
+ const { pkgs, workspaces, rootPkg: { config: rootConfig } } = await getPkgs(root)
69
66
 
70
- for (const { pkg, path, config } of pkgs) {
67
+ for (const { pkgJson, path, config: pkgConfig } of pkgs) {
71
68
  // full config includes original config values
72
69
  const fullConfig = await getConfig({
73
- pkgs,
74
- workspaces,
75
70
  root,
76
- pkg,
77
71
  path,
78
- config,
79
- content,
72
+ pkgJson,
73
+ pkgs,
74
+ workspaces,
75
+ rootConfig,
76
+ pkgConfig,
80
77
  })
81
78
 
82
- const options = { root, pkg, path, config: fullConfig }
79
+ const options = { root, path, pkg: pkgJson, config: fullConfig }
83
80
  log.verbose('run-all', options)
84
81
 
85
82
  // files can export multiple checks so flatten first
@@ -26,13 +26,34 @@ const main = async ({ repo: fullRepo, token, dryRun, branch }) => {
26
26
  )
27
27
 
28
28
  const pullRequests = await (dryRun ? manifest.buildPullRequests() : manifest.createPullRequests())
29
- const releases = await (dryRun ? manifest.buildReleases() : manifest.createReleases())
29
+ const allReleases = await (dryRun ? manifest.buildReleases() : manifest.createReleases())
30
+
31
+ // We only ever get a single pull request with our current release-please settings
32
+ const rootPr = pullRequests.filter(Boolean)[0]
33
+ if (rootPr?.number) {
34
+ const commits = await github.octokit.paginate(github.octokit.rest.pulls.listCommits, {
35
+ owner: github.repository.owner,
36
+ repo: github.repository.repo,
37
+ pull_number: rootPr.number,
38
+ })
39
+ rootPr.sha = commits?.[commits.length - 1]?.sha
40
+ }
41
+
42
+ const releases = allReleases.filter(Boolean)
43
+ const [rootRelease, workspaceReleases] = releases.reduce((acc, r) => {
44
+ if (r.path === '.') {
45
+ acc[0] = r
46
+ } else {
47
+ acc[1].push(r)
48
+ }
49
+ return acc
50
+ }, [null, []])
30
51
 
31
52
  return {
32
- // We only ever get a single pull request with our current release-please settings
33
- pr: pullRequests.filter(Boolean)[0],
34
- releases: releases.filter(Boolean),
35
- release: releases.find(r => r.path === '.'),
53
+ pr: rootPr,
54
+ release: rootRelease,
55
+ releases: releases.length ? releases : null,
56
+ workspaceReleases: workspaceReleases.length ? workspaceReleases : null,
36
57
  }
37
58
  }
38
59
 
package/lib/util/files.js CHANGED
@@ -1,43 +1,57 @@
1
1
  const { join } = require('path')
2
+ const { defaultsDeep } = require('lodash')
2
3
  const { promisify } = require('util')
4
+ const merge = require('./merge.js')
5
+ const deepMapValues = require('just-deep-map-values')
3
6
  const glob = promisify(require('glob'))
4
7
  const Parser = require('./parser.js')
5
8
  const template = require('./template.js')
6
9
 
10
+ const FILE_KEYS = ['rootRepo', 'rootModule', 'workspaceRepo', 'workspaceModule']
11
+
7
12
  const globify = pattern => pattern.split('\\').join('/')
8
13
 
9
- // target paths need to be joinsed with dir and templated
10
- const fullTarget = (dir, file, options) => join(dir, template(file, options))
14
+ const fileEntries = (dir, files, options) => Object.entries(files)
15
+ // remove any false values
16
+ .filter(([_, v]) => v !== false)
17
+ // target paths need to be joinsed with dir and templated
18
+ .map(([k, source]) => {
19
+ const target = join(dir, template(k, options))
20
+ return [target, source]
21
+ })
11
22
 
12
23
  // given an obj of files, return the full target/source paths and associated parser
13
- const getParsers = (dir, files, options) => Object.entries(files).map(([t, s]) => {
14
- let {
15
- file,
16
- parser: fileParser,
17
- filter,
18
- } = typeof s === 'string' ? { file: s } : s
19
-
20
- file = join(options.config.sourceDir, file)
21
- const target = fullTarget(dir, t, options)
22
-
23
- if (typeof filter === 'function' && !filter(options)) {
24
- return null
25
- }
24
+ const getParsers = (dir, files, options) => {
25
+ const parsers = fileEntries(dir, files, options).map(([target, source]) => {
26
+ const { file, parser, filter, clean: shouldClean } = source
27
+
28
+ if (typeof filter === 'function' && !filter(options)) {
29
+ return null
30
+ }
31
+
32
+ const clean = typeof shouldClean === 'function' ? shouldClean(options) : false
26
33
 
27
- if (fileParser) {
34
+ if (parser) {
28
35
  // allow files to extend base parsers or create new ones
29
- return new (fileParser(Parser.Parsers))(target, file, options)
30
- }
36
+ return new (parser(Parser.Parsers))(target, file, options, { clean })
37
+ }
38
+
39
+ return new (Parser(file))(target, file, options, { clean })
40
+ })
31
41
 
32
- return new (Parser(file))(target, file, options)
33
- })
42
+ return parsers.filter(Boolean)
43
+ }
44
+
45
+ const getRemovals = async (dir, files, options) => {
46
+ const targets = fileEntries(dir, files, options).map(([t]) => globify(t))
47
+ const globs = await Promise.all(targets.map(t => glob(t, { cwd: dir })))
48
+ return globs.flat()
49
+ }
34
50
 
35
51
  const rmEach = async (dir, files, options, fn) => {
36
52
  const res = []
37
- for (const target of files.map((t) => fullTarget(dir, t, options))) {
38
- for (const file of await glob(globify(target), { cwd: dir })) {
39
- res.push(await fn(file))
40
- }
53
+ for (const file of await getRemovals(dir, files, options)) {
54
+ res.push(await fn(file))
41
55
  }
42
56
  return res.filter(Boolean)
43
57
  }
@@ -45,14 +59,44 @@ const rmEach = async (dir, files, options, fn) => {
45
59
  const parseEach = async (dir, files, options, fn) => {
46
60
  const res = []
47
61
  for (const parser of getParsers(dir, files, options)) {
48
- if (parser) {
49
- res.push(await fn(parser))
50
- }
62
+ res.push(await fn(parser))
51
63
  }
52
64
  return res.filter(Boolean)
53
65
  }
54
66
 
67
+ const parseConfig = (files, dir, overrides) => {
68
+ const normalizeFiles = (v) => deepMapValues(v, (value, key) => {
69
+ if (key === 'rm' && Array.isArray(value)) {
70
+ return value.reduce((acc, k) => {
71
+ acc[k] = true
72
+ return acc
73
+ }, {})
74
+ }
75
+ if (typeof value === 'string') {
76
+ const file = join(dir, value)
77
+ return key === 'file' ? file : { file }
78
+ }
79
+ if (value === true && FILE_KEYS.includes(key)) {
80
+ return {}
81
+ }
82
+ return value
83
+ })
84
+
85
+ const merged = merge(normalizeFiles(files), normalizeFiles(overrides))
86
+ const withDefaults = defaultsDeep(merged, FILE_KEYS.reduce((acc, k) => {
87
+ acc[k] = { add: {}, rm: {} }
88
+ return acc
89
+ }, {}))
90
+
91
+ return withDefaults
92
+ }
93
+
94
+ const getAddedFiles = (files) => files ? Object.keys(files.add || {}) : []
95
+
55
96
  module.exports = {
56
97
  rmEach,
57
98
  parseEach,
99
+ FILE_KEYS,
100
+ parseConfig,
101
+ getAddedFiles,
58
102
  }
@@ -0,0 +1,34 @@
1
+ const { posix } = require('path')
2
+ const { uniq } = require('lodash')
3
+ const localeCompare = require('@isaacs/string-locale-compare')('en')
4
+
5
+ const sortGitPaths = (a, b) => localeCompare(a.replace(/^!/g, ''), b.replace(/^!/g, ''))
6
+
7
+ const allowDir = (p) => {
8
+ const parts = p.split(posix.sep)
9
+ return parts.flatMap((part, index, list) => {
10
+ const prev = list.slice(0, index)
11
+ const isLast = index === list.length - 1
12
+ const ignorePart = ['', ...prev, part, ''].join(posix.sep)
13
+ return [`!${ignorePart}`, !isLast && `${ignorePart}*`]
14
+ }).filter(Boolean)
15
+ }
16
+
17
+ const allowRootDir = (p) => {
18
+ // This negates the first part of each path for the gitignore
19
+ // files. It should be used to allow directories where everything
20
+ // should be allowed inside such as .github/. It shouldn't be used on
21
+ // directories like `workspaces/` since we want to be explicit and
22
+ // only allow each workspace directory individually. For those use
23
+ // the allowDir method above.
24
+ const [first, hasChildren] = p.split(posix.sep)
25
+ return `${first}${hasChildren ? posix.sep : ''}`
26
+ }
27
+
28
+ const gitignore = {
29
+ allowDir: (dirs) => uniq(dirs.map(allowDir).flat()),
30
+ allowRootDir: (dirs) => dirs.map(allowRootDir).map((p) => `!${posix.sep}${p}`),
31
+ sort: (arr) => uniq(arr.sort(sortGitPaths)),
32
+ }
33
+
34
+ module.exports = gitignore
@@ -0,0 +1,21 @@
1
+ const { mergeWith } = require('lodash')
2
+
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
+ })
9
+
10
+ const mergeWithArrays = (...keys) =>
11
+ (...objects) => mergeWith({}, ...objects, (value, srcValue, key) => {
12
+ if (Array.isArray(srcValue)) {
13
+ if (keys.includes(key)) {
14
+ return (Array.isArray(value) ? value : []).concat(srcValue)
15
+ }
16
+ return srcValue
17
+ }
18
+ })
19
+
20
+ module.exports = merge
21
+ module.exports.withArrays = mergeWithArrays
@@ -4,10 +4,13 @@ const yaml = require('yaml')
4
4
  const NpmPackageJson = require('@npmcli/package-json')
5
5
  const jsonParse = require('json-parse-even-better-errors')
6
6
  const Diff = require('diff')
7
- const { unset, merge } = require('lodash')
7
+ const { unset } = require('lodash')
8
8
  const template = require('./template.js')
9
9
  const jsonDiff = require('./json-diff')
10
+ const merge = require('./merge.js')
11
+
10
12
  const setFirst = (first, rest) => ({ ...first, ...rest })
13
+
11
14
  const traverse = (value, visit, keys = []) => {
12
15
  if (keys.length) {
13
16
  const res = visit(keys, value)
@@ -22,17 +25,25 @@ const traverse = (value, visit, keys = []) => {
22
25
  }
23
26
  }
24
27
 
28
+ const fsOk = (code) => (error) => {
29
+ if (error.code === 'ENOENT') {
30
+ return null
31
+ }
32
+ return Object.assign(error, { code })
33
+ }
34
+
25
35
  class Base {
26
36
  static types = []
27
- static header = 'This file is automatically added by {{__NAME__}}. Do not edit.'
37
+ static header = 'This file is automatically added by {{ __NAME__ }}. Do not edit.'
28
38
  comment = (v) => v
29
39
  merge = false // supply a merge function which runs on prepare for certain types
30
40
  DELETE = template.DELETE
31
41
 
32
- constructor (target, source, options) {
42
+ constructor (target, source, options, fileOptions) {
33
43
  this.target = target
34
44
  this.source = source
35
45
  this.options = options
46
+ this.fileOptions = fileOptions
36
47
  }
37
48
 
38
49
  header () {
@@ -41,6 +52,13 @@ class Base {
41
52
  }
42
53
  }
43
54
 
55
+ clean () {
56
+ if (this.fileOptions.clean) {
57
+ return fs.rm(this.target).catch(fsOk())
58
+ }
59
+ return null
60
+ }
61
+
44
62
  read (s) {
45
63
  return fs.readFile(s, { encoding: 'utf-8' })
46
64
  }
@@ -87,13 +105,17 @@ class Base {
87
105
  // XXX: everything is allowed to be overridden in base classes but we could
88
106
  // find a different solution than making everything public
89
107
  applyWrite () {
90
- return Promise.resolve(this.read(this.source))
108
+ return Promise.resolve(this.clean())
109
+ .then(() => this.read(this.source))
91
110
  // replace template vars first, this will throw for nonexistant vars
92
111
  // because it must be parseable after this step
93
112
  .then((s) => this.template(s))
94
113
  // parse into whatever data structure is necessary for maniuplating
95
114
  // diffing, merging, etc. by default its a string
96
- .then((s) => this.parse(s))
115
+ .then((s) => {
116
+ this.sourcePreParse = s
117
+ return this.parse(s)
118
+ })
97
119
  // prepare the source for writing and diffing, pass in current
98
120
  // target for merging. errors parsing or preparing targets are ok here
99
121
  .then((s) => this.applyTarget().catch(() => null).then((t) => this.prepare(s, t)))
@@ -108,14 +130,9 @@ class Base {
108
130
  }
109
131
 
110
132
  async applyDiff () {
111
- const target = await this.applyTarget().catch((e) => {
112
- // handle if old does not exist
113
- if (e.code === 'ENOENT') {
114
- return null
115
- } else {
116
- return { code: 'ETARGETERROR', error: e }
117
- }
118
- })
133
+ // handle if old does not exist
134
+ const targetError = 'ETARGETERROR'
135
+ const target = await this.applyTarget().catch(fsOk(targetError))
119
136
 
120
137
  // no need to diff if current file does not exist
121
138
  if (target === null) {
@@ -130,11 +147,11 @@ class Base {
130
147
 
131
148
  // if there was a target error then there is no need to diff
132
149
  // so we just show the source with an error message
133
- if (target.code === 'ETARGETERROR') {
150
+ if (target.code === targetError) {
134
151
  const msg = `[${this.options.config.__NAME__} ERROR]`
135
152
  return [
136
153
  `${msg} There was an erroring getting the target file`,
137
- `${msg} ${target.error}`,
154
+ `${msg} ${target}`,
138
155
  `${msg} It will be overwritten with the following source:`,
139
156
  '-'.repeat(40),
140
157
  this.toString(source),
@@ -174,7 +191,12 @@ class Yml extends Base {
174
191
  comment = (c) => ` ${c}`
175
192
 
176
193
  toString (s) {
177
- return s.toString({ lineWidth: 0, indent: 2 })
194
+ try {
195
+ return s.toString({ lineWidth: 0, indent: 2 })
196
+ } catch (err) {
197
+ err.message = [this.target, this.sourcePreParse, ...s.errors, err.message].join('\n')
198
+ throw err
199
+ }
178
200
  }
179
201
 
180
202
  parse (s) {
@@ -191,6 +213,41 @@ class Yml extends Base {
191
213
  }
192
214
  }
193
215
 
216
+ class YmlMerge extends Yml {
217
+ prepare (source, t) {
218
+ if (t === null) {
219
+ // If target does not exist or is in an
220
+ // error state, we cant do anything but write
221
+ // the whole document
222
+ return super.prepare(source)
223
+ }
224
+
225
+ const key = [].concat(this.key)
226
+
227
+ const getId = (node) => {
228
+ const index = node.items.findIndex(p => p.key?.value === this.id)
229
+ return index !== -1 ? node.items[index].value?.value : node.toJSON()
230
+ }
231
+
232
+ const target = this.parse(t)
233
+ const targetNodes = target.getIn(key).items.reduce((acc, node, index) => {
234
+ acc[getId(node)] = { node, index }
235
+ return acc
236
+ }, {})
237
+
238
+ for (const node of source.getIn(key).items) {
239
+ const index = targetNodes[getId(node)]?.index
240
+ if (typeof index === 'number' && index !== -1) {
241
+ target.setIn([...key, index], node)
242
+ } else {
243
+ target.addIn(key, node)
244
+ }
245
+ }
246
+
247
+ return super.prepare(target)
248
+ }
249
+ }
250
+
194
251
  class Json extends Base {
195
252
  static types = ['json']
196
253
  // its a json comment! not really but we do add a special key
@@ -219,8 +276,8 @@ class Json extends Base {
219
276
  }
220
277
 
221
278
  class JsonMerge extends Json {
222
- static header = 'This file is partially managed by {{__NAME__}}. Edits may be overwritten.'
223
- merge = (t, s) => merge({}, t, s)
279
+ static header = 'This file is partially managed by {{ __NAME__ }}. Edits may be overwritten.'
280
+ merge = (t, s) => merge(t, s)
224
281
  }
225
282
 
226
283
  class PackageJson extends JsonMerge {
@@ -259,6 +316,7 @@ const Parsers = {
259
316
  Ini,
260
317
  Markdown,
261
318
  Yml,
319
+ YmlMerge,
262
320
  Json,
263
321
  JsonMerge,
264
322
  PackageJson,