@npmcli/template-oss 2.9.0 → 3.0.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 (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 +36 -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 +90 -0
  26. package/lib/content/npmrc +0 -2
  27. package/lib/content/package.json +27 -0
  28. package/lib/content/post-dependabot.yml +16 -15
  29. package/lib/content/pull-request.yml +12 -11
  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 +21 -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 +30 -0
  37. package/lib/util/json-diff.js +38 -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 +279 -0
  41. package/lib/util/template.js +41 -0
  42. package/package.json +29 -26
  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
package/README.md CHANGED
@@ -7,106 +7,115 @@ single devDependency.
7
7
 
8
8
  ### Configuration
9
9
 
10
- Configure the use of `template-oss` in the root `package.json`.
10
+ Configure the use of `@npmcli/template-oss` in your `package.json` using the
11
+ `templateOSS` property.
12
+
11
13
 
12
14
  ```js
13
15
  {
14
16
  name: 'my-package',
15
- // ...
16
17
  templateOSS: {
17
18
  // copy repo specific files for the root pkg
18
- applyRootRepoFiles: true,
19
+ rootRepo: true,
19
20
  // modify package.json and copy module specific files for the root pkg
20
- applyRootModuleFiles: true,
21
- // copy repo files for each whitelisted workspaces
22
- applyWorkspaceRepoFiles: true,
23
- // whitelist workspace by package name to modify package.json
24
- // and copy module files
21
+ rootModule: true,
22
+ // copy repo files for all workspaces
23
+ workspaceRepo: true,
24
+ // copy module files for all workspaces
25
+ workspaceModule: true,
26
+ // filter allowed workspaces by package name
27
+ // defaults to all workspaces
25
28
  workspaces: ['workspace-package-name'],
26
- version: '2.3.1'
29
+ // The rest of the config is passed in as variables
30
+ // that can be used to template files in the content
31
+ // directory. Some common ones are:
32
+ // Turns off ci in windows
33
+ windowsCI: false,
34
+ // Change the versions tested in CI and engines
35
+ ciVersions: ['10', '12', '14']
27
36
  }
28
37
  }
38
+ ```
29
39
 
30
- ### `package.json` patches
40
+ #### Workspaces
31
41
 
32
- These fields will be set in the project's `package.json`:
42
+ Individual workspaces can also supply their own config, if they are included by
43
+ the root package's `templateOSS.workspaces` array. These settings will override
44
+ any of the same settings in the root.
33
45
 
34
46
  ```js
35
47
  {
36
- author: 'GitHub Inc.',
37
- files: ['bin', 'lib'],
38
- license: 'ISC',
39
- templateVersion: $TEMPLATE_VERSION,
40
- scripts: {
41
- lint: `eslint '**/*.js'`,
42
- postlint: 'npm-template-check',
43
- lintfix: 'npm run lint -- --fix',
44
- 'template-copy': 'npm-template-copy --force',
45
- preversion: 'npm test',
46
- postversion: 'npm publish',
47
- prepublishOnly: 'git push origin --follow-tags',
48
- snap: 'tap',
49
- test: 'tap',
50
- posttest: 'npm run lint',
51
- },
52
- engines: {
53
- node: '^12.13.0 || ^14.15.0 || >=16',
54
- },
48
+ name: 'my-workspace',
49
+ templateOSS: {
50
+ // copy repo files for this workspace
51
+ workspaceRepo: true,
52
+ // copy module files for this workspace
53
+ moduleRepo: true,
54
+ // Changes windowsCI setting for this workspace
55
+ windowsCI: false,
56
+ }
55
57
  }
56
58
  ```
57
59
 
58
- The `"templateVersion"` field will be set to the version of this package being
59
- installed. This is used to determine if the postinstall script should take any
60
- action.
60
+ ### Content
61
+
62
+ All the templated content for this repo lives in
63
+ [`lib/content/`](./lib/content/). The `index.js`[./lib/content/index.js] file
64
+ controls how and where this content is written.
65
+
66
+ Content files can be overwritten or merged with the existing target file.
67
+ Currently mergining is only supported for `package.json` files.
61
68
 
62
- #### Extending
69
+ Each content file goes through the following pipeline:
63
70
 
64
- The `changes` constant located in `lib/postinstall/update-package.js` should contain
65
- all patches for the `package.json` file. Be sure to correctly expand any object/array
66
- based values with the original package content.
71
+ 1. It is read from its source location
72
+ 1. It is are templated using Handlebars with the variables from each packages's
73
+ config (with some derived values generated in [`config.js`](./lib/config.js)
74
+ 1. It is parsed based on its file extension in
75
+ [`parser.js`](./lib/util/parser.js)
76
+ 1. Additional logic is applied by the parser
77
+ 1. It is written to its target location
67
78
 
68
- ### Static files
79
+ ### Usage
69
80
 
70
- Any existing `.eslintrc.*` files will be removed, unless they also match the
71
- pattern `.eslintrc.local.*`
81
+ This package provides two bin scripts:
72
82
 
73
- These files will be copied, overwriting any existing files:
83
+ #### `template-oss-check`
74
84
 
75
- - `.eslintrc.js`
76
- - `.github/workflows/ci.yml`
77
- - `.github/ISSUE_TEMPLATE/bug.yml`
78
- - `.github/ISSUE_TEMPLATE/config.yml`
79
- - `.github/CODEOWNERS`
80
- - `.gitignore`
81
- - `LICENSE.md`
82
- - `SECURITY.md`
85
+ This will check if any of the applied files different from the target content,
86
+ or if any of the other associated checks fail. The diffs of each file or check
87
+ will be reported with instructions on how to fix it.
83
88
 
84
- ### Dynamic Files
89
+ #### `template-oss-apply [--force]`
85
90
 
86
- Currently, the only dynamic file generated is a github workflow for a given workspace.
87
- `.github/workflows/ci-$$package-name$$.yml`
91
+ This will write all source files to their target locations in the cwd. It will
92
+ do nothing if `package.json#templateOSS.version` is the same as the version
93
+ being run. This can be overridden by `--force`.
88
94
 
89
- #### Extending
95
+ This is the script that is run on `postinsall`.
90
96
 
91
- Place files in the `lib/content/` directory, use only the file name and remove
92
- any leading `.` characters (i.e. `.github/workflows/ci.yml` becomes `ci.yml`
93
- and `.gitignore` becomes `gitignore`).
97
+ ### Extending
94
98
 
95
- Modify the `repoFiles` and `moduleFiles` objects at the top of `lib/postinstall/copy-content.js` to include
96
- your new file. The object keys are destination paths, and values are source.
99
+ #### `lib/apply`
97
100
 
98
- ### `package.json` checks
101
+ This directory is where all the logic for applying files lives. It should be
102
+ possible to add new files without modifying anything in this directory. To add a
103
+ file, add the templated file to `lib/content/$FILENAME` and add entry for it in
104
+ `lib/content/index.js` depending on where and when it should be written (root vs
105
+ workspace, repo vs module, add vs remove, etc).
99
106
 
100
- `npm-template-check` is run by `postlint` and will error if the `package.json`
101
- is not configured properly, with steps to run to correct any problems.
107
+ #### `lib/check`
102
108
 
103
- ### Manual copy
109
+ All checks live in this directory and have the same signature. A check must be
110
+ added to `lib/check/index.js` for it to be run.
104
111
 
105
- Template files will be copied automatically when `template-oss` is updated.
106
- You can force an update with `npm run template-copy`.
112
+ #### Generic vs specific extensions
107
113
 
108
- #### Extending
114
+ This repo is designed so that all (fine, most) of the logic in `lib/` is generic
115
+ and could be applied across projects of many different types.
109
116
 
110
- Add any unwanted packages to `unwantedPackages` in `lib/check.js`. Currently
111
- the best way to install any packages is to include them as `peerDependencies`
112
- in this repo.
117
+ The files in `lib/content` are extremely specific to the npm CLI. It would be
118
+ trivial to swap out this content directory for a different one as it is only
119
+ referenced in a single place in `lib/config.js`. However, it's not currently
120
+ possible to change this value at runtime, but that might become possible in
121
+ future versions of this package.
package/bin/apply.js ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ const apply = require('../lib/apply/index.js')
4
+
5
+ const main = async () => {
6
+ const {
7
+ npm_config_global: globalMode,
8
+ npm_config_local_prefix: root,
9
+ } = process.env
10
+
11
+ // do nothing in global mode or when the local prefix isn't set
12
+ if (globalMode === 'true' || !root) {
13
+ return
14
+ }
15
+
16
+ await apply(root)
17
+ }
18
+
19
+ module.exports = main().catch((err) => {
20
+ console.error(err.stack)
21
+ process.exitCode = 1
22
+ })
package/bin/check.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ const check = require('../lib/check/index.js')
4
+ const output = require('../lib/util/output.js')
5
+
6
+ const main = async () => {
7
+ const {
8
+ npm_config_local_prefix: root,
9
+ } = process.env
10
+
11
+ if (!root) {
12
+ throw new Error('This package requires npm >7.21.1')
13
+ }
14
+
15
+ const problems = await check(root)
16
+
17
+ if (problems.length) {
18
+ process.exitCode = 1
19
+ console.error(output(problems))
20
+ }
21
+ }
22
+
23
+ module.exports = main().catch((err) => {
24
+ console.error(err.stack)
25
+ process.exitCode = 1
26
+ })
@@ -0,0 +1,31 @@
1
+ const fs = require('@npmcli/fs')
2
+ const log = require('proc-log')
3
+ const { rmEach, parseEach } = require('../util/files.js')
4
+
5
+ const run = async (dir, files, options) => {
6
+ const { rm = [], add = {} } = files
7
+
8
+ log.verbose('apply-files', 'rm', rm)
9
+ await rmEach(dir, rm, options, (f) => fs.rm(f))
10
+
11
+ log.verbose('apply-files', 'add', add)
12
+ await parseEach(dir, add, options, (p) => p.applyWrite())
13
+ }
14
+
15
+ module.exports = [{
16
+ run: (options) => run(
17
+ options.config.repoDir,
18
+ options.config.repoFiles,
19
+ options
20
+ ),
21
+ when: ({ config: c }) => c.isForce || (c.needsUpdate && c.applyRepo),
22
+ name: 'apply-repo',
23
+ }, {
24
+ run: (options) => run(
25
+ options.config.moduleDir,
26
+ options.config.moduleFiles,
27
+ options
28
+ ),
29
+ when: ({ config: c }) => c.isForce || (c.needsUpdate && c.applyModule),
30
+ name: 'apply-module',
31
+ }]
@@ -0,0 +1,5 @@
1
+ const run = require('../index.js')
2
+
3
+ module.exports = (root, content) => run(root, content, [
4
+ require('./apply-files.js'),
5
+ ])
@@ -0,0 +1,73 @@
1
+ const log = require('proc-log')
2
+ const { relative, basename } = require('path')
3
+ const { rmEach, parseEach } = require('../util/files.js')
4
+ const { partition } = require('lodash')
5
+
6
+ const solution = 'npx template-oss-apply --force'
7
+
8
+ const run = async (type, dir, files, options) => {
9
+ const res = []
10
+ const rel = (f) => relative(options.root, f)
11
+ const { add: addFiles = {}, rm: rmFiles = [] } = files
12
+
13
+ const rm = await rmEach(dir, rmFiles, options, (f) => rel(f))
14
+ const [add, update] = partition(await parseEach(dir, addFiles, options, async (p) => {
15
+ const diff = await p.applyDiff()
16
+ const target = rel(p.target)
17
+ if (diff === null) {
18
+ // needs to be added
19
+ return target
20
+ } else if (diff === true) {
21
+ // its ok, no diff, this is filtered out
22
+ return null
23
+ }
24
+ return { file: target, diff }
25
+ }), (d) => typeof d === 'string')
26
+
27
+ log.verbose('check-apply', 'rm', rm)
28
+ if (rm.length) {
29
+ res.push({
30
+ title: `The following ${type} files need to be deleted:`,
31
+ body: rm,
32
+ solution,
33
+ })
34
+ }
35
+
36
+ log.verbose('check-apply', 'add', add)
37
+ if (add.length) {
38
+ res.push({
39
+ title: `The following ${type} files need to be added:`,
40
+ body: add,
41
+ solution,
42
+ })
43
+ }
44
+
45
+ log.verbose('check-apply', 'update', update)
46
+ res.push(...update.map(({ file, diff }) => ({
47
+ title: `The ${type} file ${basename(file)} needs to be updated:`,
48
+ body: [`${file}\n${'='.repeat(40)}\n${diff}`],
49
+ solution,
50
+ })))
51
+
52
+ return res
53
+ }
54
+
55
+ module.exports = [{
56
+ run: (options) => run(
57
+ 'repo',
58
+ options.config.repoDir,
59
+ options.config.repoFiles,
60
+ options
61
+ ),
62
+ when: ({ config: c }) => c.applyRepo,
63
+ name: 'check-repo',
64
+ }, {
65
+ run: (options) => run(
66
+ 'module',
67
+ options.config.moduleDir,
68
+ options.config.moduleFiles,
69
+ options
70
+ ),
71
+ when: ({ config: c }) => c.applyModule,
72
+ name: 'check-module',
73
+ }]
@@ -0,0 +1,31 @@
1
+ const fs = require('@npmcli/fs')
2
+ const { EOL } = require('os')
3
+ const { join, relative } = require('path')
4
+
5
+ const run = async ({ root, path }) => {
6
+ // XXX: our changelogs are always markdown
7
+ // but they could be other extensions so
8
+ // make this glob for possible matches
9
+ const changelog = join(path, 'CHANGELOG.md')
10
+
11
+ if (await fs.exists(changelog)) {
12
+ const content = await fs.readFile(changelog, { encoding: 'utf8' })
13
+ const mustStart = `# Changelog${EOL}${EOL}#`
14
+ if (!content.startsWith(mustStart)) {
15
+ return {
16
+ title: `The ${relative(root, changelog)} is incorrect:`,
17
+ body: [
18
+ 'The changelog should start with',
19
+ `"${mustStart}"`,
20
+ ],
21
+ solution: 'reformat the changelog to have the correct heading',
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ module.exports = {
28
+ run,
29
+ when: ({ config: c }) => c.applyModule,
30
+ name: 'check-changelog',
31
+ }
@@ -0,0 +1,67 @@
1
+ const log = require('proc-log')
2
+ const { EOL } = require('os')
3
+ const { resolve, relative, basename } = require('path')
4
+ const fs = require('@npmcli/fs')
5
+ const git = require('@npmcli/git')
6
+
7
+ const NAME = 'check-gitignore'
8
+
9
+ // The problem we are trying to solve is when a new .gitignore file
10
+ // is copied into an existing repo, there could be files already checked in
11
+ // to git that are now ignored by new gitignore rules. We want to warn
12
+ // about these files.
13
+ const run = async ({ root, path, config }) => {
14
+ log.verbose(NAME, { root, path })
15
+
16
+ const relativeToRoot = (f) => relative(root, resolve(path, f))
17
+
18
+ // use the root to detect a git repo but the project directory (path) for the
19
+ // ignore check
20
+ const ignoreFile = resolve(path, '.gitignore')
21
+ if (!await git.is({ cwd: root }) || !await fs.exists(ignoreFile)) {
22
+ log.verbose(NAME, 'no git or no gitignore')
23
+ return null
24
+ }
25
+
26
+ log.verbose(NAME, `using ignore file ${ignoreFile}`)
27
+
28
+ const res = await git.spawn([
29
+ 'ls-files',
30
+ '--cached',
31
+ '--ignored',
32
+ // https://git-scm.com/docs/git-ls-files#_exclude_patterns
33
+ `--${config.isRoot ? 'exclude-from' : 'exclude-per-directory'}=${basename(ignoreFile)}`,
34
+ ], { cwd: path })
35
+
36
+ log.verbose(NAME, 'ls-files', res)
37
+
38
+ // TODO: files should be filtered if they have already been moved/deleted
39
+ // but not committed. Currently you must commit for this check to pass.
40
+ const files = res.stdout
41
+ .trim()
42
+ .split('\n')
43
+ .filter(Boolean)
44
+
45
+ if (!files.length) {
46
+ return null
47
+ }
48
+
49
+ const ignores = (await fs.readFile(ignoreFile))
50
+ .toString()
51
+ .split(EOL)
52
+ .filter((l) => l && !l.trim().startsWith('#'))
53
+
54
+ const relIgnore = relativeToRoot(ignoreFile)
55
+
56
+ return {
57
+ title: `The following files are tracked by git but matching a pattern in ${relIgnore}:`,
58
+ body: files.map(relativeToRoot),
59
+ solution: ['move files to not match one of the following patterns:', ...ignores],
60
+ }
61
+ }
62
+
63
+ module.exports = {
64
+ run,
65
+ when: ({ config: c }) => c.applyModule,
66
+ name: NAME,
67
+ }
@@ -0,0 +1,36 @@
1
+ const hasPackage = require('../util/has-package.js')
2
+ const { groupBy } = require('lodash')
3
+
4
+ const run = ({ pkg, config: { requiredPackages = {} } }) => {
5
+ // ensure required packages are present in the correct place
6
+
7
+ const mustHave = Object.entries(requiredPackages).flatMap(([location, pkgs]) =>
8
+ // first make a flat array of {name, version, location}
9
+ Object.entries(pkgs).map(([name, version]) => ({
10
+ name,
11
+ version,
12
+ location,
13
+ })))
14
+ .filter(({ name, version, location }) => !hasPackage(pkg, name, version, [location]))
15
+
16
+ if (mustHave.length) {
17
+ return Object.entries(groupBy(mustHave, 'location')).map(([location, specs]) => {
18
+ const rm = specs.map(({ name }) => name).join(' ')
19
+ const install = specs.map(({ name, version }) => `${name}@${version}`).join(' ')
20
+ const installLocation = hasPackage.flags[location]
21
+
22
+ return {
23
+ title: `The following required ${location} were not found:`,
24
+ body: specs.map(({ name, version }) => `${name}@${version}`),
25
+ // solution is to remove any existing all at once but add back in by --save-<location>
26
+ solution: [`npm rm ${rm}`, `npm i ${install} ${installLocation}`].join(' && '),
27
+ }
28
+ })
29
+ }
30
+ }
31
+
32
+ module.exports = {
33
+ run,
34
+ when: ({ config: c }) => c.applyModule,
35
+ name: 'check-required-packages',
36
+ }
@@ -0,0 +1,23 @@
1
+
2
+ const hasPackage = require('../util/has-package.js')
3
+
4
+ const run = ({ pkg, config: { allowedPackages = [], unwantedPackages = [] } }) => {
5
+ // ensure packages that should not be present are removed
6
+ const hasUnwanted = unwantedPackages
7
+ .filter((name) => !allowedPackages.includes(name))
8
+ .filter((name) => hasPackage(pkg, name))
9
+
10
+ if (hasUnwanted.length) {
11
+ return {
12
+ title: 'The following unwanted packages were found:',
13
+ body: hasUnwanted,
14
+ solution: `npm rm ${hasUnwanted.join(' ')}`,
15
+ }
16
+ }
17
+ }
18
+
19
+ module.exports = {
20
+ run,
21
+ when: ({ config: c }) => c.applyModule,
22
+ name: 'check-unwanted-packages',
23
+ }
@@ -0,0 +1,9 @@
1
+ const run = require('../index.js')
2
+
3
+ module.exports = (root, content) => run(root, content, [
4
+ require('./check-apply.js'),
5
+ require('./check-required.js'),
6
+ require('./check-unwanted.js'),
7
+ require('./check-gitignore.js'),
8
+ require('./check-changelog.js'),
9
+ ])