@npmcli/template-oss 2.9.2 → 3.1.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 (52) hide show
  1. package/README.md +77 -68
  2. package/bin/apply.js +22 -0
  3. package/bin/check.js +26 -0
  4. package/lib/apply/apply-files.js +31 -0
  5. package/lib/apply/index.js +5 -0
  6. package/lib/check/check-apply.js +73 -0
  7. package/lib/check/check-changelog.js +31 -0
  8. package/lib/check/check-gitignore.js +67 -0
  9. package/lib/check/check-required.js +55 -0
  10. package/lib/check/check-unwanted.js +23 -0
  11. package/lib/check/index.js +9 -0
  12. package/lib/config.js +151 -40
  13. package/lib/content/CODEOWNERS +1 -1
  14. package/lib/content/LICENSE.md +0 -2
  15. package/lib/content/SECURITY.md +0 -2
  16. package/lib/content/audit.yml +5 -12
  17. package/lib/content/bug.yml +45 -46
  18. package/lib/content/ci.yml +35 -38
  19. package/lib/content/codeql-analysis.yml +11 -9
  20. package/lib/content/commitlintrc.js +1 -4
  21. package/lib/content/config.yml +0 -2
  22. package/lib/content/dependabot.yml +13 -14
  23. package/lib/content/eslintrc.js +0 -2
  24. package/lib/content/gitignore +8 -14
  25. package/lib/content/index.js +92 -0
  26. package/lib/content/npmrc +0 -2
  27. package/lib/content/pkg.json +27 -0
  28. package/lib/content/post-dependabot.yml +12 -15
  29. package/lib/content/pull-request.yml +11 -13
  30. package/lib/content/release-please.yml +18 -11
  31. package/lib/content/setup-git.yml +11 -0
  32. package/lib/content/setup-node.yml +25 -0
  33. package/lib/index.js +100 -0
  34. package/lib/util/files.js +43 -0
  35. package/lib/util/get-git-url.js +24 -0
  36. package/lib/util/has-package.js +83 -0
  37. package/lib/util/json-diff.js +33 -0
  38. package/lib/util/output.js +35 -0
  39. package/lib/util/parse-ci-versions.js +78 -0
  40. package/lib/util/parser.js +280 -0
  41. package/lib/util/template.js +41 -0
  42. package/package.json +27 -25
  43. package/bin/.gitattributes +0 -3
  44. package/bin/npm-template-check.js +0 -44
  45. package/bin/postinstall.js +0 -31
  46. package/lib/content/ci-no-windows.yml +0 -48
  47. package/lib/content/ci-workspace.yml +0 -63
  48. package/lib/content/release-please-workspace.yml +0 -29
  49. package/lib/postinstall/copy-content.js +0 -133
  50. package/lib/postinstall/update-package.js +0 -100
  51. package/lib/postlint/check-gitignore.js +0 -59
  52. package/lib/postlint/check-package.js +0 -90
@@ -0,0 +1,280 @@
1
+ const fs = require('@npmcli/fs')
2
+ const { EOL } = require('os')
3
+ const { basename, extname, dirname } = require('path')
4
+ const yaml = require('yaml')
5
+ const NpmPackageJson = require('@npmcli/package-json')
6
+ const jsonParse = require('json-parse-even-better-errors')
7
+ const Diff = require('diff')
8
+ const { unset, merge } = require('lodash')
9
+ const template = require('./template.js')
10
+ const jsonDiff = require('./json-diff')
11
+ const setFirst = (first, rest) => ({ ...first, ...rest })
12
+ const traverse = (value, visit, keys = []) => {
13
+ if (keys.length) {
14
+ const res = visit(keys, value)
15
+ if (res != null) {
16
+ return
17
+ }
18
+ }
19
+ if (typeof value === 'object' && value !== null) {
20
+ for (const [k, v] of Object.entries(value)) {
21
+ traverse(v, visit, keys.concat(k))
22
+ }
23
+ }
24
+ }
25
+
26
+ class Base {
27
+ static types = []
28
+ static header = 'This file is automatically added by {{__NAME__}}. Do not edit.'
29
+ comment = (v) => v
30
+ merge = false // supply a merge function which runs on prepare for certain types
31
+ DELETE = template.DELETE
32
+
33
+ constructor (target, source, options) {
34
+ this.target = target
35
+ this.source = source
36
+ this.options = options
37
+ }
38
+
39
+ header () {
40
+ if (typeof this.comment === 'function') {
41
+ return this.comment(this.template(this.constructor.header || ''))
42
+ }
43
+ }
44
+
45
+ read (s) {
46
+ return fs.readFile(s, { encoding: 'utf-8' })
47
+ }
48
+
49
+ template (s) {
50
+ return template(s, this.options)
51
+ }
52
+
53
+ parse (s) {
54
+ return s
55
+ }
56
+
57
+ prepare (s) {
58
+ const header = this.header()
59
+ return header ? header + EOL + EOL + s : s
60
+ }
61
+
62
+ prepareTarget (s) {
63
+ return s
64
+ }
65
+
66
+ toString (s) {
67
+ return s.toString()
68
+ }
69
+
70
+ async write (s) {
71
+ // XXX: find more efficient way to do this. we can build all possible dirs before get here
72
+ await fs.mkdir(dirname(this.target), { owner: 'inherit', recursive: true, force: true })
73
+ return fs.writeFile(this.target, this.toString(s), { owner: 'inherit' })
74
+ }
75
+
76
+ diffPatch (t, s) {
77
+ // create a patch and strip out the filename. if it ends up an empty string
78
+ // then return true since the files are equal
79
+ return Diff.createPatch('', t.replace(/\r\n/g, '\n'), s.replace(/\r\n/g, '\n'))
80
+ .split('\n').slice(4).join('\n')
81
+ }
82
+
83
+ diff (t, s) {
84
+ return this.diffPatch(t, s)
85
+ }
86
+
87
+ // the apply methods are the only ones that should be called publically
88
+ // XXX: everything is allowed to be overridden in base classes but we could
89
+ // find a different solution than making everything public
90
+ applyWrite () {
91
+ return Promise.resolve(this.read(this.source))
92
+ // replace template vars first, this will throw for nonexistant vars
93
+ // because it must be parseable after this step
94
+ .then((s) => this.template(s))
95
+ // parse into whatever data structure is necessary for maniuplating
96
+ // diffing, merging, etc. by default its a string
97
+ .then((s) => this.parse(s))
98
+ // prepare the source for writing and diffing, pass in current
99
+ // target for merging. errors parsing or preparing targets are ok here
100
+ .then((s) => this.applyTarget().catch(() => null).then((t) => this.prepare(s, t)))
101
+ .then((s) => this.write(s))
102
+ }
103
+
104
+ applyTarget () {
105
+ return Promise.resolve(this.read(this.target))
106
+ .then((s) => this.parse(s))
107
+ // for only preparing the target for diffing
108
+ .then((s) => this.prepareTarget(s))
109
+ }
110
+
111
+ async applyDiff () {
112
+ const target = await this.applyTarget().catch((e) => {
113
+ // handle if old does not exist
114
+ if (e.code === 'ENOENT') {
115
+ return null
116
+ } else {
117
+ return { code: 'ETARGETERROR', error: e }
118
+ }
119
+ })
120
+
121
+ // no need to diff if current file does not exist
122
+ if (target === null) {
123
+ return null
124
+ }
125
+
126
+ const source = await Promise.resolve(this.read(this.source))
127
+ .then((s) => this.template(s))
128
+ .then((s) => this.parse(s))
129
+ // gets the target to diff against in case it needs to merge, etc
130
+ .then((s) => this.prepare(s, target))
131
+
132
+ // if there was a target error then there is no need to diff
133
+ // so we just show the source with an error message
134
+ if (target.code === 'ETARGETERROR') {
135
+ const msg = `[${this.options.config.__NAME__} ERROR]`
136
+ return [
137
+ `${msg} There was an erroring getting the target file`,
138
+ `${msg} ${target.error}`,
139
+ `${msg} It will be overwritten with the following source:`,
140
+ '-'.repeat(40),
141
+ this.toString(source),
142
+ ].join('\n')
143
+ }
144
+
145
+ // individual diff methods are responsible for returning a string
146
+ // representing the diff. an empty trimmed string means no diff
147
+ const diffRes = this.diff(target, source).trim()
148
+ return diffRes || true
149
+ }
150
+ }
151
+
152
+ class Gitignore extends Base {
153
+ static types = ['codeowners', 'gitignore']
154
+ comment = (c) => `# ${c}`
155
+ }
156
+
157
+ class Js extends Base {
158
+ static types = ['js']
159
+ comment = (c) => `/* ${c} */`
160
+ }
161
+
162
+ class Ini extends Base {
163
+ // XXX: add merge mode for updating ini files
164
+ static types = ['npmrc']
165
+ comment = (c) => `; ${c}`
166
+ }
167
+
168
+ class Markdown extends Base {
169
+ static types = ['md']
170
+ comment = (c) => `<!-- ${c} -->`
171
+ }
172
+
173
+ class Yml extends Base {
174
+ static types = ['yml']
175
+ comment = (c) => ` ${c}`
176
+
177
+ toString (s) {
178
+ return s.toString({ lineWidth: 0, indent: 2 })
179
+ }
180
+
181
+ parse (s) {
182
+ return yaml.parseDocument(s)
183
+ }
184
+
185
+ prepare (s) {
186
+ s.commentBefore = this.header()
187
+ return this.toString(s)
188
+ }
189
+
190
+ prepareTarget (s) {
191
+ return this.toString(s)
192
+ }
193
+ }
194
+
195
+ class Json extends Base {
196
+ static types = ['json']
197
+ // its a json comment! not really but we do add a special key
198
+ // to json objects
199
+ comment = (c) => ({ [`//${this.options.config.__NAME__}`]: c })
200
+
201
+ toString (s) {
202
+ return JSON.stringify(s, (_, v) => v === this.DELETE ? undefined : v, 2)
203
+ }
204
+
205
+ parse (s) {
206
+ return jsonParse(s)
207
+ }
208
+
209
+ prepare (s, t) {
210
+ let source = s
211
+ if (typeof this.merge === 'function' && t) {
212
+ source = this.merge(t, s)
213
+ }
214
+ return setFirst(this.header(), source)
215
+ }
216
+
217
+ diff (t, s) {
218
+ return jsonDiff(t, s, this.DELETE)
219
+ }
220
+ }
221
+
222
+ class JsonMerge extends Json {
223
+ static header = 'This file is partially managed by {{__NAME__}}. Edits may be overwritten.'
224
+ merge = (t, s) => merge({}, t, s)
225
+ }
226
+
227
+ class PackageJson extends JsonMerge {
228
+ static types = ['pkg.json']
229
+
230
+ async prepare (s, t) {
231
+ // merge new source with current pkg content
232
+ const update = super.prepare(s, t)
233
+
234
+ // move comment to config field
235
+ const configKey = this.options.config.__CONFIG_KEY__
236
+ const header = this.header()
237
+ const headerKey = Object.keys(header)[0]
238
+ update[configKey] = setFirst(header, update[configKey])
239
+ delete update[headerKey]
240
+
241
+ return update
242
+ }
243
+
244
+ async write (s) {
245
+ const pkg = await NpmPackageJson.load(dirname(this.target))
246
+ pkg.update(s)
247
+ traverse(pkg.content, (keys, value) => {
248
+ if (value === this.DELETE) {
249
+ return unset(pkg.content, keys)
250
+ }
251
+ })
252
+ pkg.save()
253
+ }
254
+ }
255
+
256
+ const Parsers = {
257
+ Base,
258
+ Gitignore,
259
+ Js,
260
+ Ini,
261
+ Markdown,
262
+ Yml,
263
+ Json,
264
+ JsonMerge,
265
+ PackageJson,
266
+ }
267
+
268
+ const parserLookup = Object.values(Parsers)
269
+
270
+ const getParser = (file) => {
271
+ const base = basename(file).toLowerCase()
272
+ const ext = extname(file).slice(1).toLowerCase()
273
+
274
+ return parserLookup.find((p) => p.types.includes(base))
275
+ || parserLookup.find((p) => p.types.includes(ext))
276
+ || Parsers.Base
277
+ }
278
+
279
+ module.exports = getParser
280
+ module.exports.Parsers = Parsers
@@ -0,0 +1,41 @@
1
+ const Handlebars = require('handlebars')
2
+ const { basename, extname, join } = require('path')
3
+ const fs = require('fs')
4
+ const DELETE = '__DELETE__'
5
+
6
+ const partialName = (s) =>
7
+ basename(s, extname(s)).replace(/-([a-z])/g, (_, g) => g.toUpperCase())
8
+
9
+ const setupHandlebars = (partialsDir) => {
10
+ Handlebars.registerHelper('obj', ({ hash }) => hash)
11
+ Handlebars.registerHelper('json', (c) => JSON.stringify(c))
12
+ Handlebars.registerHelper('del', () => JSON.stringify(DELETE))
13
+
14
+ // Load all content files as camelcase partial names
15
+ for (const f of fs.readdirSync(join(partialsDir))) {
16
+ Handlebars.registerPartial(
17
+ partialName(f),
18
+ fs.readFileSync(join(partialsDir, f)).toString()
19
+ )
20
+ }
21
+ }
22
+
23
+ const cache = new Map()
24
+
25
+ const template = (str, { config, ...options }) => {
26
+ if (cache.size === 0) {
27
+ setupHandlebars(config.sourceDir)
28
+ }
29
+
30
+ let t = cache.get(str)
31
+ if (t == null) {
32
+ t = Handlebars.compile(str, { strict: true })
33
+ cache.set(str, t)
34
+ }
35
+
36
+ // merge in config as top level data in templates
37
+ return t({ ...options, ...config })
38
+ }
39
+
40
+ module.exports = template
41
+ module.exports.DELETE = DELETE
package/package.json CHANGED
@@ -1,28 +1,28 @@
1
1
  {
2
2
  "name": "@npmcli/template-oss",
3
- "version": "2.9.2",
3
+ "version": "3.1.1",
4
4
  "description": "templated files used in npm CLI team oss projects",
5
- "main": "lib/index.js",
5
+ "main": "lib/content/index.js",
6
6
  "bin": {
7
- "npm-template-check": "bin/npm-template-check.js",
8
- "npm-template-copy": "bin/postinstall.js"
7
+ "template-oss-apply": "bin/apply.js",
8
+ "template-oss-check": "bin/check.js"
9
9
  },
10
10
  "scripts": {
11
- "lint": "eslint '**/*.js'",
11
+ "lint": "eslint \"**/*.js\"",
12
12
  "lintfix": "npm run lint -- --fix",
13
- "postinstall": "node bin/postinstall.js",
14
- "postlint": "npm-template-check",
15
13
  "posttest": "npm run lint",
16
14
  "postversion": "npm publish",
17
15
  "prepublishOnly": "git push origin --follow-tags",
18
16
  "preversion": "npm test",
19
17
  "snap": "tap",
20
18
  "test": "tap",
21
- "template-copy": "npm-template-copy --force"
19
+ "template-oss-apply": "template-oss-apply --force",
20
+ "postlint": "template-oss-check",
21
+ "postinstall": "template-oss-apply"
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/npm/template-oss.git"
25
+ "url": "https://github.com/npm/template-oss.git"
26
26
  },
27
27
  "keywords": [
28
28
  "npm",
@@ -32,31 +32,33 @@
32
32
  "license": "ISC",
33
33
  "dependencies": {
34
34
  "@npmcli/fs": "^2.0.1",
35
- "@npmcli/map-workspaces": "^2.0.1",
35
+ "@npmcli/git": "^3.0.0",
36
+ "@npmcli/map-workspaces": "^2.0.2",
36
37
  "@npmcli/package-json": "^1.0.1",
38
+ "diff": "^5.0.0",
39
+ "handlebars": "^4.7.7",
40
+ "hosted-git-info": "^5.0.0",
37
41
  "json-parse-even-better-errors": "^2.3.1",
38
- "which": "^2.0.2"
42
+ "just-diff": "^5.0.1",
43
+ "lodash": "^4.17.21",
44
+ "npm-package-arg": "^9.0.1",
45
+ "proc-log": "^2.0.0",
46
+ "semver": "^7.3.5",
47
+ "yaml": "^2.0.0-10"
39
48
  },
40
49
  "files": [
41
- "bin",
42
- "lib"
50
+ "bin/",
51
+ "lib/"
43
52
  ],
44
53
  "devDependencies": {
45
- "@npmcli/eslint-config": "*",
46
- "@npmcli/promise-spawn": "^2.0.0",
54
+ "@npmcli/eslint-config": "^3.0.1",
47
55
  "@npmcli/template-oss": "file:./",
48
- "eslint": "^8.10.0",
49
- "eslint-plugin-node": "^11.1.0",
50
- "tap": "*"
56
+ "tap": "^16.0.0"
51
57
  },
52
- "peerDependencies": {
53
- "@npmcli/eslint-config": "^2.0.0",
54
- "tap": "^15.0.9"
55
- },
56
- "tap": {
57
- "coverage-map": "map.js"
58
+ "templateOSS": {
59
+ "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten."
58
60
  },
59
61
  "engines": {
60
- "node": "^12.13.0 || ^14.15.0 || >=16"
62
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
61
63
  }
62
64
  }
@@ -1,3 +0,0 @@
1
- # Dont modify line endings of our bin scripts
2
- # so git status stays clean for windows tests
3
- *.js -crlf
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const checkPackage = require('../lib/postlint/check-package.js')
4
- const checkGitIgnore = require('../lib/postlint/check-gitignore.js')
5
- const getConfig = require('../lib/config.js')
6
-
7
- const main = async () => {
8
- const {
9
- npm_config_local_prefix: root,
10
- } = process.env
11
-
12
- if (!root) {
13
- throw new Error('This package requires npm >7.21.1')
14
- }
15
-
16
- const config = await getConfig(root)
17
-
18
- const problemSets = []
19
- for (const path of config.paths) {
20
- if (path !== root || config.applyRootModuleFiles) {
21
- problemSets.push(await checkPackage(path))
22
- }
23
- if (path !== root || config.applyRootRepoFiles) {
24
- problemSets.push(await checkGitIgnore(path))
25
- }
26
- }
27
-
28
- const problems = problemSets.flat()
29
-
30
- if (problems.length) {
31
- console.error('Some problems were detected:')
32
- console.error()
33
- console.error(problems.map((problem) => problem.message).join('\n'))
34
- console.error()
35
- console.error('To correct them:')
36
- console.error(problems.map((problem) => problem.solution).join('\n'))
37
- process.exitCode = 1
38
- }
39
- }
40
-
41
- module.exports = main().catch((err) => {
42
- console.error(err.stack)
43
- process.exitCode = 1
44
- })
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const copyContent = require('../lib/postinstall/copy-content.js')
4
- const patchPackage = require('../lib/postinstall/update-package.js')
5
- const getConfig = require('../lib/config.js')
6
-
7
- const main = async () => {
8
- const {
9
- npm_config_global: globalMode,
10
- npm_config_local_prefix: root,
11
- } = process.env
12
-
13
- // do nothing in global mode or when the local prefix isn't set
14
- if (globalMode === 'true' || !root) {
15
- return
16
- }
17
-
18
- const config = await getConfig(root)
19
- for (const path of config.paths) {
20
- if (!await patchPackage(path, root, config)) {
21
- continue
22
- }
23
-
24
- await copyContent(path, root, config)
25
- }
26
- }
27
-
28
- module.exports = main().catch((err) => {
29
- console.error(err.stack)
30
- process.exitCode = 1
31
- })
@@ -1,48 +0,0 @@
1
- # This file is automatically added by @npmcli/template-oss. Do not edit.
2
-
3
- name: CI
4
-
5
- on:
6
- pull_request:
7
- push:
8
- branches:
9
- - main
10
- - latest
11
- schedule:
12
- # "At 02:00 on Monday" https://crontab.guru/#0_1_*_*_1
13
- - cron: "0 2 * * 1"
14
-
15
- jobs:
16
- lint:
17
- runs-on: ubuntu-latest
18
- steps:
19
- - uses: actions/checkout@v2
20
- - uses: actions/setup-node@v2
21
- with:
22
- node-version: '16'
23
- - run: npm i --prefer-online -g npm@latest
24
- - run: npm i
25
- - run: npm run lint
26
-
27
- test:
28
- strategy:
29
- fail-fast: false
30
- matrix:
31
- node-version: [12.13.0, 12.x, 14.15.0, 14.x, 16.13.0, 16.x]
32
- platform:
33
- - os: ubuntu-latest
34
- shell: bash
35
- - os: macos-latest
36
- shell: bash
37
- runs-on: ${{ matrix.platform.os }}
38
- defaults:
39
- run:
40
- shell: ${{ matrix.platform.shell }}
41
- steps:
42
- - uses: actions/checkout@v2
43
- - uses: actions/setup-node@v2
44
- with:
45
- node-version: ${{ matrix.node-version }}
46
- - run: npm i --prefer-online -g npm@latest
47
- - run: npm i
48
- - run: npm test --ignore-scripts
@@ -1,63 +0,0 @@
1
- name: Node Workspace CI %%pkgname%%
2
-
3
- on:
4
- pull_request:
5
- paths:
6
- - %%pkgpath%%/**
7
- push:
8
- paths:
9
- - %%pkgpath%%/**
10
- branches:
11
- - release-next
12
- - latest
13
- workflow_dispatch:
14
-
15
- jobs:
16
- lint:
17
- runs-on: ubuntu-latest
18
- steps:
19
- # Checkout the npm/cli repo
20
- - uses: actions/checkout@v2
21
- - uses: actions/setup-node@v2
22
- with:
23
- node-version: '16'
24
- - run: npm i --prefer-online -g npm@latest
25
- - run: npm i
26
- - run: npm run lint -w %%pkgpath%%
27
-
28
- test:
29
- strategy:
30
- fail-fast: false
31
- matrix:
32
- node-version: [12.13.0, 12.x, 14.15.0, 14.x, 16.13.0, 16.x]
33
- platform:
34
- - os: ubuntu-latest
35
- shell: bash
36
- - os: macos-latest
37
- shell: bash
38
- - os: windows-latest
39
- shell: cmd
40
- runs-on: ${{ matrix.platform.os }}
41
- defaults:
42
- run:
43
- shell: ${{ matrix.platform.shell }}
44
- steps:
45
- - uses: actions/checkout@v2
46
- - uses: actions/setup-node@v2
47
- with:
48
- node-version: ${{ matrix.node-version }}
49
- # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
50
- - name: Update to workable npm (windows)
51
- if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14'))
52
- run: |
53
- curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
54
- tar xf npm-7.5.4.tgz
55
- cd package
56
- node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
57
- cd ..
58
- rmdir /s /q package
59
- - name: Update npm
60
- run: npm i --prefer-online --no-fund --no-audit -g npm@latest
61
- - run: npm -v
62
- - run: npm i
63
- - run: npm test --ignore-scripts -w %%pkgpath%%
@@ -1,29 +0,0 @@
1
- # This file is automatically added by @npmcli/template-oss. Do not edit.
2
-
3
- name: Node Workspace Release Please %%pkgname%%
4
-
5
- on:
6
- push:
7
- paths:
8
- - %%pkgpath%%/**
9
- branches:
10
- - release-next
11
- - latest
12
-
13
- jobs:
14
- release-please:
15
- runs-on: ubuntu-latest
16
- steps:
17
- - uses: google-github-actions/release-please-action@v2
18
- id: release
19
- with:
20
- release-type: node
21
- monorepo-tags: true
22
- path: %%pkgpath%%
23
- # If you change changelog-types be sure to also update commitlintrc.js
24
- changelog-types: >
25
- [{"type":"feat","section":"Features","hidden":false},
26
- {"type":"fix","section":"Bug Fixes","hidden":false},
27
- {"type":"docs","section":"Documentation","hidden":false},
28
- {"type":"deps","section":"Dependencies","hidden":false},
29
- {"type":"chore","hidden":true}]