@npmcli/template-oss 3.0.0 → 3.1.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.
@@ -1,29 +1,48 @@
1
+ const log = require('proc-log')
2
+ const npa = require('npm-package-arg')
3
+ const { partition } = require('lodash')
1
4
  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]
5
+
6
+ const rmCommand = (specs) =>
7
+ `npm rm ${specs.map((s) => s.name).join(' ')}`.trim()
8
+
9
+ const installCommand = (specs, flags) => specs.length ?
10
+ `npm i ${specs.map((s) => `${s.name}@${s.fetchSpec}`).join(' ')} ${flags.join(' ')}`.trim() : ''
11
+
12
+ // ensure required packages are present in the correct place
13
+ const run = ({ pkg, path, config: { requiredPackages = {} } }) => {
14
+ // keys are the dependency location in package.json
15
+ // values are a filtered list of parsed specs that dont exist in the current package
16
+ // { [location]: [spec1, spec2] }
17
+ const requiredByLocation = Object.entries(requiredPackages)
18
+ .reduce((acc, [location, pkgs]) => {
19
+ acc[location] = pkgs
20
+ .filter((spec) => !hasPackage(pkg, spec, [location], path))
21
+ .map((spec) => npa(spec))
22
+ log.verbose(location, pkg, pkgs)
23
+ return acc
24
+ }, {})
25
+
26
+ const requiredEntries = Object.entries(requiredByLocation)
27
+
28
+ log.verbose('check-required', requiredEntries)
29
+
30
+ if (requiredEntries.flatMap(([, specs]) => specs).length) {
31
+ return requiredEntries.map(([location, specs]) => {
32
+ const locationFlag = hasPackage.flags[location]
33
+ const [exactSpecs, saveSpecs] = partition(specs, (s) => s.type === 'version')
34
+
35
+ log.verbose('check-required', location, specs)
21
36
 
22
37
  return {
23
38
  title: `The following required ${location} were not found:`,
24
- body: specs.map(({ name, version }) => `${name}@${version}`),
39
+ body: specs.map((s) => s.rawSpec ? `${s.name}@${s.rawSpec}` : s.name),
25
40
  // 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(' && '),
41
+ solution: [
42
+ rmCommand(specs),
43
+ installCommand(saveSpecs, [locationFlag]),
44
+ installCommand(exactSpecs, [locationFlag, '--save-exact']),
45
+ ].filter(Boolean).join(' && '),
27
46
  }
28
47
  })
29
48
  }
@@ -11,6 +11,6 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  {{> setupGit}}
14
- {{> setupNode }}
14
+ {{> setupNode}}
15
15
  - run: npm i --package-lock
16
16
  - run: npm audit
@@ -7,7 +7,7 @@ on:
7
7
  - '*'
8
8
  {{#if pkgRelPath}}
9
9
  paths:
10
- - {{pkgRelPath}}
10
+ - {{pkgRelPath}}/**
11
11
  {{/if}}
12
12
  push:
13
13
  branches:
@@ -16,7 +16,7 @@ on:
16
16
  {{/each}}
17
17
  {{#if pkgRelPath}}
18
18
  paths:
19
- - {{pkgRelPath}}
19
+ - {{pkgRelPath}}/**
20
20
  {{/if}}
21
21
  schedule:
22
22
  # "At 02:00 on Monday" https://crontab.guru/#0_2_*_*_1
@@ -54,6 +54,6 @@ jobs:
54
54
  shell: $\{{ matrix.platform.shell }}
55
55
  steps:
56
56
  {{> setupGit}}
57
- {{> setupNode}}
57
+ {{> setupNode useMatrix=true}}
58
58
  - run: npm i
59
59
  - run: npm test --ignore-scripts {{~#if isWorkspace}} -w {{pkgName}}{{/if}}
@@ -7,7 +7,7 @@ updates:
7
7
  interval: daily
8
8
  allow:
9
9
  - dependency-type: direct
10
- versioning-strategy: increase
10
+ versioning-strategy: increase-if-necessary
11
11
  commit-message:
12
12
  prefix: deps
13
13
  prefix-development: chore
@@ -1,3 +1,5 @@
1
+ const { name: NAME, version: LATEST_VERSION } = require('../../package.json')
2
+
1
3
  // Changes applied to the root of the repo
2
4
  const rootRepo = {
3
5
  add: {
@@ -26,7 +28,7 @@ const rootModule = {
26
28
  '.gitignore': 'gitignore',
27
29
  '.npmrc': 'npmrc',
28
30
  'SECURITY.md': 'SECURITY.md',
29
- 'package.json': 'package.json',
31
+ 'package.json': 'pkg.json',
30
32
  },
31
33
  rm: [
32
34
  '.eslintrc.!(js|local.*)',
@@ -37,7 +39,7 @@ const rootModule = {
37
39
  const workspaceRepo = {
38
40
  add: {
39
41
  '.github/workflows/release-please-{{pkgNameFs}}.yml': 'release-please.yml',
40
- '.github/workflows/ci-{{pkgNameFs}}.yml': 'ci.yml'
42
+ '.github/workflows/ci-{{pkgNameFs}}.yml': 'ci.yml',
41
43
  },
42
44
  }
43
45
 
@@ -46,7 +48,7 @@ const workspaceModule = {
46
48
  add: {
47
49
  '.eslintrc.js': 'eslintrc.js',
48
50
  '.gitignore': 'gitignore',
49
- 'package.json': 'package.json',
51
+ 'package.json': 'pkg.json',
50
52
  },
51
53
  rm: [
52
54
  '.npmrc',
@@ -73,18 +75,18 @@ module.exports = {
73
75
  'standard',
74
76
  ],
75
77
  requiredPackages: {
76
- devDependencies: {
77
- '@npmcli/template-oss': '*',
78
- '@npmcli/eslint-config': '>=3.0.0',
79
- tap: '>=15.0.0',
80
- },
78
+ devDependencies: [
79
+ `${NAME}@${LATEST_VERSION}`,
80
+ '@npmcli/eslint-config',
81
+ 'tap',
82
+ ],
81
83
  },
82
84
  allowedPackages: [],
83
85
  changelogTypes: [
84
- { type: "feat", section: "Features", hidden: false },
85
- { type: "fix", section: "Bug Fixes", hidden: false },
86
- { type: "docs", section: "Documentation", hidden: false },
87
- { type: "deps", section: "Dependencies", hidden: false },
88
- { type: "chore", hidden: true },
86
+ { type: 'feat', section: 'Features', hidden: false },
87
+ { type: 'fix', section: 'Bug Fixes', hidden: false },
88
+ { type: 'docs', section: 'Documentation', hidden: false },
89
+ { type: 'deps', section: 'Dependencies', hidden: false },
90
+ { type: 'chore', hidden: true },
89
91
  ],
90
92
  }
File without changes
@@ -20,7 +20,7 @@ jobs:
20
20
  with:
21
21
  github-token: "$\{{ secrets.GITHUB_TOKEN }}"
22
22
  - name: npm install and commit
23
- if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss')
23
+ if: contains(steps.metadata.outputs.dependency-names, '{{__NAME__}}')
24
24
  env:
25
25
  GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }}
26
26
  run: |
@@ -16,13 +16,10 @@ jobs:
16
16
  {{> setupGit with=(obj fetch-depth=0)}}
17
17
  {{> setupNode}}
18
18
  - name: Install deps
19
- run: |
20
- npm i -D @commitlint/cli @commitlint/config-conventional
19
+ run: npm i -D @commitlint/cli @commitlint/config-conventional
21
20
  - name: Check commits OR PR title
22
21
  env:
23
22
  PR_TITLE: $\{{ github.event.pull_request.title }}
24
23
  run: |
25
- npx commitlint -x @commitlint/config-conventional -V \
26
- --from origin/main --to $\{{ github.event.pull_request.head.sha }} \
27
- || echo $PR_TITLE | \
28
- npx commitlint -x @commitlint/config-conventional -V
24
+ npx --offline commitlint -V --from origin/main --to $\{{ github.event.pull_request.head.sha }} \
25
+ || echo $PR_TITLE | npx --offline commitlint -V
@@ -1,9 +1,10 @@
1
1
  - uses: actions/setup-node@v3
2
2
  with:
3
- node-version: {{#each ciVersions}}{{#if @last}}{{.}}{{/if}}{{/each}}
3
+ node-version: {{#if useMatrix}}$\{{ matrix.node-version }}{{else}}{{#each ciVersions}}{{#if @last}}{{.}}{{/if}}{{/each}}{{/if}}
4
+ {{#if useMatrix}}
4
5
  - name: Update to workable npm (windows)
5
6
  # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
6
- if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14'))
7
+ if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
7
8
  run: |
8
9
  curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
9
10
  tar xf npm-7.5.4.tgz
@@ -13,9 +14,12 @@
13
14
  rmdir /s /q package
14
15
  - name: Update npm to 7
15
16
  # If we do test on npm 10 it needs npm7
16
- if: matrix.node-version <= 10
17
+ if: startsWith(matrix.node-version, '10.')
17
18
  run: npm i --prefer-online --no-fund --no-audit -g npm@7
18
19
  - name: Update npm to latest
19
- if: matrix.node-version > 10
20
+ if: $\{{ !startsWith(matrix.node-version, '10.') }}
21
+ {{else}}
22
+ - name: Update npm to latest
23
+ {{/if}}
20
24
  run: npm i --prefer-online --no-fund --no-audit -g npm@latest
21
25
  - run: npm -v
@@ -1,5 +1,7 @@
1
- const intersects = require('semver/ranges/intersects')
1
+ const semver = require('semver')
2
+ const npa = require('npm-package-arg')
2
3
  const { has } = require('lodash')
4
+ const { join } = require('path')
3
5
 
4
6
  const installLocations = [
5
7
  'dependencies',
@@ -9,20 +11,71 @@ const installLocations = [
9
11
  'optionalDependencies',
10
12
  ]
11
13
 
14
+ // from a spec get either a semver version or range. it gets parsed with npa and
15
+ // only a few appropriate types are handled. eg this doesnt match any git
16
+ // shas/tags, etc
17
+ const getSpecVersion = (spec, where) => {
18
+ const arg = npa(spec, where)
19
+ switch (arg.type) {
20
+ case 'range':
21
+ return new semver.Range(arg.fetchSpec)
22
+ case 'tag': {
23
+ // special case an empty spec to mean any version
24
+ return arg.rawSpec === '' && new semver.Range('*')
25
+ }
26
+ case 'version':
27
+ return new semver.SemVer(arg.fetchSpec)
28
+ case 'directory': {
29
+ // allows this repo to use a file spec as a devdep and pass this check
30
+ const pkg = require(join(arg.fetchSpec, 'package.json'))
31
+ return new semver.SemVer(pkg.version)
32
+ }
33
+ }
34
+ return null
35
+ }
36
+
37
+ const isVersion = (s) => s instanceof semver.SemVer
38
+
39
+ // Returns whether the pkg has the dependency in a semver
40
+ // compatible version in one or more locationscccc
12
41
  const hasPackage = (
13
42
  pkg,
14
- name,
15
- version = '*',
16
- locations = installLocations
17
- ) => locations
18
- .map((l) => pkg[l])
19
- .some((deps) =>
20
- has(deps, name) &&
21
- (version === '*' || intersects(deps[name], version))
22
- )
43
+ spec,
44
+ locations = installLocations,
45
+ path
46
+ ) => {
47
+ const name = npa(spec).name
48
+ const requested = getSpecVersion(spec)
23
49
 
24
- module.exports = hasPackage
50
+ if (!requested) {
51
+ return false
52
+ }
25
53
 
54
+ const existingByLocation = locations
55
+ .map((location) => pkg[location])
56
+ .filter((deps) => has(deps, name))
57
+ .map((deps) => getSpecVersion(`${name}@${deps[name]}`, path))
58
+ .filter(Boolean)
59
+
60
+ return existingByLocation.some((existing) => {
61
+ switch ([existing, requested].map((t) => isVersion(t) ? 'VER' : 'RNG').join('-')) {
62
+ case `VER-VER`:
63
+ // two versions, use semver.eq to check equality
64
+ return semver.eq(existing, requested)
65
+ case `RNG-RNG`:
66
+ // two ranges, existing must be entirely within the requested
67
+ return semver.subset(existing, requested)
68
+ case `VER-RNG`:
69
+ // requesting a range with existing version is ok if it satisfies
70
+ return semver.satisfies(existing, requested)
71
+ case `RNG-VER`:
72
+ // requesting a pinned version but has a range, always false
73
+ return false
74
+ }
75
+ })
76
+ }
77
+
78
+ module.exports = hasPackage
26
79
  module.exports.flags = installLocations.reduce((acc, location) => {
27
80
  const type = location.replace(/dependencies/i, '')
28
81
  acc[location] = '--save' + (type ? `-${type}` : '')
@@ -4,11 +4,10 @@ const { diff } = require('just-diff')
4
4
 
5
5
  const j = (obj, replacer = null) => JSON.stringify(obj, replacer, 2)
6
6
 
7
- const jsonDiff = (s1, s2, DELETE) => {
8
- // DELETE is a special string that will be the value of updated if it exists
9
- // but should be deleted
10
-
11
- const ops = diff(s1, s2).map(({ op, path, value }) => {
7
+ // DELETE is a special string that will be the value of updated if it exists
8
+ // but should be deleted
9
+ const jsonDiff = (s1, s2, DELETE) => diff(s1, s2)
10
+ .map(({ op, path, value }) => {
12
11
  // there could be cases where a whole object is reported
13
12
  // as missing and the expected value does not need to show
14
13
  // special DELETED values so filter those out here
@@ -26,13 +25,9 @@ const jsonDiff = (s1, s2, DELETE) => {
26
25
  } else if (op === 'add' && value !== DELETE) {
27
26
  return AD
28
27
  }
29
- }).filter(Boolean).sort((a, b) => a.localeCompare(b))
30
-
31
- if (!ops.length) {
32
- return true
33
- }
34
-
35
- return ops.join('\n')
36
- }
28
+ })
29
+ .filter(Boolean)
30
+ .sort((a, b) => a.localeCompare(b))
31
+ .join('\n')
37
32
 
38
33
  module.exports = jsonDiff
@@ -77,7 +77,7 @@ class Base {
77
77
  // create a patch and strip out the filename. if it ends up an empty string
78
78
  // then return true since the files are equal
79
79
  return Diff.createPatch('', t.replace(/\r\n/g, '\n'), s.replace(/\r\n/g, '\n'))
80
- .split('\n').slice(4).join('\n').trim() || true
80
+ .split('\n').slice(4).join('\n')
81
81
  }
82
82
 
83
83
  diff (t, s) {
@@ -142,9 +142,10 @@ class Base {
142
142
  ].join('\n')
143
143
  }
144
144
 
145
- // individual diff methods are responsible for formatting or returning
146
- // true if the are equal
147
- return this.diff(target, source)
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
148
149
  }
149
150
  }
150
151
 
@@ -224,7 +225,7 @@ class JsonMerge extends Json {
224
225
  }
225
226
 
226
227
  class PackageJson extends JsonMerge {
227
- static types = ['package.json']
228
+ static types = ['pkg.json']
228
229
 
229
230
  async prepare (s, t) {
230
231
  // merge new source with current pkg content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/template-oss",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "templated files used in npm CLI team oss projects",
5
5
  "main": "lib/content/index.js",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "json-parse-even-better-errors": "^2.3.1",
41
41
  "just-diff": "^5.0.1",
42
42
  "lodash": "^4.17.21",
43
+ "npm-package-arg": "^9.0.1",
43
44
  "proc-log": "^2.0.0",
44
45
  "semver": "^7.3.5",
45
46
  "yaml": "^2.0.0-10"
@@ -58,8 +59,5 @@
58
59
  },
59
60
  "engines": {
60
61
  "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
61
- },
62
- "eslintIgnore": [
63
- "lib/content/"
64
- ]
62
+ }
65
63
  }