diff --git a/.commitlintrc.js b/.commitlintrc.js index cf8f3d76..5b0b1a52 100644 --- a/.commitlintrc.js +++ b/.commitlintrc.js @@ -1,10 +1,9 @@ -// This file is automatically added by @npmcli/template-oss. Do not edit. +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ module.exports = { extends: ['@commitlint/config-conventional'], - // If you change rules be sure to also update release-please.yml rules: { - 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'chore', 'deps']], + 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], 'header-max-length': [2, 'always', 80], 'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']], }, diff --git a/.eslintrc.js b/.eslintrc.js index 022767bc..8204f234 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,4 @@ -// This file is automatically added by @npmcli/template-oss. Do not edit. +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ const { readdirSync: readdir } = require('fs') diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ef874313..2c54b0d2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,3 @@ -* @npm/cli-team +# This file is automatically added by @npmcli/template-oss. Do not edit. + +* @npm/cli-team diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index fa80b2de..d043192f 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -3,52 +3,52 @@ name: Bug description: File a bug/issue title: "[BUG] " -labels: [Bug, Needs Triage] +labels: [ Bug, Needs Triage ] + body: -- type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please [search here](./issues) to see if an issue already exists for your problem. - options: - - label: I have searched the existing issues - required: true -- type: textarea - attributes: - label: Current Behavior - description: A clear & concise description of what you're experiencing. - validations: - required: false -- type: textarea - attributes: - label: Expected Behavior - description: A clear & concise description of what you expected to happen. - validations: - required: false -- type: textarea - attributes: - label: Steps To Reproduce - description: Steps to reproduce the behavior. - value: | - 1. In this environment... - 2. With this config... - 3. Run '...' - 4. See error... - validations: - required: false -- type: textarea - attributes: - label: Environment - description: | - examples: - - **npm**: 7.6.3 - - **Node**: 13.14.0 - - **OS**: Ubuntu 20.04 - - **platform**: Macbook Pro - value: | + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please [search here](./issues) to see if an issue already exists for your problem. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A clear & concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A clear & concise description of what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + value: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + examples: + - **npm**: 7.6.3 + - **Node**: 13.14.0 + - **OS**: Ubuntu 20.04 + - **platform**: Macbook Pro + value: | - npm: - Node: - OS: - platform: - validations: - required: false - + validations: + required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4c8e23f4..d4d25432 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,17 @@ # This file is automatically added by @npmcli/template-oss. Do not edit. version: 2 + updates: -- package-ecosystem: npm - directory: "/" - schedule: - interval: daily - allow: - - dependency-type: direct - versioning-strategy: increase - commit-message: - prefix: deps - prefix-development: chore - labels: - - "Dependencies" + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + allow: + - dependency-type: direct + versioning-strategy: increase + commit-message: + prefix: deps + prefix-development: chore + labels: + - "Dependencies" diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 5f07e044..7797a00e 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -3,21 +3,40 @@ name: Audit on: + workflow_dispatch: null schedule: # "At 01:00 on Monday" https://crontab.guru/#0_1_*_*_1 - cron: "0 1 * * 1" - workflow_dispatch: jobs: audit: - name: npm audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 with: - node-version: '16' - - name: Install deps - run: npm i --package-lock - - name: Audit - run: npm audit + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i --package-lock + - run: npm audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 175457e2..7669ba98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,10 @@ name: CI on: + workflow_dispatch: null pull_request: + branches: + - '*' push: branches: - main @@ -16,11 +19,32 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 with: - node-version: '16' - - run: npm i --prefer-online -g npm@latest + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v - run: npm i - run: npm run lint @@ -28,25 +52,35 @@ jobs: strategy: fail-fast: false matrix: - node-version: [12.13.0, 12.x, 14.15.0, 14.x, 16.13.0, 16.x] + node-version: + - 12.13.0 + - 12.x + - 14.15.0 + - 14.x + - 16.0.0 + - 16.x platform: - - os: ubuntu-latest - shell: bash - - os: macos-latest - shell: bash - - os: windows-latest - shell: cmd + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + - os: windows-latest + shell: cmd runs-on: ${{ matrix.platform.os }} defaults: run: shell: ${{ matrix.platform.shell }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + node-version: 16.x - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) run: | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz @@ -55,7 +89,12 @@ jobs: node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz cd .. rmdir /s /q package - - name: Update npm + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 run: npm i --prefer-online --no-fund --no-audit -g npm@latest - run: npm -v - run: npm i diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c7694c49..56cd7b9c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -4,10 +4,14 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: + - main + - latest pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: + - main + - latest schedule: # "At 03:00 on Monday" https://crontab.guru/#0_3_*_*_1 - cron: "0 3 * * 1" @@ -24,15 +28,17 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: [ javascript ] steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml index d3c12f40..ca91a05a 100644 --- a/.github/workflows/post-dependabot.yml +++ b/.github/workflows/post-dependabot.yml @@ -1,6 +1,7 @@ # This file is automatically added by @npmcli/template-oss. Do not edit. -name: "Post Dependabot Actions" +name: Post Dependabot Actions + on: pull_request # https://docs.github.com/en/rest/overview/permissions-required-for-github-apps @@ -10,27 +11,47 @@ permissions: jobs: Install: runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} + if: github.actor == 'dependabot[bot]' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 with: - node-version: '16' + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v1.1.1 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: npm install and commit - if: ${{contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss')}} + if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - git config --local user.email "ops+npm-cli@npmjs.com" - git config --local user.name "npm cli ops bot" gh pr checkout ${{ github.event.pull_request.number }} npm install --no-scripts - npm run template-copy + npm run template-oss-apply git add . git commit -am "chore: postinstall for dependabot template-oss PR" git push diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 948960e7..215850b7 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,19 +4,45 @@ name: Pull Request Linting on: pull_request: - types: [opened, reopened, edited, synchronize] + types: + - opened + - reopened + - edited + - synchronize jobs: check: name: Check PR Title or Commits runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-node@v2 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 with: - node-version: '16' + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v - name: Install deps run: | npm i -D @commitlint/cli @commitlint/config-conventional @@ -24,4 +50,7 @@ jobs: env: PR_TITLE: ${{ github.event.pull_request.title }} run: | - npx commitlint -x @commitlint/config-conventional -V --from origin/main --to ${{ github.event.pull_request.head.sha }} || echo $PR_TITLE | npx commitlint -x @commitlint/config-conventional -V + npx commitlint -x @commitlint/config-conventional -V \ + --from origin/main --to ${{ github.event.pull_request.head.sha }} \ + || echo $PR_TITLE | \ + npx commitlint -x @commitlint/config-conventional -V diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index c5a165f3..34ebf789 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -6,19 +6,22 @@ on: push: branches: - main + - latest jobs: release-please: runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v2 + - uses: google-github-actions/release-please-action@v3 id: release with: release-type: node - # If you change changelog-types be sure to also update commitlintrc.js + changelog-type: github changelog-types: > - [{"type":"feat","section":"Features","hidden":false}, - {"type":"fix","section":"Bug Fixes","hidden":false}, - {"type":"docs","section":"Documentation","hidden":false}, - {"type":"deps","section":"Dependencies","hidden":false}, - {"type":"chore","hidden":true}] + [ + {"type":"feat","section":"Features","hidden":false}, + {"type":"fix","section":"Bug Fixes","hidden":false}, + {"type":"docs","section":"Documentation","hidden":false}, + {"type":"deps","section":"Dependencies","hidden":false}, + {"type":"chore","hidden":true} + ] diff --git a/.gitignore b/.gitignore index 6ed44c72..bf011b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,20 +4,22 @@ /* # keep these -!/.commitlintrc.js -!/.npmrc -!/.eslintrc* -!/.github +!/.eslintrc.local.* !**/.gitignore -!/package.json -!/docs -!/bin -!/lib +!/docs/ +!/tap-snapshots/ +!/test/ !/map.js -!/tap-snapshots -!/test -!/scripts +!/scripts/ !/README* !/LICENSE* -!/SECURITY* !/CHANGELOG* +!/.commitlintrc.js +!/.eslintrc.js +!/.github/ +!/.gitignore +!/.npmrc +!/SECURITY.md +!/bin/ +!/lib/ +!/package.json diff --git a/.npmrc b/.npmrc index 878b7dde..529f93e7 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,3 @@ -;This file is automatically added by @npmcli/template-oss. Do not edit. +; This file is automatically added by @npmcli/template-oss. Do not edit. package-lock=false diff --git a/README.md b/README.md index 06a244e9..1a423510 100644 --- a/README.md +++ b/README.md @@ -7,106 +7,115 @@ single devDependency. ### Configuration -Configure the use of `template-oss` in the root `package.json`. +Configure the use of `@npmcli/template-oss` in your `package.json` using the +`templateOSS` property. + ```js { name: 'my-package', - // ... templateOSS: { // copy repo specific files for the root pkg - applyRootRepoFiles: true, + rootRepo: true, // modify package.json and copy module specific files for the root pkg - applyRootModuleFiles: true, - // copy repo files for each whitelisted workspaces - applyWorkspaceRepoFiles: true, - // whitelist workspace by package name to modify package.json - // and copy module files + rootModule: true, + // copy repo files for all workspaces + workspaceRepo: true, + // copy module files for all workspaces + workspaceModule: true, + // filter allowed workspaces by package name + // defaults to all workspaces workspaces: ['workspace-package-name'], - version: '2.3.1' + // The rest of the config is passed in as variables + // that can be used to template files in the content + // directory. Some common ones are: + // Turns off ci in windows + windowsCI: false, + // Change the versions tested in CI and engines + ciVersions: ['10', '12', '14'] } } +``` -### `package.json` patches +#### Workspaces -These fields will be set in the project's `package.json`: +Individual workspaces can also supply their own config, if they are included by +the root package's `templateOSS.workspaces` array. These settings will override +any of the same settings in the root. ```js { - author: 'GitHub Inc.', - files: ['bin', 'lib'], - license: 'ISC', - templateVersion: $TEMPLATE_VERSION, - scripts: { - lint: 'eslint "**/*.js"', - postlint: 'npm-template-check', - lintfix: 'npm run lint -- --fix', - 'template-copy': 'npm-template-copy --force', - preversion: 'npm test', - postversion: 'npm publish', - prepublishOnly: 'git push origin --follow-tags', - snap: 'tap', - test: 'tap', - posttest: 'npm run lint', - }, - engines: { - node: '^12.13.0 || ^14.15.0 || >=16', - }, + name: 'my-workspace', + templateOSS: { + // copy repo files for this workspace + workspaceRepo: true, + // copy module files for this workspace + moduleRepo: true, + // Changes windowsCI setting for this workspace + windowsCI: false, + } } ``` -The `"templateVersion"` field will be set to the version of this package being -installed. This is used to determine if the postinstall script should take any -action. +### Content + +All the templated content for this repo lives in +[`lib/content/`](./lib/content/). The `index.js`[./lib/content/index.js] file +controls how and where this content is written. + +Content files can be overwritten or merged with the existing target file. +Currently mergining is only supported for `package.json` files. -#### Extending +Each content file goes through the following pipeline: -The `changes` constant located in `lib/postinstall/update-package.js` should contain -all patches for the `package.json` file. Be sure to correctly expand any object/array -based values with the original package content. +1. It is read from its source location +1. It is are templated using Handlebars with the variables from each packages's + config (with some derived values generated in [`config.js`](./lib/config.js) +1. It is parsed based on its file extension in + [`parser.js`](./lib/util/parser.js) +1. Additional logic is applied by the parser +1. It is written to its target location -### Static files +### Usage -Any existing `.eslintrc.*` files will be removed, unless they also match the -pattern `.eslintrc.local.*` +This package provides two bin scripts: -These files will be copied, overwriting any existing files: +#### `template-oss-check` -- `.eslintrc.js` -- `.github/workflows/ci.yml` -- `.github/ISSUE_TEMPLATE/bug.yml` -- `.github/ISSUE_TEMPLATE/config.yml` -- `.github/CODEOWNERS` -- `.gitignore` -- `LICENSE.md` -- `SECURITY.md` +This will check if any of the applied files different from the target content, +or if any of the other associated checks fail. The diffs of each file or check +will be reported with instructions on how to fix it. -### Dynamic Files +#### `template-oss-apply [--force]` -Currently, the only dynamic file generated is a github workflow for a given workspace. -`.github/workflows/ci-$$package-name$$.yml` +This will write all source files to their target locations in the cwd. It will +do nothing if `package.json#templateOSS.version` is the same as the version +being run. This can be overridden by `--force`. -#### Extending +This is the script that is run on `postinsall`. -Place files in the `lib/content/` directory, use only the file name and remove -any leading `.` characters (i.e. `.github/workflows/ci.yml` becomes `ci.yml` -and `.gitignore` becomes `gitignore`). +### Extending -Modify the `repoFiles` and `moduleFiles` objects at the top of `lib/postinstall/copy-content.js` to include -your new file. The object keys are destination paths, and values are source. +#### `lib/apply` -### `package.json` checks +This directory is where all the logic for applying files lives. It should be +possible to add new files without modifying anything in this directory. To add a +file, add the templated file to `lib/content/$FILENAME` and add entry for it in +`lib/content/index.js` depending on where and when it should be written (root vs +workspace, repo vs module, add vs remove, etc). -`npm-template-check` is run by `postlint` and will error if the `package.json` -is not configured properly, with steps to run to correct any problems. +#### `lib/check` -### Manual copy +All checks live in this directory and have the same signature. A check must be +added to `lib/check/index.js` for it to be run. -Template files will be copied automatically when `template-oss` is updated. -You can force an update with `npm run template-copy`. +#### Generic vs specific extensions -#### Extending +This repo is designed so that all (fine, most) of the logic in `lib/` is generic +and could be applied across projects of many different types. -Add any unwanted packages to `unwantedPackages` in `lib/check.js`. Currently -the best way to install any packages is to include them as `peerDependencies` -in this repo. +The files in `lib/content` are extremely specific to the npm CLI. It would be +trivial to swap out this content directory for a different one as it is only +referenced in a single place in `lib/config.js`. However, it's not currently +possible to change this value at runtime, but that might become possible in +future versions of this package. diff --git a/bin/.gitattributes b/bin/.gitattributes deleted file mode 100644 index 2d6e55eb..00000000 --- a/bin/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -# Dont modify line endings of our bin scripts -# so git status stays clean for windows tests -*.js -crlf diff --git a/bin/apply.js b/bin/apply.js new file mode 100755 index 00000000..2405a3e0 --- /dev/null +++ b/bin/apply.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +const apply = require('../lib/apply/index.js') + +const main = async () => { + const { + npm_config_global: globalMode, + npm_config_local_prefix: root, + } = process.env + + // do nothing in global mode or when the local prefix isn't set + if (globalMode === 'true' || !root) { + return + } + + await apply(root) +} + +module.exports = main().catch((err) => { + console.error(err.stack) + process.exitCode = 1 +}) diff --git a/bin/check.js b/bin/check.js new file mode 100755 index 00000000..8b3417cb --- /dev/null +++ b/bin/check.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +const check = require('../lib/check/index.js') +const output = require('../lib/util/output.js') + +const main = async () => { + const { + npm_config_local_prefix: root, + } = process.env + + if (!root) { + throw new Error('This package requires npm >7.21.1') + } + + const problems = await check(root) + + if (problems.length) { + process.exitCode = 1 + console.error(output(problems)) + } +} + +module.exports = main().catch((err) => { + console.error(err.stack) + process.exitCode = 1 +}) diff --git a/bin/npm-template-check.js b/bin/npm-template-check.js deleted file mode 100755 index 0f9b2599..00000000 --- a/bin/npm-template-check.js +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env node - -const checkPackage = require('../lib/postlint/check-package.js') -const checkGitIgnore = require('../lib/postlint/check-gitignore.js') -const getConfig = require('../lib/config.js') - -const main = async () => { - const { - npm_config_local_prefix: root, - } = process.env - - if (!root) { - throw new Error('This package requires npm >7.21.1') - } - - const config = await getConfig(root) - - const problemSets = [] - for (const path of config.paths) { - if (path !== root || config.applyRootModuleFiles) { - problemSets.push(await checkPackage(path)) - } - if (path !== root || config.applyRootRepoFiles) { - problemSets.push(await checkGitIgnore(path)) - } - } - - const problems = problemSets.flat() - - if (problems.length) { - console.error('Some problems were detected:') - console.error() - console.error(problems.map((problem) => problem.message).join('\n')) - console.error() - console.error('To correct them:') - console.error(problems.map((problem) => problem.solution).join('\n')) - process.exitCode = 1 - } -} - -module.exports = main().catch((err) => { - console.error(err.stack) - process.exitCode = 1 -}) diff --git a/bin/postinstall.js b/bin/postinstall.js deleted file mode 100755 index 0022257d..00000000 --- a/bin/postinstall.js +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node - -const copyContent = require('../lib/postinstall/copy-content.js') -const patchPackage = require('../lib/postinstall/update-package.js') -const getConfig = require('../lib/config.js') - -const main = async () => { - const { - npm_config_global: globalMode, - npm_config_local_prefix: root, - } = process.env - - // do nothing in global mode or when the local prefix isn't set - if (globalMode === 'true' || !root) { - return - } - - const config = await getConfig(root) - for (const path of config.paths) { - if (!await patchPackage(path, root, config)) { - continue - } - - await copyContent(path, root, config) - } -} - -module.exports = main().catch((err) => { - console.error(err.stack) - process.exitCode = 1 -}) diff --git a/lib/apply/apply-files.js b/lib/apply/apply-files.js new file mode 100644 index 00000000..383e2380 --- /dev/null +++ b/lib/apply/apply-files.js @@ -0,0 +1,31 @@ +const fs = require('@npmcli/fs') +const log = require('proc-log') +const { rmEach, parseEach } = require('../util/files.js') + +const run = async (dir, files, options) => { + const { rm = [], add = {} } = files + + log.verbose('apply-files', 'rm', rm) + await rmEach(dir, rm, options, (f) => fs.rm(f)) + + log.verbose('apply-files', 'add', add) + await parseEach(dir, add, options, (p) => p.applyWrite()) +} + +module.exports = [{ + run: (options) => run( + options.config.repoDir, + options.config.repoFiles, + options + ), + when: ({ config: c }) => c.isForce || (c.needsUpdate && c.applyRepo), + name: 'apply-repo', +}, { + run: (options) => run( + options.config.moduleDir, + options.config.moduleFiles, + options + ), + when: ({ config: c }) => c.isForce || (c.needsUpdate && c.applyModule), + name: 'apply-module', +}] diff --git a/lib/apply/index.js b/lib/apply/index.js new file mode 100644 index 00000000..4917332b --- /dev/null +++ b/lib/apply/index.js @@ -0,0 +1,5 @@ +const run = require('../index.js') + +module.exports = (root, content) => run(root, content, [ + require('./apply-files.js'), +]) diff --git a/lib/check/check-apply.js b/lib/check/check-apply.js new file mode 100644 index 00000000..aeae7b54 --- /dev/null +++ b/lib/check/check-apply.js @@ -0,0 +1,73 @@ +const log = require('proc-log') +const { relative, basename } = require('path') +const { rmEach, parseEach } = require('../util/files.js') +const { partition } = require('lodash') + +const solution = 'npx template-oss-apply --force' + +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') + + log.verbose('check-apply', 'rm', rm) + if (rm.length) { + res.push({ + title: `The following ${type} files need to be deleted:`, + body: rm, + solution, + }) + } + + log.verbose('check-apply', 'add', add) + if (add.length) { + res.push({ + title: `The following ${type} files need to be added:`, + body: add, + solution, + }) + } + + log.verbose('check-apply', 'update', update) + res.push(...update.map(({ file, diff }) => ({ + title: `The ${type} file ${basename(file)} needs to be updated:`, + body: [`${file}\n${'='.repeat(40)}\n${diff}`], + solution, + }))) + + return res +} + +module.exports = [{ + run: (options) => run( + 'repo', + options.config.repoDir, + options.config.repoFiles, + options + ), + when: ({ config: c }) => c.applyRepo, + name: 'check-repo', +}, { + run: (options) => run( + 'module', + options.config.moduleDir, + options.config.moduleFiles, + options + ), + when: ({ config: c }) => c.applyModule, + name: 'check-module', +}] diff --git a/lib/check/check-changelog.js b/lib/check/check-changelog.js new file mode 100644 index 00000000..dc603ec9 --- /dev/null +++ b/lib/check/check-changelog.js @@ -0,0 +1,31 @@ +const fs = require('@npmcli/fs') +const { EOL } = require('os') +const { join, relative } = require('path') + +const run = async ({ root, path }) => { + // XXX: our changelogs are always markdown + // but they could be other extensions so + // make this glob for possible matches + const changelog = join(path, 'CHANGELOG.md') + + if (await fs.exists(changelog)) { + const content = await fs.readFile(changelog, { encoding: 'utf8' }) + const mustStart = `# Changelog${EOL}${EOL}#` + if (!content.startsWith(mustStart)) { + return { + title: `The ${relative(root, changelog)} is incorrect:`, + body: [ + 'The changelog should start with', + `"${mustStart}"`, + ], + solution: 'reformat the changelog to have the correct heading', + } + } + } +} + +module.exports = { + run, + when: ({ config: c }) => c.applyModule, + name: 'check-changelog', +} diff --git a/lib/check/check-gitignore.js b/lib/check/check-gitignore.js new file mode 100644 index 00000000..5d39248a --- /dev/null +++ b/lib/check/check-gitignore.js @@ -0,0 +1,67 @@ +const log = require('proc-log') +const { EOL } = require('os') +const { resolve, relative, basename } = require('path') +const fs = require('@npmcli/fs') +const git = require('@npmcli/git') + +const NAME = 'check-gitignore' + +// The problem we are trying to solve is when a new .gitignore file +// is copied into an existing repo, there could be files already checked in +// to git that are now ignored by new gitignore rules. We want to warn +// about these files. +const run = async ({ root, path, config }) => { + log.verbose(NAME, { root, path }) + + const relativeToRoot = (f) => relative(root, resolve(path, f)) + + // use the root to detect a git repo but the project directory (path) for the + // ignore check + const ignoreFile = resolve(path, '.gitignore') + if (!await git.is({ cwd: root }) || !await fs.exists(ignoreFile)) { + log.verbose(NAME, 'no git or no gitignore') + return null + } + + log.verbose(NAME, `using ignore file ${ignoreFile}`) + + const res = await git.spawn([ + 'ls-files', + '--cached', + '--ignored', + // https://git-scm.com/docs/git-ls-files#_exclude_patterns + `--${config.isRoot ? 'exclude-from' : 'exclude-per-directory'}=${basename(ignoreFile)}`, + ], { cwd: path }) + + log.verbose(NAME, 'ls-files', res) + + // TODO: files should be filtered if they have already been moved/deleted + // but not committed. Currently you must commit for this check to pass. + const files = res.stdout + .trim() + .split('\n') + .filter(Boolean) + + if (!files.length) { + return null + } + + const ignores = (await fs.readFile(ignoreFile)) + .toString() + .split(EOL) + .filter((l) => l && !l.trim().startsWith('#')) + + const relIgnore = relativeToRoot(ignoreFile) + + return { + title: `The following files are tracked by git but matching a pattern in ${relIgnore}:`, + body: files.map(relativeToRoot), + solution: ['move files to not match one of the following patterns:', ...ignores], + } +} + +module.exports = { + run, + when: ({ config: c }) => c.applyModule, + name: NAME, +} diff --git a/lib/check/check-required.js b/lib/check/check-required.js new file mode 100644 index 00000000..60feea3e --- /dev/null +++ b/lib/check/check-required.js @@ -0,0 +1,36 @@ +const hasPackage = require('../util/has-package.js') +const { groupBy } = require('lodash') + +const run = ({ pkg, config: { requiredPackages = {} } }) => { + // ensure required packages are present in the correct place + + const mustHave = Object.entries(requiredPackages).flatMap(([location, pkgs]) => + // first make a flat array of {name, version, location} + Object.entries(pkgs).map(([name, version]) => ({ + name, + version, + location, + }))) + .filter(({ name, version, location }) => !hasPackage(pkg, name, version, [location])) + + if (mustHave.length) { + return Object.entries(groupBy(mustHave, 'location')).map(([location, specs]) => { + const rm = specs.map(({ name }) => name).join(' ') + const install = specs.map(({ name, version }) => `${name}@${version}`).join(' ') + const installLocation = hasPackage.flags[location] + + return { + title: `The following required ${location} were not found:`, + body: specs.map(({ name, version }) => `${name}@${version}`), + // solution is to remove any existing all at once but add back in by --save-<location> + solution: [`npm rm ${rm}`, `npm i ${install} ${installLocation}`].join(' && '), + } + }) + } +} + +module.exports = { + run, + when: ({ config: c }) => c.applyModule, + name: 'check-required-packages', +} diff --git a/lib/check/check-unwanted.js b/lib/check/check-unwanted.js new file mode 100644 index 00000000..13eb27fb --- /dev/null +++ b/lib/check/check-unwanted.js @@ -0,0 +1,23 @@ + +const hasPackage = require('../util/has-package.js') + +const run = ({ pkg, config: { allowedPackages = [], unwantedPackages = [] } }) => { + // ensure packages that should not be present are removed + const hasUnwanted = unwantedPackages + .filter((name) => !allowedPackages.includes(name)) + .filter((name) => hasPackage(pkg, name)) + + if (hasUnwanted.length) { + return { + title: 'The following unwanted packages were found:', + body: hasUnwanted, + solution: `npm rm ${hasUnwanted.join(' ')}`, + } + } +} + +module.exports = { + run, + when: ({ config: c }) => c.applyModule, + name: 'check-unwanted-packages', +} diff --git a/lib/check/index.js b/lib/check/index.js new file mode 100644 index 00000000..b3a24345 --- /dev/null +++ b/lib/check/index.js @@ -0,0 +1,9 @@ +const run = require('../index.js') + +module.exports = (root, content) => run(root, content, [ + require('./check-apply.js'), + require('./check-required.js'), + require('./check-unwanted.js'), + require('./check-gitignore.js'), + require('./check-changelog.js'), +]) diff --git a/lib/config.js b/lib/config.js index d378ec2e..83465259 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,51 +1,162 @@ -const PackageJson = require('@npmcli/package-json') -const mapWorkspaces = require('@npmcli/map-workspaces') - -const defaultConfig = { - applyRootRepoFiles: true, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: true, - workspaces: [], - paths: [], - force: false, +const { relative, dirname, posix, win32 } = require('path') +const log = require('proc-log') +const { uniq, defaults } = require('lodash') +const parseCIVersions = require('./util/parse-ci-versions.js') +const getGitUrl = require('./util/get-git-url.js') +const { name: NAME, version: LATEST_VERSION } = require('../package.json') + +const CONFIG_KEY = 'templateOSS' +const getPkgConfig = (pkg) => pkg[CONFIG_KEY] || {} + +const getContent = (contentPath) => { + if (typeof contentPath === 'string') { + return defaults(require(contentPath), { + sourceDir: dirname(require.resolve(contentPath)), + }) + } else { + // allow passing in content directly for tests + return contentPath + } +} + +// falsy means no content of this type +const getFiles = (config, content) => config ? content : null +const getFileKeys = (files) => files ? Object.keys(files.add || {}) : [] +const negatePath = (p) => { + // XXX: this negates the first part of each path for the gitignore + // files. it might make sense to negate more specific portions of the + // path for some paths like workspaces. so instead of ignoring !/workspaces + // it would only ignore !/workspaces/a, !/workspaces/b, etc + const [first, ...parts] = p.split(posix.sep) + const isDir = parts.length > 0 + return `!${posix.sep}${first}${isDir ? posix.sep : ''}` } -module.exports = async (root) => { - let pkg - let pkgError = false - try { - pkg = (await PackageJson.load(root)).content - } catch (e) { - pkgError = true +const makePosix = (str) => str.split(win32.sep).join(posix.sep) + +const getConfig = async ({ + pkgs, + workspaces, + root, + path, + pkg, + // default content path is looked up via require.resolve + // so use the name of this module since package.json#main + // points to the content dir + content: contentPath = NAME, + config: { + rootRepo, + rootModule, + workspaceRepo, + workspaceModule, + version, + ...pkgContent + }, +}) => { + const isRoot = root === path + const isLatest = version === LATEST_VERSION + const isDogFood = pkg.name === NAME + + // this is written to ci yml files so it needs to always use posix + const pkgRelPath = makePosix(relative(root, path)) + const gitUrl = await getGitUrl(root) + + const { + rootRepo: rootRepoContent, + rootModule: rootModuleContent, + workspaceRepo: workspaceRepoContent, + workspaceModule: workspaceModuleContent, + ...baseContent + } = getContent(contentPath) + + let repoFiles, moduleFiles + const ignorePaths = [] + + if (isRoot) { + repoFiles = getFiles(rootRepo, rootRepoContent) + moduleFiles = getFiles(rootModule, rootModuleContent) + ignorePaths.push( + // allow workspace paths if they are set, this is directly from + // map-workspaces so normalize to posix paths for gitignore + ...workspaces.map((p) => makePosix(relative(root, p))), + // allow both the repo and module files since this is the root + ...getFileKeys(repoFiles), + ...getFileKeys(moduleFiles), + // allow all workspace repo level files + ...pkgs.filter((p) => p.path !== path).flatMap((p) => + getFileKeys(getFiles(p.config.workspaceRepo, workspaceRepoContent)) + ) + ) + } else { + repoFiles = getFiles(workspaceRepo, workspaceRepoContent) + moduleFiles = getFiles(workspaceModule, workspaceModuleContent) + // In a workspace gitignores are relative to the workspace dir + // so we should only allow added module files + ignorePaths.push(...getFileKeys(moduleFiles)) } - if (pkgError || !pkg.templateOSS) { - return { - ...defaultConfig, - paths: [root], - } + + // all derived keys + const derived = { + isRoot, + isWorkspace: !isRoot, + // repo + repoDir: root, + repoFiles, + applyRepo: !!repoFiles, + // module + moduleDir: path, + moduleFiles, + applyModule: !!moduleFiles, + // package + pkgName: pkg.name, + pkgNameFs: pkg.name.replace(/\//g, '-').replace(/@/g, ''), + pkgRelPath: pkgRelPath, + // force changes if we are dogfooding this repo or with force argv + // XXX: setup proper cli arg parsing + isForce: isDogFood || process.argv.includes('--force'), + isLatest, + needsUpdate: !isLatest, + // templateoss specific values + __NAME__: NAME, + __CONFIG_KEY__: CONFIG_KEY, + __VERSION__: LATEST_VERSION, + __DOGFOOD__: isDogFood, } - const config = { - ...defaultConfig, - ...pkg.templateOSS, + + // merge the rest of base and pkg content to make the + // full content object + const content = { ...baseContent, ...pkgContent } + + // set some defaults on content that can be overwritten unlike + // derived values which are calculated from other config + const contentDefaults = {} + + if (Array.isArray(content.ciVersions)) { + const parsed = parseCIVersions(content.ciVersions) + contentDefaults.engines = parsed.engines + content.ciVersions = parsed.targets + log.verbose('config ci', parsed) } - const workspaceMap = await mapWorkspaces({ - pkg, - cwd: root, - }) - const wsPaths = [] - const workspaceSet = new Set(config.workspaces) - for (const [name, path] of workspaceMap.entries()) { - if (workspaceSet.has(name)) { - wsPaths.push(path) + + if (gitUrl) { + contentDefaults.repository = { + type: 'git', + url: gitUrl, + ...(pkgRelPath ? { directory: pkgRelPath } : {}), } } - config.workspacePaths = wsPaths - - config.paths = config.paths.concat(config.workspacePaths) - config.paths.push(root) + contentDefaults.ignorePaths = uniq( + [...ignorePaths, ...(content.distPaths || [])].map(negatePath) + ).sort() - config.force = process.argv.indexOf('--force') !== -1 + log.verbose('config', 'defaults', contentDefaults) - return config + return { + ...defaults(content, contentDefaults), + ...derived, + } } + +module.exports = getConfig +module.exports.getPkgConfig = getPkgConfig diff --git a/lib/content/CODEOWNERS b/lib/content/CODEOWNERS index ef874313..3bee8753 100644 --- a/lib/content/CODEOWNERS +++ b/lib/content/CODEOWNERS @@ -1 +1 @@ -* @npm/cli-team +* @npm/cli-team diff --git a/lib/content/LICENSE.md b/lib/content/LICENSE.md index 5fc208ff..845be76f 100644 --- a/lib/content/LICENSE.md +++ b/lib/content/LICENSE.md @@ -1,5 +1,3 @@ -<!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> - ISC License Copyright npm, Inc. diff --git a/lib/content/SECURITY.md b/lib/content/SECURITY.md index a93106d0..41b76c24 100644 --- a/lib/content/SECURITY.md +++ b/lib/content/SECURITY.md @@ -1,3 +1 @@ -<!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> - Please send vulnerability reports through [hackerone](https://hackerone.com/github). diff --git a/lib/content/audit.yml b/lib/content/audit.yml index 5f07e044..ccd4421e 100644 --- a/lib/content/audit.yml +++ b/lib/content/audit.yml @@ -1,23 +1,16 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - name: Audit on: + workflow_dispatch: schedule: # "At 01:00 on Monday" https://crontab.guru/#0_1_*_*_1 - cron: "0 1 * * 1" - workflow_dispatch: jobs: audit: - name: npm audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '16' - - name: Install deps - run: npm i --package-lock - - name: Audit - run: npm audit + {{> setupGit}} + {{> setupNode }} + - run: npm i --package-lock + - run: npm audit diff --git a/lib/content/bug.yml b/lib/content/bug.yml index fa80b2de..8803685f 100644 --- a/lib/content/bug.yml +++ b/lib/content/bug.yml @@ -1,54 +1,53 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - name: Bug description: File a bug/issue title: "[BUG] <title>" -labels: [Bug, Needs Triage] +labels: [ Bug, Needs Triage ] + body: -- type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please [search here](./issues) to see if an issue already exists for your problem. - options: - - label: I have searched the existing issues - required: true -- type: textarea - attributes: - label: Current Behavior - description: A clear & concise description of what you're experiencing. - validations: - required: false -- type: textarea - attributes: - label: Expected Behavior - description: A clear & concise description of what you expected to happen. - validations: - required: false -- type: textarea - attributes: - label: Steps To Reproduce - description: Steps to reproduce the behavior. - value: | - 1. In this environment... - 2. With this config... - 3. Run '...' - 4. See error... - validations: - required: false -- type: textarea - attributes: - label: Environment - description: | - examples: - - **npm**: 7.6.3 - - **Node**: 13.14.0 - - **OS**: Ubuntu 20.04 - - **platform**: Macbook Pro - value: | + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please [search here](./issues) to see if an issue already exists + for your problem. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A clear & concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A clear & concise description of what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + value: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + examples: + - **npm**: 7.6.3 + - **Node**: 13.14.0 + - **OS**: Ubuntu 20.04 + - **platform**: Macbook Pro + value: | - npm: - Node: - OS: - platform: - validations: - required: false - + validations: + required: false diff --git a/lib/content/ci-no-windows.yml b/lib/content/ci-no-windows.yml deleted file mode 100644 index 438b7686..00000000 --- a/lib/content/ci-no-windows.yml +++ /dev/null @@ -1,48 +0,0 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI - -on: - pull_request: - push: - branches: - - main - - latest - schedule: - # "At 02:00 on Monday" https://crontab.guru/#0_1_*_*_1 - - cron: "0 2 * * 1" - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '16' - - run: npm i --prefer-online -g npm@latest - - run: npm i - - run: npm run lint - - test: - strategy: - fail-fast: false - matrix: - node-version: [12.13.0, 12.x, 14.15.0, 14.x, 16.13.0, 16.x] - platform: - - os: ubuntu-latest - shell: bash - - os: macos-latest - shell: bash - runs-on: ${{ matrix.platform.os }} - defaults: - run: - shell: ${{ matrix.platform.shell }} - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - run: npm i --prefer-online -g npm@latest - - run: npm i - - run: npm test --ignore-scripts diff --git a/lib/content/ci-workspace.yml b/lib/content/ci-workspace.yml deleted file mode 100644 index c6e93641..00000000 --- a/lib/content/ci-workspace.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Node Workspace CI %%pkgname%% - -on: - pull_request: - paths: - - %%pkgpath%%/** - push: - paths: - - %%pkgpath%%/** - branches: - - release-next - - latest - workflow_dispatch: - -jobs: - lint: - runs-on: ubuntu-latest - steps: - # Checkout the npm/cli repo - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '16' - - run: npm i --prefer-online -g npm@latest - - run: npm i - - run: npm run lint -w %%pkgpath%% - - test: - strategy: - fail-fast: false - matrix: - node-version: [12.13.0, 12.x, 14.15.0, 14.x, 16.13.0, 16.x] - platform: - - os: ubuntu-latest - shell: bash - - os: macos-latest - shell: bash - - os: windows-latest - shell: cmd - runs-on: ${{ matrix.platform.os }} - defaults: - run: - shell: ${{ matrix.platform.shell }} - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update to workable npm (windows) - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Update npm - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - run: npm -v - - run: npm i - - run: npm test --ignore-scripts -w %%pkgpath%% diff --git a/lib/content/ci.yml b/lib/content/ci.yml index 175457e2..d2f51d15 100644 --- a/lib/content/ci.yml +++ b/lib/content/ci.yml @@ -1,13 +1,23 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: CI +name: CI {{~#if isWorkspace}} - {{pkgName}}{{/if}} on: + workflow_dispatch: pull_request: + branches: + - '*' + {{#if pkgRelPath}} + paths: + - {{pkgRelPath}} + {{/if}} push: branches: - - main - - latest + {{#each branches}} + - {{.}} + {{/each}} + {{#if pkgRelPath}} + paths: + - {{pkgRelPath}} + {{/if}} schedule: # "At 02:00 on Monday" https://crontab.guru/#0_2_*_*_1 - cron: "0 2 * * 1" @@ -16,47 +26,34 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '16' - - run: npm i --prefer-online -g npm@latest + {{> setupGit}} + {{> setupNode}} - run: npm i - - run: npm run lint + - run: npm run lint {{~#if isWorkspace}} -w {{pkgName}}{{/if}} test: strategy: fail-fast: false matrix: - node-version: [12.13.0, 12.x, 14.15.0, 14.x, 16.13.0, 16.x] + node-version: + {{#each ciVersions}} + - {{.}} + {{/each}} platform: - - os: ubuntu-latest - shell: bash - - os: macos-latest - shell: bash - - os: windows-latest - shell: cmd - runs-on: ${{ matrix.platform.os }} + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + {{#if windowsCI}} + - os: windows-latest + shell: cmd + {{/if}} + runs-on: $\{{ matrix.platform.os }} defaults: run: - shell: ${{ matrix.platform.shell }} + shell: $\{{ matrix.platform.shell }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows - - name: Update to workable npm (windows) - if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) - run: | - curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz - tar xf npm-7.5.4.tgz - cd package - node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz - cd .. - rmdir /s /q package - - name: Update npm - run: npm i --prefer-online --no-fund --no-audit -g npm@latest - - run: npm -v + {{> setupGit}} + {{> setupNode}} - run: npm i - - run: npm test --ignore-scripts + - run: npm test --ignore-scripts {{~#if isWorkspace}} -w {{pkgName}}{{/if}} diff --git a/lib/content/codeql-analysis.yml b/lib/content/codeql-analysis.yml index c7694c49..584316fe 100644 --- a/lib/content/codeql-analysis.yml +++ b/lib/content/codeql-analysis.yml @@ -1,13 +1,17 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - name: "CodeQL" on: push: - branches: [ main ] + branches: + {{#each branches}} + - {{.}} + {{/each}} pull_request: # The branches below must be a subset of the branches above - branches: [ main ] + branches: + {{#each branches}} + - {{.}} + {{/each}} schedule: # "At 03:00 on Monday" https://crontab.guru/#0_3_*_*_1 - cron: "0 3 * * 1" @@ -24,15 +28,13 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: [javascript] steps: - - name: Checkout repository - uses: actions/checkout@v2 - + {{> setupGit}} - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: - languages: ${{ matrix.language }} + languages: $\{{ matrix.language }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 diff --git a/lib/content/commitlintrc.js b/lib/content/commitlintrc.js index cf8f3d76..2a0b0cde 100644 --- a/lib/content/commitlintrc.js +++ b/lib/content/commitlintrc.js @@ -1,10 +1,7 @@ -// This file is automatically added by @npmcli/template-oss. Do not edit. - module.exports = { extends: ['@commitlint/config-conventional'], - // If you change rules be sure to also update release-please.yml rules: { - 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'chore', 'deps']], + 'type-enum': [2, 'always', [{{#each changelogTypes}}'{{type}}'{{#unless @last}}, {{/unless}}{{/each}}]], 'header-max-length': [2, 'always', 80], 'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']], }, diff --git a/lib/content/config.yml b/lib/content/config.yml index d640909f..0086358d 100644 --- a/lib/content/config.yml +++ b/lib/content/config.yml @@ -1,3 +1 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - blank_issues_enabled: true diff --git a/lib/content/dependabot.yml b/lib/content/dependabot.yml index 4c8e23f4..af6a0802 100644 --- a/lib/content/dependabot.yml +++ b/lib/content/dependabot.yml @@ -1,16 +1,15 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - version: 2 + updates: -- package-ecosystem: npm - directory: "/" - schedule: - interval: daily - allow: - - dependency-type: direct - versioning-strategy: increase - commit-message: - prefix: deps - prefix-development: chore - labels: - - "Dependencies" + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + allow: + - dependency-type: direct + versioning-strategy: increase + commit-message: + prefix: deps + prefix-development: chore + labels: + - "Dependencies" diff --git a/lib/content/eslintrc.js b/lib/content/eslintrc.js index 022767bc..7e95cd4f 100644 --- a/lib/content/eslintrc.js +++ b/lib/content/eslintrc.js @@ -1,5 +1,3 @@ -// This file is automatically added by @npmcli/template-oss. Do not edit. - const { readdirSync: readdir } = require('fs') const localConfigs = readdir(__dirname) diff --git a/lib/content/gitignore b/lib/content/gitignore index 6ed44c72..f1cbf0fe 100644 --- a/lib/content/gitignore +++ b/lib/content/gitignore @@ -1,23 +1,17 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - # ignore everything in the root /* # keep these -!/.commitlintrc.js -!/.npmrc -!/.eslintrc* -!/.github +!/.eslintrc.local.* !**/.gitignore -!/package.json -!/docs -!/bin -!/lib +!/docs/ +!/tap-snapshots/ +!/test/ !/map.js -!/tap-snapshots -!/test -!/scripts +!/scripts/ !/README* !/LICENSE* -!/SECURITY* !/CHANGELOG* +{{#each ignorePaths}} +{{.}} +{{/each}} diff --git a/lib/content/index.js b/lib/content/index.js new file mode 100644 index 00000000..483eb51f --- /dev/null +++ b/lib/content/index.js @@ -0,0 +1,90 @@ +// Changes applied to the root of the repo +const rootRepo = { + add: { + '.commitlintrc.js': 'commitlintrc.js', + '.github/workflows/ci.yml': 'ci.yml', + '.github/ISSUE_TEMPLATE/bug.yml': 'bug.yml', + '.github/ISSUE_TEMPLATE/config.yml': 'config.yml', + '.github/CODEOWNERS': 'CODEOWNERS', + '.github/dependabot.yml': 'dependabot.yml', + '.github/workflows/audit.yml': 'audit.yml', + '.github/workflows/codeql-analysis.yml': 'codeql-analysis.yml', + '.github/workflows/post-dependabot.yml': 'post-dependabot.yml', + '.github/workflows/pull-request.yml': 'pull-request.yml', + '.github/workflows/release-please.yml': 'release-please.yml', + }, +} + +// These are also applied to the root of the repo +// but can by controlled by the `rootModule` config +// XXX: im not sure the distinction between repo +// and module in the root. both are applied to the same +// dir. so we might want to combine these +const rootModule = { + add: { + '.eslintrc.js': 'eslintrc.js', + '.gitignore': 'gitignore', + '.npmrc': 'npmrc', + 'SECURITY.md': 'SECURITY.md', + 'package.json': 'package.json', + }, + rm: [ + '.eslintrc.!(js|local.*)', + ], +} + +// Changes for each workspace but applied to the root of the repo +const workspaceRepo = { + add: { + '.github/workflows/release-please-{{pkgNameFs}}.yml': 'release-please.yml', + '.github/workflows/ci-{{pkgNameFs}}.yml': 'ci.yml' + }, +} + +// Changes for each workspace but applied to the relative workspace dir +const workspaceModule = { + add: { + '.eslintrc.js': 'eslintrc.js', + '.gitignore': 'gitignore', + 'package.json': 'package.json', + }, + rm: [ + '.npmrc', + '.eslintrc.!(js|local.*)', + ], +} + +module.exports = { + rootRepo, + rootModule, + workspaceRepo, + workspaceModule, + windowsCI: true, + branches: ['main', 'latest'], + distPaths: ['bin/', 'lib/'], + ciVersions: ['12.13.0', '12.x', '14.15.0', '14.x', '16.0.0', '16.x'], + unwantedPackages: [ + 'eslint', + 'eslint-plugin-node', + '@npmcli/lint', + 'eslint-plugin-promise', + 'eslint-plugin-standard', + 'eslint-plugin-import', + 'standard', + ], + requiredPackages: { + devDependencies: { + '@npmcli/template-oss': '*', + '@npmcli/eslint-config': '^3.0.0', + tap: '^15.0.0', + }, + }, + allowedPackages: [], + changelogTypes: [ + { type: "feat", section: "Features", hidden: false }, + { type: "fix", section: "Bug Fixes", hidden: false }, + { type: "docs", section: "Documentation", hidden: false }, + { type: "deps", section: "Dependencies", hidden: false }, + { type: "chore", hidden: true }, + ], +} diff --git a/lib/content/npmrc b/lib/content/npmrc index 878b7dde..43c97e71 100644 --- a/lib/content/npmrc +++ b/lib/content/npmrc @@ -1,3 +1 @@ -;This file is automatically added by @npmcli/template-oss. Do not edit. - package-lock=false diff --git a/lib/content/package.json b/lib/content/package.json new file mode 100644 index 00000000..f3ba9626 --- /dev/null +++ b/lib/content/package.json @@ -0,0 +1,27 @@ +{ + "author": "GitHub Inc.", + "files": {{{json distPaths}}}, + "scripts": { + "lint": "eslint \"**/*.js\"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint", + "template-copy": {{{del}}}, + "lint:fix": {{{del}}} + }, + "repository": {{#if repository}}{{{json repository}}}{{else}}{{{del}}}{{/if}}, + "engines": { + "node": {{{json engines}}} + }, + {{{json __CONFIG_KEY__}}}: { + "version": {{#if __DOGFOOD__}}{{{del}}}{{else}}{{{json __VERSION__}}}{{/if}} + }, + "templateVersion": {{{del}}}, + "standard": {{{del}}} +} diff --git a/lib/content/post-dependabot.yml b/lib/content/post-dependabot.yml index d3c12f40..ffc94a6d 100644 --- a/lib/content/post-dependabot.yml +++ b/lib/content/post-dependabot.yml @@ -1,7 +1,7 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. +name: Post Dependabot Actions -name: "Post Dependabot Actions" -on: pull_request +on: + pull_request # https://docs.github.com/en/rest/overview/permissions-required-for-github-apps permissions: @@ -10,27 +10,23 @@ permissions: jobs: Install: runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} + if: github.actor == 'dependabot[bot]' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '16' + {{> setupGit}} + {{> setupNode}} - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v1.1.1 with: - github-token: "${{ secrets.GITHUB_TOKEN }}" + github-token: "$\{{ secrets.GITHUB_TOKEN }}" - name: npm install and commit - if: ${{contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss')}} + if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} run: | - git config --local user.email "ops+npm-cli@npmjs.com" - git config --local user.name "npm cli ops bot" - gh pr checkout ${{ github.event.pull_request.number }} + gh pr checkout $\{{ github.event.pull_request.number }} npm install --no-scripts - npm run template-copy + npm run template-oss-apply git add . git commit -am "chore: postinstall for dependabot template-oss PR" git push diff --git a/lib/content/pull-request.yml b/lib/content/pull-request.yml index 948960e7..4566128f 100644 --- a/lib/content/pull-request.yml +++ b/lib/content/pull-request.yml @@ -1,27 +1,28 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - name: Pull Request Linting on: pull_request: - types: [opened, reopened, edited, synchronize] + types: + - opened + - reopened + - edited + - synchronize jobs: check: name: Check PR Title or Commits runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: actions/setup-node@v2 - with: - node-version: '16' + {{> setupGit with=(obj fetch-depth=0)}} + {{> setupNode}} - name: Install deps run: | npm i -D @commitlint/cli @commitlint/config-conventional - name: Check commits OR PR title env: - PR_TITLE: ${{ github.event.pull_request.title }} + PR_TITLE: $\{{ github.event.pull_request.title }} run: | - npx commitlint -x @commitlint/config-conventional -V --from origin/main --to ${{ github.event.pull_request.head.sha }} || echo $PR_TITLE | npx commitlint -x @commitlint/config-conventional -V + npx commitlint -x @commitlint/config-conventional -V \ + --from origin/main --to $\{{ github.event.pull_request.head.sha }} \ + || echo $PR_TITLE | \ + npx commitlint -x @commitlint/config-conventional -V diff --git a/lib/content/release-please-workspace.yml b/lib/content/release-please-workspace.yml deleted file mode 100644 index acd2eda5..00000000 --- a/lib/content/release-please-workspace.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: Node Workspace Release Please %%pkgname%% - -on: - push: - paths: - - %%pkgpath%%/** - branches: - - release-next - - latest - -jobs: - release-please: - runs-on: ubuntu-latest - steps: - - uses: google-github-actions/release-please-action@v2 - id: release - with: - release-type: node - monorepo-tags: true - path: %%pkgpath%% - # If you change changelog-types be sure to also update commitlintrc.js - changelog-types: > - [{"type":"feat","section":"Features","hidden":false}, - {"type":"fix","section":"Bug Fixes","hidden":false}, - {"type":"docs","section":"Documentation","hidden":false}, - {"type":"deps","section":"Dependencies","hidden":false}, - {"type":"chore","hidden":true}] diff --git a/lib/content/release-please.yml b/lib/content/release-please.yml index c5a165f3..2a43aefb 100644 --- a/lib/content/release-please.yml +++ b/lib/content/release-please.yml @@ -1,24 +1,32 @@ -# This file is automatically added by @npmcli/template-oss. Do not edit. - -name: Release Please +name: Release Please {{~#if isWorkspace}} - {{pkgName}}{{/if}} on: push: + {{#if pkgRelPath}} + paths: + - {{pkgRelPath}}/** + {{/if}} branches: - - main + {{#each branches}} + - {{.}} + {{/each}} jobs: release-please: runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v2 + - uses: google-github-actions/release-please-action@v3 id: release with: release-type: node - # If you change changelog-types be sure to also update commitlintrc.js + changelog-type: github + {{#if pkgRelPath}} + monorepo-tags: true + paths: {{pkgRelPath}} + {{/if}} changelog-types: > - [{"type":"feat","section":"Features","hidden":false}, - {"type":"fix","section":"Bug Fixes","hidden":false}, - {"type":"docs","section":"Documentation","hidden":false}, - {"type":"deps","section":"Dependencies","hidden":false}, - {"type":"chore","hidden":true}] + [ + {{#each changelogTypes}} + {{{json .}}}{{#unless @last}},{{/unless}} + {{/each}} + ] diff --git a/lib/content/setup-git.yml b/lib/content/setup-git.yml new file mode 100644 index 00000000..4ecd4697 --- /dev/null +++ b/lib/content/setup-git.yml @@ -0,0 +1,11 @@ +- uses: actions/checkout@v3 +{{#if with}} + with: + {{#each with}} + {{@key}}: {{this}} + {{/each}} +{{/if}} +- name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" diff --git a/lib/content/setup-node.yml b/lib/content/setup-node.yml new file mode 100644 index 00000000..55a0eacd --- /dev/null +++ b/lib/content/setup-node.yml @@ -0,0 +1,21 @@ +- uses: actions/setup-node@v3 + with: + node-version: {{#each ciVersions}}{{#if @last}}{{.}}{{/if}}{{/each}} +- name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz + cd .. + rmdir /s /q package +- name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 +- name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest +- run: npm -v diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..a055823d --- /dev/null +++ b/lib/index.js @@ -0,0 +1,100 @@ +const log = require('proc-log') +const { defaults } = require('lodash') +const getConfig = require('./config.js') +const PackageJson = require('@npmcli/package-json') +const mapWorkspaces = require('@npmcli/map-workspaces') + +const getPkg = async (path, baseConfig) => { + log.verbose('get-pkg', path) + + const pkg = (await PackageJson.load(path)).content + const pkgConfig = getConfig.getPkgConfig(pkg) + log.verbose('get-pkg', pkgConfig) + + return { pkg, path, config: { ...baseConfig, ...pkgConfig } } +} + +const getWsPkgs = async (root, rootPkg) => { + const wsPkgs = [] + + // workspaces are only used to filter paths and control changes to workspaces + // so dont pass it along with the rest of the config + const { workspaces, ...baseConfig } = rootPkg.config + + // Include all by default + const include = (name) => Array.isArray(workspaces) ? workspaces.includes(name) : true + + // Look through all workspaces on the root pkg + const rootWorkspaces = await mapWorkspaces({ pkg: rootPkg.pkg, cwd: root }) + + for (const [wsName, wsPath] of rootWorkspaces.entries()) { + if (include(wsName)) { + // A workspace can control its own workspaceRepo and workspaceModule settings + // which are true by default on the root config + wsPkgs.push(await getPkg(wsPath, baseConfig)) + } + } + + return { + pkgs: wsPkgs, + paths: [...rootWorkspaces.values()], + } +} + +const getPkgs = async (root) => { + log.verbose('get-pkgs', 'root', root) + + const rootPkg = await getPkg(root) + const pkgs = [rootPkg] + + defaults(rootPkg.config, { + rootRepo: true, + rootModule: true, + workspaceRepo: true, + workspaceModule: true, + workspaces: null, + }) + + const ws = await getWsPkgs(root, rootPkg) + + return { + pkgs: pkgs.concat(ws.pkgs), + workspaces: ws.paths, + } +} + +const runAll = async (root, content, checks) => { + const results = [] + const { pkgs, workspaces } = await getPkgs(root) + + for (const { pkg, path, config } of pkgs) { + // full config includes original config values + const fullConfig = await getConfig({ + pkgs, + workspaces, + root, + pkg, + path, + config, + content, + }) + + const options = { root, pkg, path, config: fullConfig } + log.verbose('run-all', options) + + // files can export multiple checks so flatten first + for (const { when, run, name } of checks.flat()) { + log.info('attempting to run', name) + if (await when(options)) { + log.info('running', name) + results.push(await run(options)) + } + } + } + + // checks can return multiple results or nothing + // so flatten first and remove nulls before returning + return results.flat().filter(Boolean) +} + +module.exports = runAll diff --git a/lib/postinstall/copy-content.js b/lib/postinstall/copy-content.js deleted file mode 100644 index 084cc7e5..00000000 --- a/lib/postinstall/copy-content.js +++ /dev/null @@ -1,133 +0,0 @@ -const { dirname, join, resolve } = require('path') -const fs = require('@npmcli/fs') -const PackageJson = require('@npmcli/package-json') - -const contentDir = resolve(__dirname, '..', 'content') - -// keys are destination paths in the target project -// values are paths to contents relative to '../content/' -const moduleFiles = { - '.eslintrc.js': './eslintrc.js', - '.gitignore': './gitignore', - '.npmrc': './npmrc', - 'SECURITY.md': './SECURITY.md', -} - -const repoFiles = { - '.commitlintrc.js': './commitlintrc.js', - '.github/workflows/ci.yml': './ci.yml', - '.github/ISSUE_TEMPLATE/bug.yml': './bug.yml', - '.github/ISSUE_TEMPLATE/config.yml': './config.yml', - '.github/CODEOWNERS': './CODEOWNERS', - '.github/dependabot.yml': './dependabot.yml', - '.github/workflows/audit.yml': './audit.yml', - '.github/workflows/codeql-analysis.yml': './codeql-analysis.yml', - '.github/workflows/post-dependabot.yml': './post-dependabot.yml', - '.github/workflows/pull-request.yml': './pull-request.yml', - '.github/workflows/release-please.yml': './release-please.yml', -} - -// currently no workspace module files -// const workspaceModuleFiles = {} - -const workspaceRepoFiles = { - '.github/workflows/release-please-%%pkgfsname%%.yml': './release-please-workspace.yml', - '.github/workflows/ci-%%pkgfsname%%.yml': './ci-workspace.yml', -} - -const filesToDelete = [ - // remove any eslint config files that aren't local to the project - /^\.eslintrc\.(?!(local\.)).*/, -] - -const defaultConfig = { - applyRootRepoFiles: true, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: true, - windowsCI: true, -} - -const findReplace = (str, replace = {}) => { - for (const [f, r] of Object.entries(replace)) { - str = str.replace(new RegExp(f, 'g'), r) - } - return str -} - -const copyFile = async (source, target, replacements) => { - if (replacements) { - const content = await fs.readFile(source, { encoding: 'utf-8' }) - return fs.writeFile(target, findReplace(content, replacements), { owner: 'inherit' }) - } - return fs.copyFile(source, target, { owner: 'inherit' }) -} - -const copyFiles = async (targetDir, files, replacements) => { - for (let [target, source] of Object.entries(files)) { - target = findReplace(join(targetDir, target), replacements) - source = join(contentDir, source) - // if the target is a subdirectory of the path, mkdirp it first - if (dirname(target) !== targetDir) { - await fs.mkdir(dirname(target), { - owner: 'inherit', - recursive: true, - force: true, - }) - } - await copyFile(source, target, replacements) - } -} - -// given a root directory, copy all files in the content map -// after purging any files we need to delete -const copyContent = async (path, rootPath, config) => { - config = { ...defaultConfig, ...config } - const isWorkspace = path !== rootPath - - const contents = await fs.readdir(path) - - if (isWorkspace || config.applyRootModuleFiles) { - // delete files and copy moduleFiles if it's a workspace - // or if we enabled doing so for the root - for (const file of contents) { - if (filesToDelete.some((p) => p.test(file))) { - await fs.rm(join(path, file)) - } - } - await copyFiles(path, moduleFiles) - } - - if (!isWorkspace) { - if (config.applyRootRepoFiles) { - await copyFiles(rootPath, repoFiles) - if (!config.windowsCI) { - // copyFiles already did the mkdir so we can just fs.copyFile now - await fs.copyFile( - join(contentDir, 'ci-no-windows.yml'), - join(rootPath, '.github', 'workflows', 'ci.yml') - ) - } - } - return - } - - // only workspace now - - // TODO: await copyFiles(path, workspaceModuleFiles) - // if we ever have workspace specific files - - // transform and copy all workspace repo files - if (config.applyWorkspaceRepoFiles) { - const workspacePkg = (await PackageJson.load(path)).content - await copyFiles(rootPath, workspaceRepoFiles, { - '%%pkgname%%': workspacePkg.name, - '%%pkgfsname%%': workspacePkg.name.replace(/\//g, '-').replace(/@/g, ''), - '%%pkgpath%%': path.substring(rootPath.length + 1), - }) - } -} - -copyContent.moduleFiles = moduleFiles -copyContent.repoFiles = repoFiles - -module.exports = copyContent diff --git a/lib/postinstall/update-package.js b/lib/postinstall/update-package.js deleted file mode 100644 index d03efeea..00000000 --- a/lib/postinstall/update-package.js +++ /dev/null @@ -1,100 +0,0 @@ -const PackageJson = require('@npmcli/package-json') - -const { - version: TEMPLATE_VERSION, - name: TEMPLATE_NAME, -} = require('../../package.json') - -const changes = { - author: 'GitHub Inc.', - files: ['bin', 'lib'], - scripts: { - lint: 'eslint "**/*.js"', - postlint: 'npm-template-check', - 'template-copy': 'npm-template-copy --force', - lintfix: 'npm run lint -- --fix', - preversion: 'npm test', - postversion: 'npm publish', - prepublishOnly: 'git push origin --follow-tags', - snap: 'tap', - test: 'tap', - posttest: 'npm run lint', - }, - engines: { - node: '^12.13.0 || ^14.15.0 || >=16', - }, -} - -const patchPackage = async (path, root, config) => { - const pkg = await PackageJson.load(path) - config = config || {} - - // If we are running this on itself, we always run the script. - // We also don't set templateVersion in package.json because - // its not relavent and would cause git churn after running - // `npm version`. - const isDogfood = pkg.content.name === TEMPLATE_NAME - const currentVersion = (pkg.content.templateOSS === undefined) ? - pkg.content.templateVersion : pkg.content.templateOSS.version - - // if the target package.json has a templateVersion field matching our own - // current version, we return false here so the postinstall script knows to - // exit early instead of running everything again - if (!config.force - && currentVersion === TEMPLATE_VERSION - && !isDogfood) { - return false - } - - const templateConfig = { - templateOSS: { - ...pkg.content.templateOSS, - ...{ version: TEMPLATE_VERSION }, - }, - } - - let update - - if (path === root && !config.applyRootModuleFiles) { - // only update templateVersion if we're skipping root module files - update = { - ...templateConfig, - } - } else { - // we build a new object here so our exported set of changes is not modified - update = { - ...changes, - scripts: { - ...pkg.content.scripts, - ...changes.scripts, - }, - ...templateConfig, - } - } - - if (isDogfood) { - delete update.templateVersion - delete update.templateOSS - } else { - if (pkg.content.templateVersion) { - update.templateVersion = undefined - } - if (pkg.content.scripts && pkg.content.scripts['lint:fix']) { - // some old packages using standard had a lint:fix script - delete update.scripts['lint:fix'] - } - if (pkg.content.standard) { - // remove standard configuration if it exists - update.standard = undefined - } - } - - pkg.update(update) - - await pkg.save() - return true -} - -patchPackage.changes = changes - -module.exports = patchPackage diff --git a/lib/postlint/check-gitignore.js b/lib/postlint/check-gitignore.js deleted file mode 100644 index ed4d0b60..00000000 --- a/lib/postlint/check-gitignore.js +++ /dev/null @@ -1,59 +0,0 @@ -const path = require('path') -const fs = require('fs') -const { sync: which } = require('which') -const { spawnSync } = require('child_process') - -// The problem we are trying to solve is when a new .gitignore file -// is copied into an existing repo, there could be files already checked in -// to git that are now ignored by new gitignore rules. We want to warn -// about these files. -const check = async (root) => { - const git = path.resolve(root, '.git') - const gitignore = path.resolve(root, '.gitignore') - - if (!fs.existsSync(git)) { - return [] - } - - if (!fs.existsSync(gitignore)) { - throw new Error(`${gitignore} must exist to run npm-template-check`) - } - - const res = spawnSync(which('git'), [ - 'ls-files', - '--cached', - '--ignored', - // The .gitignore file has already been placed by now with the postinstall - // script so when this script runs via postlint, it can use that file - `--exclude-from=${gitignore}`, - ], { encoding: 'utf-8', cwd: root }) - - const files = res.stdout - .trim() - .split('\n') - .filter(Boolean) - - if (!files.length) { - return [] - } - - const relativeGitignore = path.relative(root, gitignore) - const ignores = fs.readFileSync(gitignore) - .toString() - .split('\n') - .filter((l) => l && !l.trim().startsWith('#')) - - const message = [ - `The following files are tracked by git but matching a pattern in ${relativeGitignore}:`, - ...files.map((f) => ` ${f}`), - ].join('\n') - - const solution = [ - 'Move files to not match one of these patterns:', - ...ignores.map((i) => ` ${i}`), - ].join('\n') - - return [{ message, solution }] -} - -module.exports = check diff --git a/lib/postlint/check-package.js b/lib/postlint/check-package.js deleted file mode 100644 index 07ffe4eb..00000000 --- a/lib/postlint/check-package.js +++ /dev/null @@ -1,90 +0,0 @@ -const PackageJson = require('@npmcli/package-json') -const patchPackage = require('../postinstall/update-package.js') - -const unwantedPackages = [ - '@npmcli/lint', - 'eslint-plugin-promise', - 'eslint-plugin-standard', - 'eslint-plugin-import', - 'standard', -] - -const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) - -const check = async (root) => { - const pkg = (await PackageJson.load(root)).content - const changes = Object.entries(patchPackage.changes) - const problems = [] - const incorrectFields = [] - // 1. ensure package.json changes have been applied - for (const [key, value] of changes) { - if (!hasOwn(pkg, key)) { - incorrectFields.push({ - name: key, - found: pkg[key], - expected: value, - }) - } else if (value && typeof value === 'object') { - for (const [subKey, subValue] of Object.entries(value)) { - if (!hasOwn(pkg[key], subKey) || - pkg[key][subKey] !== subValue) { - incorrectFields.push({ - name: `${key}.${subKey}`, - found: pkg[key][subKey], - expected: subValue, - }) - } - } - } else { - if (pkg[key] !== patchPackage.changes[key]) { - incorrectFields.push({ - name: key, - found: pkg[key], - expected: value, - }) - } - } - } - - if (incorrectFields.length) { - problems.push({ - message: [ - `The following package.json fields are incorrect:`, - ...incorrectFields.map((field) => { - const message = [ - 'Field:', - `${JSON.stringify(field.name)}`, - 'Expected:', - `${JSON.stringify(field.expected)}`, - 'Found:', - `${JSON.stringify(field.found)}`, - ].join(' ') - return ` ${message}` - }), - ].join('\n'), - solution: 'npm rm @npmcli/template-oss && npm i -D @npmcli/template-oss', - }) - } - - // 2. ensure packages that should not be present are removed - const mustRemove = unwantedPackages.filter((name) => { - return hasOwn(pkg.dependencies || {}, name) || - hasOwn(pkg.devDependencies || {}, name) - }) - - if (mustRemove.length) { - problems.push({ - message: [ - 'The following unwanted packages were found:', - ...mustRemove.map((p) => ` ${p}`), - ].join('\n'), - solution: `npm rm ${mustRemove.join(' ')}`, - }) - } - - return problems -} - -check.unwantedPackages = unwantedPackages - -module.exports = check diff --git a/lib/util/files.js b/lib/util/files.js new file mode 100644 index 00000000..8dd0698a --- /dev/null +++ b/lib/util/files.js @@ -0,0 +1,43 @@ +const { join } = require('path') +const { promisify } = require('util') +const glob = promisify(require('glob')) +const Parser = require('./parser.js') +const template = require('./template.js') + +// target paths need to be joinsed with dir and templated +const fullTarget = (dir, file, options) => join(dir, template(file, options)) + +// given an obj of files, return the full target/source paths and associated parser +const getParsers = (dir, files, options) => Object.entries(files).map(([t, s]) => { + let { file, parser: fileParser } = typeof s === 'string' ? { file: s } : s + const target = fullTarget(dir, t, options) + file = join(options.config.sourceDir, file) + if (fileParser) { + // allow files to extend base parsers or create new ones + return new (fileParser(Parser.Parsers))(target, file, options) + } + return new (Parser(file))(target, file, options) +}) + +const rmEach = async (dir, files, options, fn) => { + const res = [] + for (const target of files.map((t) => fullTarget(dir, t, options))) { + for (const file of await glob(target, { cwd: dir })) { + res.push(await fn(file)) + } + } + return res.filter(Boolean) +} + +const parseEach = async (dir, files, options, fn) => { + const res = [] + for (const parser of getParsers(dir, files, options)) { + res.push(await fn(parser)) + } + return res.filter(Boolean) +} + +module.exports = { + rmEach, + parseEach, +} diff --git a/lib/util/get-git-url.js b/lib/util/get-git-url.js new file mode 100644 index 00000000..d2262be0 --- /dev/null +++ b/lib/util/get-git-url.js @@ -0,0 +1,24 @@ +const hgi = require('hosted-git-info') +const git = require('@npmcli/git') + +// parse a repo from a git origin into a format +// for a package.json#repository object +const getRepo = async (path) => { + if (!await git.is({ cwd: path })) { + return + } + + try { + const res = await git.spawn([ + 'remote', + 'get-url', + 'origin', + ], { cwd: path }) + const { domain, user, project } = hgi.fromUrl(res.stdout.trim()) + const url = new URL(`https://${domain}`) + url.pathname = `/${user}/${project}.git` + return url.toString() + } catch {} +} + +module.exports = getRepo diff --git a/lib/util/has-package.js b/lib/util/has-package.js new file mode 100644 index 00000000..11979a43 --- /dev/null +++ b/lib/util/has-package.js @@ -0,0 +1,30 @@ +const intersects = require('semver/ranges/intersects') +const { has } = require('lodash') + +const installLocations = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'bundledDependencies', + 'optionalDependencies', +] + +const hasPackage = ( + pkg, + name, + version = '*', + locations = installLocations +) => locations + .map((l) => pkg[l]) + .some((deps) => + has(deps, name) && + (version === '*' || intersects(deps[name], version)) + ) + +module.exports = hasPackage + +module.exports.flags = installLocations.reduce((acc, location) => { + const type = location.replace(/dependencies/i, '') + acc[location] = '--save' + (type ? `-${type}` : '') + return acc +}, {}) diff --git a/lib/util/json-diff.js b/lib/util/json-diff.js new file mode 100644 index 00000000..5b3b5da7 --- /dev/null +++ b/lib/util/json-diff.js @@ -0,0 +1,38 @@ +const { format } = require('util') +const { get } = require('lodash') +const { diff } = require('just-diff') + +const j = (obj, replacer = null) => JSON.stringify(obj, replacer, 2) + +const jsonDiff = (s1, s2, DELETE) => { + // DELETE is a special string that will be the value of updated if it exists + // but should be deleted + + const ops = diff(s1, s2).map(({ op, path, value }) => { + // there could be cases where a whole object is reported + // as missing and the expected value does not need to show + // special DELETED values so filter those out here + const msgVal = j(value, (_, v) => v === DELETE ? undefined : v) + const prev = j(get(s1, path)) + const key = j(path.reduce((acc, p) => acc + (typeof p === 'number' ? `[${p}]` : `.${p}`))) + + const msg = (...args) => format('%s is %s, expected %s', ...args) + const AD = msg(key, 'missing', msgVal) + const RM = msg(key, prev, 'to be removed') + const UP = msg(key, prev, msgVal) + + if (op === 'replace') { + return value === DELETE ? RM : UP + } else if (op === 'add' && value !== DELETE) { + return AD + } + }).filter(Boolean).sort((a, b) => a.localeCompare(b)) + + if (!ops.length) { + return true + } + + return ops.join('\n') +} + +module.exports = jsonDiff diff --git a/lib/util/output.js b/lib/util/output.js new file mode 100644 index 00000000..d96bea23 --- /dev/null +++ b/lib/util/output.js @@ -0,0 +1,35 @@ +const indent = (v, i = 2) => { + if (Array.isArray(v)) { + return v.map((a) => indent(a, i)).join('\n') + } + return v.toString().split('\n').map((l) => ' '.repeat(i) + l).join('\n') +} + +const output = () => { + const res = [] + const push = (...parts) => res.push(parts.join('\n')) + return { + toString: () => res.join('\n'), + sep: () => push('', '-'.repeat(67), ''), + push, + } +} + +const outputProblems = (problems) => { + const o = output() + o.push('', 'Some problems were detected:') + o.sep() + for (const { title, body, solution } of problems) { + const [solutionTitle, ...solutionRest] = Array.isArray(solution) + ? solution : [solution] + o.push(title, '', indent(body), '', `To correct it: ${solutionTitle}`) + if (solutionRest.length) { + o.push('', indent(solutionRest)) + } + o.sep() + } + + return o.toString() +} + +module.exports = outputProblems diff --git a/lib/util/parse-ci-versions.js b/lib/util/parse-ci-versions.js new file mode 100644 index 00000000..7a1bf0e3 --- /dev/null +++ b/lib/util/parse-ci-versions.js @@ -0,0 +1,78 @@ +const semver = require('semver') +const { partition, uniq, groupBy } = require('lodash') + +// try to parse a version. if its invalid then +// try to parse it as a range instead +const versionOrRange = (v) => semver.parse(v) || new semver.Range(v) + +// get the version or the upper bound of the range +// used for sorting to give the latest ci target +const getMaxVersion = (v) => v.version || v.set[0][1].semver.version + +// given an array of versions, returns an object where +// each key is a major and each value is a sorted list of versions +const versionsByMajor = (versions) => { + const majors = groupBy(versions, (v) => semver.major(v)) + for (const [k, vs] of Object.entries(majors)) { + majors[k] = semver.sort(vs)[0] + } + return majors +} + +// given a list of semver ci targets like: +// ['12.13.0', '12.x', '14.15.0', '14.x', '16.0.0', '16.x'] +// this will parse into a uniq list of lowest "supported" +// versions. In our cases so that will return +// '^12.13.0 || ^14.15.0 || >=16'. This is not super generic but fits +// our use case for now where we want to test on a bunch of +// specific versions/ranges and map them to somewhat loose +// semver range for package.json#engines.node. This only supports +// returning ^ ranges and makes the last version >= currently. +// +// Assumptions: +// - ranges span a single major version +// - specific versions are lower then the upper bound of +// ranges within the same major version +const parseCITargets = (targets = []) => { + const [versions, ranges] = partition( + targets.map((t) => versionOrRange(t)), + (t) => t.version + ) + + const sorted = [...versions, ...ranges] + .sort((a, b) => semver.compareBuild(getMaxVersion(a), getMaxVersion(b))) + .map((v) => v.version || v.raw) + + // object of {major: lowestVersion } for all passed in versions + const minVersions = versionsByMajor(versions) + + // object of {major: lowestVersionInRange } for all passed in ranges + const minRanges = versionsByMajor(ranges.map((r) => semver.minVersion(r))) + + // Given all the uniq major versions in targets... + const parsedRanges = uniq([...Object.keys(minVersions), ...Object.keys(minRanges)]) + // first sort by major to make it display nicer + .sort((a, b) => Number(a) - Number(b)) + .map((major) => { + const minVersion = minVersions[major] + const minRange = minRanges[major] + // if we only have one then return that + if (!minVersion || !minRange) { + return minVersion || minRange + } + // otherwise return min version + // XXX: this assumes the versions are lower than the upper + // bound for any range for the same major. This is ok for + // now but will break with more complex/specific semver ranges + return minVersion + }) + // make the last version allow all greater than + .map((v, index, list) => (index === list.length - 1 ? '>=' : '^') + v) + + return { + targets: sorted, + engines: parsedRanges.join(' || '), + } +} + +module.exports = parseCITargets diff --git a/lib/util/parser.js b/lib/util/parser.js new file mode 100644 index 00000000..8cb8ef1f --- /dev/null +++ b/lib/util/parser.js @@ -0,0 +1,279 @@ +const fs = require('@npmcli/fs') +const { EOL } = require('os') +const { basename, extname, dirname } = require('path') +const yaml = require('yaml') +const NpmPackageJson = require('@npmcli/package-json') +const jsonParse = require('json-parse-even-better-errors') +const Diff = require('diff') +const { unset, merge } = require('lodash') +const template = require('./template.js') +const jsonDiff = require('./json-diff') +const setFirst = (first, rest) => ({ ...first, ...rest }) +const traverse = (value, visit, keys = []) => { + if (keys.length) { + const res = visit(keys, value) + if (res != null) { + return + } + } + if (typeof value === 'object' && value !== null) { + for (const [k, v] of Object.entries(value)) { + traverse(v, visit, keys.concat(k)) + } + } +} + +class Base { + static types = [] + static header = 'This file is automatically added by {{__NAME__}}. Do not edit.' + comment = (v) => v + merge = false // supply a merge function which runs on prepare for certain types + DELETE = template.DELETE + + constructor (target, source, options) { + this.target = target + this.source = source + this.options = options + } + + header () { + if (typeof this.comment === 'function') { + return this.comment(this.template(this.constructor.header || '')) + } + } + + read (s) { + return fs.readFile(s, { encoding: 'utf-8' }) + } + + template (s) { + return template(s, this.options) + } + + parse (s) { + return s + } + + prepare (s) { + const header = this.header() + return header ? header + EOL + EOL + s : s + } + + prepareTarget (s) { + return s + } + + toString (s) { + return s.toString() + } + + async write (s) { + // XXX: find more efficient way to do this. we can build all possible dirs before get here + await fs.mkdir(dirname(this.target), { owner: 'inherit', recursive: true, force: true }) + return fs.writeFile(this.target, this.toString(s), { owner: 'inherit' }) + } + + diffPatch (t, s) { + // create a patch and strip out the filename. if it ends up an empty string + // then return true since the files are equal + return Diff.createPatch('', t.replace(/\r\n/g, '\n'), s.replace(/\r\n/g, '\n')) + .split('\n').slice(4).join('\n').trim() || true + } + + diff (t, s) { + return this.diffPatch(t, s) + } + + // the apply methods are the only ones that should be called publically + // XXX: everything is allowed to be overridden in base classes but we could + // find a different solution than making everything public + applyWrite () { + return Promise.resolve(this.read(this.source)) + // replace template vars first, this will throw for nonexistant vars + // because it must be parseable after this step + .then((s) => this.template(s)) + // parse into whatever data structure is necessary for maniuplating + // diffing, merging, etc. by default its a string + .then((s) => this.parse(s)) + // prepare the source for writing and diffing, pass in current + // target for merging. errors parsing or preparing targets are ok here + .then((s) => this.applyTarget().catch(() => null).then((t) => this.prepare(s, t))) + .then((s) => this.write(s)) + } + + applyTarget () { + return Promise.resolve(this.read(this.target)) + .then((s) => this.parse(s)) + // for only preparing the target for diffing + .then((s) => this.prepareTarget(s)) + } + + async applyDiff () { + const target = await this.applyTarget().catch((e) => { + // handle if old does not exist + if (e.code === 'ENOENT') { + return null + } else { + return { code: 'ETARGETERROR', error: e } + } + }) + + // no need to diff if current file does not exist + if (target === null) { + return null + } + + const source = await Promise.resolve(this.read(this.source)) + .then((s) => this.template(s)) + .then((s) => this.parse(s)) + // gets the target to diff against in case it needs to merge, etc + .then((s) => this.prepare(s, target)) + + // if there was a target error then there is no need to diff + // so we just show the source with an error message + if (target.code === 'ETARGETERROR') { + const msg = `[${this.options.config.__NAME__} ERROR]` + return [ + `${msg} There was an erroring getting the target file`, + `${msg} ${target.error}`, + `${msg} It will be overwritten with the following source:`, + '-'.repeat(40), + this.toString(source), + ].join('\n') + } + + // individual diff methods are responsible for formatting or returning + // true if the are equal + return this.diff(target, source) + } +} + +class Gitignore extends Base { + static types = ['codeowners', 'gitignore'] + comment = (c) => `# ${c}` +} + +class Js extends Base { + static types = ['js'] + comment = (c) => `/* ${c} */` +} + +class Ini extends Base { + // XXX: add merge mode for updating ini files + static types = ['npmrc'] + comment = (c) => `; ${c}` +} + +class Markdown extends Base { + static types = ['md'] + comment = (c) => `<!-- ${c} -->` +} + +class Yml extends Base { + static types = ['yml'] + comment = (c) => ` ${c}` + + toString (s) { + return s.toString({ lineWidth: 0, indent: 2 }) + } + + parse (s) { + return yaml.parseDocument(s) + } + + prepare (s) { + s.commentBefore = this.header() + return this.toString(s) + } + + prepareTarget (s) { + return this.toString(s) + } +} + +class Json extends Base { + 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 }) + + toString (s) { + return JSON.stringify(s, (_, v) => v === this.DELETE ? undefined : v, 2) + } + + parse (s) { + return jsonParse(s) + } + + prepare (s, t) { + let source = s + if (typeof this.merge === 'function' && t) { + source = this.merge(t, s) + } + return setFirst(this.header(), source) + } + + diff (t, s) { + return jsonDiff(t, s, this.DELETE) + } +} + +class JsonMerge extends Json { + static header = 'This file is partially managed by {{__NAME__}}. Edits may be overwritten.' + merge = (t, s) => merge({}, t, s) +} + +class PackageJson extends JsonMerge { + static types = ['package.json'] + + async prepare (s, t) { + // merge new source with current pkg content + const update = super.prepare(s, t) + + // move comment to config field + const configKey = this.options.config.__CONFIG_KEY__ + const header = this.header() + const headerKey = Object.keys(header)[0] + update[configKey] = setFirst(header, update[configKey]) + delete update[headerKey] + + return update + } + + async write (s) { + const pkg = await NpmPackageJson.load(dirname(this.target)) + pkg.update(s) + traverse(pkg.content, (keys, value) => { + if (value === this.DELETE) { + return unset(pkg.content, keys) + } + }) + pkg.save() + } +} + +const Parsers = { + Base, + Gitignore, + Js, + Ini, + Markdown, + Yml, + Json, + JsonMerge, + PackageJson, +} + +const parserLookup = Object.values(Parsers) + +const getParser = (file) => { + const base = basename(file).toLowerCase() + const ext = extname(file).slice(1).toLowerCase() + + return parserLookup.find((p) => p.types.includes(base)) + || parserLookup.find((p) => p.types.includes(ext)) + || Parsers.Base +} + +module.exports = getParser +module.exports.Parsers = Parsers diff --git a/lib/util/template.js b/lib/util/template.js new file mode 100644 index 00000000..4c89cdd3 --- /dev/null +++ b/lib/util/template.js @@ -0,0 +1,41 @@ +const Handlebars = require('handlebars') +const { basename, extname, join } = require('path') +const fs = require('fs') +const DELETE = '__DELETE__' + +const partialName = (s) => + basename(s, extname(s)).replace(/-([a-z])/g, (_, g) => g.toUpperCase()) + +const setupHandlebars = (partialsDir) => { + Handlebars.registerHelper('obj', ({ hash }) => hash) + Handlebars.registerHelper('json', (c) => JSON.stringify(c)) + Handlebars.registerHelper('del', () => JSON.stringify(DELETE)) + + // Load all content files as camelcase partial names + for (const f of fs.readdirSync(join(partialsDir))) { + Handlebars.registerPartial( + partialName(f), + fs.readFileSync(join(partialsDir, f)).toString() + ) + } +} + +const cache = new Map() + +const template = (str, { config, ...options }) => { + if (cache.size === 0) { + setupHandlebars(config.sourceDir) + } + + let t = cache.get(str) + if (t == null) { + t = Handlebars.compile(str, { strict: true }) + cache.set(str, t) + } + + // merge in config as top level data in templates + return t({ ...options, ...config }) +} + +module.exports = template +module.exports.DELETE = DELETE diff --git a/map.js b/map.js deleted file mode 100644 index ab540b2e..00000000 --- a/map.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = testFile => testFile - .replace(/^test\/bin\//, 'bin/') - .replace(/^test\//, 'lib/') diff --git a/package.json b/package.json index 3901cf5c..84303d72 100644 --- a/package.json +++ b/package.json @@ -2,27 +2,26 @@ "name": "@npmcli/template-oss", "version": "2.9.2", "description": "templated files used in npm CLI team oss projects", - "main": "lib/index.js", + "main": "lib/content/index.js", "bin": { - "npm-template-check": "bin/npm-template-check.js", - "npm-template-copy": "bin/postinstall.js" + "template-oss-apply": "bin/apply.js", + "template-oss-check": "bin/check.js" }, "scripts": { "lint": "eslint \"**/*.js\"", "lintfix": "npm run lint -- --fix", - "postinstall": "node bin/postinstall.js", - "postlint": "npm-template-check", "posttest": "npm run lint", "postversion": "npm publish", "prepublishOnly": "git push origin --follow-tags", "preversion": "npm test", "snap": "tap", "test": "tap", - "template-copy": "npm-template-copy --force" + "template-oss-apply": "template-oss-apply --force", + "postlint": "template-oss-check" }, "repository": { "type": "git", - "url": "git+https://github.com/npm/template-oss.git" + "url": "https://github.com/npm/template-oss.git" }, "keywords": [ "npm", @@ -32,31 +31,35 @@ "license": "ISC", "dependencies": { "@npmcli/fs": "^2.0.1", + "@npmcli/git": "^3.0.0", "@npmcli/map-workspaces": "^2.0.2", "@npmcli/package-json": "^1.0.1", + "diff": "^5.0.0", + "handlebars": "^4.7.7", + "hosted-git-info": "^4.1.0", "json-parse-even-better-errors": "^2.3.1", - "which": "^2.0.2" + "just-diff": "^5.0.1", + "lodash": "^4.17.21", + "proc-log": "^2.0.0", + "semver": "^7.3.5", + "yaml": "^2.0.0-10" }, "files": [ - "bin", - "lib" + "bin/", + "lib/" ], "devDependencies": { - "@npmcli/eslint-config": "*", - "@npmcli/promise-spawn": "^2.0.1", + "@npmcli/eslint-config": "^3.0.0", "@npmcli/template-oss": "file:./", - "eslint": "^8.11.0", - "eslint-plugin-node": "^11.1.0", - "tap": "*" + "tap": "^15.1.6" }, - "peerDependencies": { - "@npmcli/eslint-config": "^2.0.0", - "tap": "^15.0.9" - }, - "tap": { - "coverage-map": "map.js" + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten." }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16" - } + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "eslintIgnore": [ + "lib/content/" + ] } diff --git a/tap-snapshots/test/apply/full-content.js.test.cjs b/tap-snapshots/test/apply/full-content.js.test.cjs new file mode 100644 index 00000000..2dd69a26 --- /dev/null +++ b/tap-snapshots/test/apply/full-content.js.test.cjs @@ -0,0 +1,1521 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/apply/full-content.js TAP default > expect resolving Promise 1`] = ` +.commitlintrc.js +======================================== +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], + 'header-max-length': [2, 'always', 80], + 'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']], + }, +} + +.eslintrc.js +======================================== +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +const { readdirSync: readdir } = require('fs') + +const localConfigs = readdir(__dirname) + .filter((file) => file.startsWith('.eslintrc.local.')) + .map((file) => \`./\${file}\`) + +module.exports = { + extends: [ + '@npmcli', + ...localConfigs, + ], +} + +.github/CODEOWNERS +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +* @npm/cli-team + +.github/ISSUE_TEMPLATE/bug.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Bug +description: File a bug/issue +title: "[BUG] <title>" +labels: [ Bug, Needs Triage ] + +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please [search here](./issues) to see if an issue already exists for your problem. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A clear & concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A clear & concise description of what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + value: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + examples: + - **npm**: 7.6.3 + - **Node**: 13.14.0 + - **OS**: Ubuntu 20.04 + - **platform**: Macbook Pro + value: | + - npm: + - Node: + - OS: + - platform: + validations: + required: false + +.github/ISSUE_TEMPLATE/config.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +blank_issues_enabled: true + +.github/dependabot.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +version: 2 + +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + allow: + - dependency-type: direct + versioning-strategy: increase + commit-message: + prefix: deps + prefix-development: chore + labels: + - "Dependencies" + +.github/workflows/audit.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Audit + +on: + workflow_dispatch: null + schedule: + # "At 01:00 on Monday" https://crontab.guru/#0_1_*_*_1 + - cron: "0 1 * * 1" + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i --package-lock + - run: npm audit + +.github/workflows/ci.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI + +on: + workflow_dispatch: null + pull_request: + branches: + - '*' + push: + branches: + - main + - latest + schedule: + # "At 02:00 on Monday" https://crontab.guru/#0_2_*_*_1 + - cron: "0 2 * * 1" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm run lint + + test: + strategy: + fail-fast: false + matrix: + node-version: + - 12.13.0 + - 12.x + - 14.15.0 + - 14.x + - 16.0.0 + - 16.x + platform: + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + - os: windows-latest + shell: cmd + runs-on: \${{ matrix.platform.os }} + defaults: + run: + shell: \${{ matrix.platform.shell }} + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm test --ignore-scripts + +.github/workflows/codeql-analysis.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: "CodeQL" + +on: + push: + branches: + - main + - latest + pull_request: + # The branches below must be a subset of the branches above + branches: + - main + - latest + schedule: + # "At 03:00 on Monday" https://crontab.guru/#0_3_*_*_1 + - cron: "0 3 * * 1" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ javascript ] + + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: \${{ matrix.language }} + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 + +.github/workflows/post-dependabot.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Post Dependabot Actions + +on: pull_request + +# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps +permissions: + contents: write + +jobs: + Install: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.1.1 + with: + github-token: "\${{ secrets.GITHUB_TOKEN }}" + - name: npm install and commit + if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + run: | + gh pr checkout \${{ github.event.pull_request.number }} + npm install --no-scripts + npm run template-oss-apply + git add . + git commit -am "chore: postinstall for dependabot template-oss PR" + git push + +.github/workflows/pull-request.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Pull Request Linting + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + +jobs: + check: + name: Check PR Title or Commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - name: Install deps + run: | + npm i -D @commitlint/cli @commitlint/config-conventional + - name: Check commits OR PR title + env: + PR_TITLE: \${{ github.event.pull_request.title }} + run: | + npx commitlint -x @commitlint/config-conventional -V / + --from origin/main --to \${{ github.event.pull_request.head.sha }} / + || echo $PR_TITLE | / + npx commitlint -x @commitlint/config-conventional -V + +.github/workflows/release-please.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release Please + +on: + push: + branches: + - main + - latest + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + release-type: node + changelog-type: github + changelog-types: > + [ + {"type":"feat","section":"Features","hidden":false}, + {"type":"fix","section":"Bug Fixes","hidden":false}, + {"type":"docs","section":"Documentation","hidden":false}, + {"type":"deps","section":"Dependencies","hidden":false}, + {"type":"chore","hidden":true} + ] + +.gitignore +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +# ignore everything in the root +/* + +# keep these +!/.eslintrc.local.* +!**/.gitignore +!/docs/ +!/tap-snapshots/ +!/test/ +!/map.js +!/scripts/ +!/README* +!/LICENSE* +!/CHANGELOG* +!/.commitlintrc.js +!/.eslintrc.js +!/.github/ +!/.gitignore +!/.npmrc +!/SECURITY.md +!/bin/ +!/lib/ +!/package.json + +.npmrc +======================================== +; This file is automatically added by @npmcli/template-oss. Do not edit. + +package-lock=false + +SECURITY.md +======================================== +<!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> + +Please send vulnerability reports through [hackerone](https://hackerone.com/github). + +package.json +======================================== +{ + "name": "testpkg", + "scripts": { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + }, + "author": "GitHub Inc.", + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } +} +` + +exports[`test/apply/full-content.js TAP workspaces + everything > expect resolving Promise 1`] = ` +.commitlintrc.js +======================================== +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], + 'header-max-length': [2, 'always', 80], + 'subject-case': [0, 'always', ['lower-case', 'sentence-case', 'start-case']], + }, +} + +.eslintrc.js +======================================== +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +const { readdirSync: readdir } = require('fs') + +const localConfigs = readdir(__dirname) + .filter((file) => file.startsWith('.eslintrc.local.')) + .map((file) => \`./\${file}\`) + +module.exports = { + extends: [ + '@npmcli', + ...localConfigs, + ], +} + +.eslintrc.local.yml +======================================== +KEEP + +.github/CODEOWNERS +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +* @npm/cli-team + +.github/ISSUE_TEMPLATE/bug.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Bug +description: File a bug/issue +title: "[BUG] <title>" +labels: [ Bug, Needs Triage ] + +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please [search here](./issues) to see if an issue already exists for your problem. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A clear & concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A clear & concise description of what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + value: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + examples: + - **npm**: 7.6.3 + - **Node**: 13.14.0 + - **OS**: Ubuntu 20.04 + - **platform**: Macbook Pro + value: | + - npm: + - Node: + - OS: + - platform: + validations: + required: false + +.github/ISSUE_TEMPLATE/config.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +blank_issues_enabled: true + +.github/dependabot.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +version: 2 + +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + allow: + - dependency-type: direct + versioning-strategy: increase + commit-message: + prefix: deps + prefix-development: chore + labels: + - "Dependencies" + +.github/workflows/audit.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Audit + +on: + workflow_dispatch: null + schedule: + # "At 01:00 on Monday" https://crontab.guru/#0_1_*_*_1 + - cron: "0 1 * * 1" + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i --package-lock + - run: npm audit + +.github/workflows/ci-bbb.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI - bbb + +on: + workflow_dispatch: null + pull_request: + branches: + - '*' + paths: + - workspaces/b + push: + branches: + - main + - latest + paths: + - workspaces/b + schedule: + # "At 02:00 on Monday" https://crontab.guru/#0_2_*_*_1 + - cron: "0 2 * * 1" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm run lint -w bbb + + test: + strategy: + fail-fast: false + matrix: + node-version: + - 12.13.0 + - 12.x + - 14.15.0 + - 14.x + - 16.0.0 + - 16.x + platform: + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + - os: windows-latest + shell: cmd + runs-on: \${{ matrix.platform.os }} + defaults: + run: + shell: \${{ matrix.platform.shell }} + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm test --ignore-scripts -w bbb + +.github/workflows/ci-name-aaaa.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI - @name/aaaa + +on: + workflow_dispatch: null + pull_request: + branches: + - '*' + paths: + - workspaces/a + push: + branches: + - main + - latest + paths: + - workspaces/a + schedule: + # "At 02:00 on Monday" https://crontab.guru/#0_2_*_*_1 + - cron: "0 2 * * 1" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm run lint -w @name/aaaa + + test: + strategy: + fail-fast: false + matrix: + node-version: + - 12.13.0 + - 12.x + - 14.15.0 + - 14.x + - 16.0.0 + - 16.x + platform: + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + - os: windows-latest + shell: cmd + runs-on: \${{ matrix.platform.os }} + defaults: + run: + shell: \${{ matrix.platform.shell }} + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm test --ignore-scripts -w @name/aaaa + +.github/workflows/ci.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: CI + +on: + workflow_dispatch: null + pull_request: + branches: + - '*' + push: + branches: + - main + - latest + schedule: + # "At 02:00 on Monday" https://crontab.guru/#0_2_*_*_1 + - cron: "0 2 * * 1" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm run lint + + test: + strategy: + fail-fast: false + matrix: + node-version: + - 12.13.0 + - 12.x + - 14.15.0 + - 14.x + - 16.0.0 + - 16.x + platform: + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + - os: windows-latest + shell: cmd + runs-on: \${{ matrix.platform.os }} + defaults: + run: + shell: \${{ matrix.platform.shell }} + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i + - run: npm test --ignore-scripts + +.github/workflows/codeql-analysis.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: "CodeQL" + +on: + push: + branches: + - main + - latest + pull_request: + # The branches below must be a subset of the branches above + branches: + - main + - latest + schedule: + # "At 03:00 on Monday" https://crontab.guru/#0_3_*_*_1 + - cron: "0 3 * * 1" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ javascript ] + + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: \${{ matrix.language }} + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 + +.github/workflows/post-dependabot.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Post Dependabot Actions + +on: pull_request + +# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps +permissions: + contents: write + +jobs: + Install: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.1.1 + with: + github-token: "\${{ secrets.GITHUB_TOKEN }}" + - name: npm install and commit + if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + run: | + gh pr checkout \${{ github.event.pull_request.number }} + npm install --no-scripts + npm run template-oss-apply + git add . + git commit -am "chore: postinstall for dependabot template-oss PR" + git push + +.github/workflows/pull-request.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Pull Request Linting + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + +jobs: + check: + name: Check PR Title or Commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - name: Install deps + run: | + npm i -D @commitlint/cli @commitlint/config-conventional + - name: Check commits OR PR title + env: + PR_TITLE: \${{ github.event.pull_request.title }} + run: | + npx commitlint -x @commitlint/config-conventional -V / + --from origin/main --to \${{ github.event.pull_request.head.sha }} / + || echo $PR_TITLE | / + npx commitlint -x @commitlint/config-conventional -V + +.github/workflows/release-please-bbb.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release Please - bbb + +on: + push: + paths: + - workspaces/b/** + branches: + - main + - latest + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + release-type: node + changelog-type: github + monorepo-tags: true + paths: workspaces/b + changelog-types: > + [ + {"type":"feat","section":"Features","hidden":false}, + {"type":"fix","section":"Bug Fixes","hidden":false}, + {"type":"docs","section":"Documentation","hidden":false}, + {"type":"deps","section":"Dependencies","hidden":false}, + {"type":"chore","hidden":true} + ] + +.github/workflows/release-please-name-aaaa.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release Please - @name/aaaa + +on: + push: + paths: + - workspaces/a/** + branches: + - main + - latest + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + release-type: node + changelog-type: github + monorepo-tags: true + paths: workspaces/a + changelog-types: > + [ + {"type":"feat","section":"Features","hidden":false}, + {"type":"fix","section":"Bug Fixes","hidden":false}, + {"type":"docs","section":"Documentation","hidden":false}, + {"type":"deps","section":"Dependencies","hidden":false}, + {"type":"chore","hidden":true} + ] + +.github/workflows/release-please.yml +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +name: Release Please + +on: + push: + branches: + - main + - latest + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + release-type: node + changelog-type: github + changelog-types: > + [ + {"type":"feat","section":"Features","hidden":false}, + {"type":"fix","section":"Bug Fixes","hidden":false}, + {"type":"docs","section":"Documentation","hidden":false}, + {"type":"deps","section":"Dependencies","hidden":false}, + {"type":"chore","hidden":true} + ] + +.gitignore +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +# ignore everything in the root +/* + +# keep these +!/.eslintrc.local.* +!**/.gitignore +!/docs/ +!/tap-snapshots/ +!/test/ +!/map.js +!/scripts/ +!/README* +!/LICENSE* +!/CHANGELOG* +!/.commitlintrc.js +!/.eslintrc.js +!/.github/ +!/.gitignore +!/.npmrc +!/SECURITY.md +!/bin/ +!/lib/ +!/package.json +!/workspaces/ + +.npmrc +======================================== +; This file is automatically added by @npmcli/template-oss. Do not edit. + +package-lock=false + +SECURITY.md +======================================== +<!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> + +Please send vulnerability reports through [hackerone](https://hackerone.com/github). + +package.json +======================================== +{ + "name": "testpkg", + "workspaces": [ + "workspaces/a", + "workspaces/b" + ], + "scripts": { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + }, + "author": "GitHub Inc.", + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } +} + +workspaces/a/.eslintrc.js +======================================== +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +const { readdirSync: readdir } = require('fs') + +const localConfigs = readdir(__dirname) + .filter((file) => file.startsWith('.eslintrc.local.')) + .map((file) => \`./\${file}\`) + +module.exports = { + extends: [ + '@npmcli', + ...localConfigs, + ], +} + +workspaces/a/.eslintrc.local.yml +======================================== +KEEP + +workspaces/a/.gitignore +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +# ignore everything in the root +/* + +# keep these +!/.eslintrc.local.* +!**/.gitignore +!/docs/ +!/tap-snapshots/ +!/test/ +!/map.js +!/scripts/ +!/README* +!/LICENSE* +!/CHANGELOG* +!/.eslintrc.js +!/.gitignore +!/bin/ +!/lib/ +!/package.json + +workspaces/a/package.json +======================================== +{ + "name": "@name/aaaa", + "scripts": { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + }, + "author": "GitHub Inc.", + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } +} + +workspaces/b/.eslintrc.js +======================================== +/* This file is automatically added by @npmcli/template-oss. Do not edit. */ + +const { readdirSync: readdir } = require('fs') + +const localConfigs = readdir(__dirname) + .filter((file) => file.startsWith('.eslintrc.local.')) + .map((file) => \`./\${file}\`) + +module.exports = { + extends: [ + '@npmcli', + ...localConfigs, + ], +} + +workspaces/b/.eslintrc.local.yml +======================================== +KEEP + +workspaces/b/.gitignore +======================================== +# This file is automatically added by @npmcli/template-oss. Do not edit. + +# ignore everything in the root +/* + +# keep these +!/.eslintrc.local.* +!**/.gitignore +!/docs/ +!/tap-snapshots/ +!/test/ +!/map.js +!/scripts/ +!/README* +!/LICENSE* +!/CHANGELOG* +!/.eslintrc.js +!/.gitignore +!/bin/ +!/lib/ +!/package.json + +workspaces/b/package.json +======================================== +{ + "name": "bbb", + "scripts": { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + }, + "author": "GitHub Inc.", + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } +} +` diff --git a/tap-snapshots/test/apply/index.js.test.cjs b/tap-snapshots/test/apply/index.js.test.cjs new file mode 100644 index 00000000..3068dd7b --- /dev/null +++ b/tap-snapshots/test/apply/index.js.test.cjs @@ -0,0 +1,49 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/apply/index.js TAP turn off all > expect resolving Promise 1`] = ` +package.json +` + +exports[`test/apply/index.js TAP turn off module > expect resolving Promise 1`] = ` +.commitlintrc.js +.github/CODEOWNERS +.github/ISSUE_TEMPLATE/bug.yml +.github/ISSUE_TEMPLATE/config.yml +.github/dependabot.yml +.github/workflows/audit.yml +.github/workflows/ci.yml +.github/workflows/codeql-analysis.yml +.github/workflows/post-dependabot.yml +.github/workflows/pull-request.yml +.github/workflows/release-please.yml +package.json +` + +exports[`test/apply/index.js TAP turn off repo > expect resolving Promise 1`] = ` +.eslintrc.js +.gitignore +.npmrc +SECURITY.md +package.json +` + +exports[`test/apply/index.js TAP workspaces > expect resolving Promise 1`] = ` +.github/workflows/ci-d.yml +.github/workflows/release-please-d.yml +package.json +workspaces/a/.eslintrc.js +workspaces/a/.gitignore +workspaces/a/package.json +workspaces/b/.eslintrc.js +workspaces/b/.gitignore +workspaces/b/package.json +workspaces/c/package.json +workspaces/d/.eslintrc.js +workspaces/d/.gitignore +workspaces/d/package.json +` diff --git a/tap-snapshots/test/bin/check.js.test.cjs b/tap-snapshots/test/bin/check.js.test.cjs new file mode 100644 index 00000000..4983251d --- /dev/null +++ b/tap-snapshots/test/bin/check.js.test.cjs @@ -0,0 +1,31 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/bin/check.js TAP problems > must match snapshot 1`] = ` + +Some problems were detected: + +------------------------------------------------------------------- + +message1 + + a + b + +To correct it: solution1 + +------------------------------------------------------------------- + +message2 + + c + +To correct it: solution2 + +------------------------------------------------------------------- + +` diff --git a/tap-snapshots/test/check/changelog.js.test.cjs b/tap-snapshots/test/check/changelog.js.test.cjs new file mode 100644 index 00000000..a76ff451 --- /dev/null +++ b/tap-snapshots/test/check/changelog.js.test.cjs @@ -0,0 +1,23 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/check/changelog.js TAP will report incorrect changelog > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The CHANGELOG.md is incorrect: + + The changelog should start with + "# Changelog + + #" + +To correct it: reformat the changelog to have the correct heading + +------------------------------------------------------------------- +` diff --git a/tap-snapshots/test/check/diffs.js.test.cjs b/tap-snapshots/test/check/diffs.js.test.cjs new file mode 100644 index 00000000..8d6739ba --- /dev/null +++ b/tap-snapshots/test/check/diffs.js.test.cjs @@ -0,0 +1,444 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/check/diffs.js TAP different headers > initial check 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following repo files need to be added: + + header.txt + noheader.txt + nocomment.txt + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP different headers > source after apply 1`] = ` +content/index.js +======================================== + +module.exports = { + rootRepo: { + add: { + 'header.txt': { + file: 'source.txt', + parser: (p) => class extends p.Base { + static header = 'Different header' + }, + }, + 'noheader.txt': { + file: 'source.txt', + parser: (p) => class extends p.Base { + static header = null + }, + }, + 'nocomment.txt': { + file: 'source.txt', + parser: (p) => class extends p.Base { + comment = null + }, + }, + }, + }, +} + +content/source.txt +======================================== +source + +header.txt +======================================== +Different header + +source + +nocomment.txt +======================================== +source + +noheader.txt +======================================== +source + +package.json +======================================== +{ + "name": "testpkg" +} +` + +exports[`test/check/diffs.js TAP json delete > initial check 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The repo file target.json needs to be updated: + + target.json + ======================================== + "//@npmcli/template-oss" is missing, expected "This file is automatically added by @npmcli/template-oss. Do not edit." + "a" is 1, expected to be removed + "b" is missing, expected 2 + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP json delete > source after apply 1`] = ` +content/index.js +======================================== +module.exports = { + rootRepo: { + add: { + 'target.json': { + file: 'source.json', + parser: (p) => p.Json, + }, + }, + }, +} + +content/source.json +======================================== +{"a":"__DELETE__","b":2} + +package.json +======================================== +{ + "name": "testpkg" +} + +target.json +======================================== +{ + "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", + "b": 2 +} +` + +exports[`test/check/diffs.js TAP json merge > initial check 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The repo file target.json needs to be updated: + + target.json + ======================================== + "//@npmcli/template-oss" is missing, expected "This file is partially managed by @npmcli/template-oss. Edits may be overwritten." + "b" is missing, expected 1 + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP json merge > source after apply 1`] = ` +content/index.js +======================================== +module.exports = { + rootRepo: { + add: { + 'target.json': { + file: 'source.json', + parser: (p) => p.JsonMerge, + }, + }, + }, +} + +content/source.json +======================================== +{"b":1} + +package.json +======================================== +{ + "name": "testpkg" +} + +target.json +======================================== +{ + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "a": 1, + "b": 1 +} +` + +exports[`test/check/diffs.js TAP json overwrite > initial check 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The repo file target.json needs to be updated: + + target.json + ======================================== + "//@npmcli/template-oss" is missing, expected "This file is automatically added by @npmcli/template-oss. Do not edit." + "b" is missing, expected 1 + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP json overwrite > source after apply 1`] = ` +content/index.js +======================================== +module.exports={rootRepo:{add:{'target.json':'source.json'}}} + +content/source.json +======================================== +{"b":1} + +package.json +======================================== +{ + "name": "testpkg" +} + +target.json +======================================== +{ + "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", + "b": 1 +} +` + +exports[`test/check/diffs.js TAP node 10 > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The repo file ci.yml needs to be updated: + + .github/workflows/ci.yml + ======================================== + @@ -52,8 +52,9 @@ + strategy: + fail-fast: false + matrix: + node-version: + + - 10 + - 12.13.0 + - 12.x + - 14.15.0 + - 14.x + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The module file package.json needs to be updated: + + package.json + ======================================== + "engines.node" is "^12.13.0 || ^14.15.0 || >=16.0.0", expected "^10.0.0 || ^12.13.0 || ^14.15.0 || >=16.0.0" + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP unknown file type > initial check 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following repo files need to be added: + + target.txt + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP unknown file type > source after apply 1`] = ` +content/index.js +======================================== +module.exports={rootRepo:{add:{'target.txt':'source.txt'}}} + +content/source.txt +======================================== +source + +package.json +======================================== +{ + "name": "testpkg" +} + +target.txt +======================================== +This file is automatically added by @npmcli/template-oss. Do not edit. + +source +` + +exports[`test/check/diffs.js TAP update, remove, errors > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The repo file ci.yml needs to be updated: + + .github/workflows/ci.yml + ======================================== + @@ -78,4 +78,24 @@ + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + + - name: Update to workable npm (windows) + + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + + run: | + + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + + tar xf npm-7.5.4.tgz + + cd package + + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + + cd .. + + rmdir /s /q package + + - name: Update npm to 7 + + # If we do test on npm 10 it needs npm7 + + if: matrix.node-version <= 10 + + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + + - name: Update npm to latest + + if: matrix.node-version > 10 + + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + + - run: npm -v + + - run: npm i + + - run: npm test --ignore-scripts + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The repo file audit.yml needs to be updated: + + .github/workflows/audit.yml + ======================================== + [@npmcli/template-oss ERROR] There was an erroring getting the target file + [@npmcli/template-oss ERROR] Error: Document with errors cannot be stringified + [@npmcli/template-oss ERROR] It will be overwritten with the following source: + ---------------------------------------- + # This file is automatically added by @npmcli/template-oss. Do not edit. + + name: Audit + + on: + workflow_dispatch: null + schedule: + # "At 01:00 on Monday" https://crontab.guru/#0_1_*_*_1 + - cron: "0 1 * * 1" + + jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup git user + run: | + git config --global user.email "ops+npm-cli@npmjs.com" + git config --global user.name "npm cli ops bot" + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Update to workable npm (windows) + # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows + if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12') || startsWith(matrix.node-version, '14')) + run: | + curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz + tar xf npm-7.5.4.tgz + cd package + node lib/npm.js install --no-fund --no-audit -g ../npm-7.5.4.tgz + cd .. + rmdir /s /q package + - name: Update npm to 7 + # If we do test on npm 10 it needs npm7 + if: matrix.node-version <= 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@7 + - name: Update npm to latest + if: matrix.node-version > 10 + run: npm i --prefer-online --no-fund --no-audit -g npm@latest + - run: npm -v + - run: npm i --package-lock + - run: npm audit + + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following module files need to be deleted: + + .eslintrc.json + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following module files need to be added: + + .npmrc + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP will diff json > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The module file package.json needs to be updated: + + package.json + ======================================== + "author" is "heynow", expected "GitHub Inc." + "files[1]" is "x", expected "lib/" + "scripts.lint:fix" is "x", expected to be removed + "scripts.preversion" is "x", expected "npm test" + "standard" is { + "config": "x " + }, expected to be removed + "templateVersion" is "1", expected to be removed + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` + +exports[`test/check/diffs.js TAP workspaces > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following module files need to be deleted: + + workspaces/a/.npmrc + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following module files need to be deleted: + + workspaces/b/.npmrc + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- +` diff --git a/tap-snapshots/test/check/gitignore.js.test.cjs b/tap-snapshots/test/check/gitignore.js.test.cjs new file mode 100644 index 00000000..89e415f5 --- /dev/null +++ b/tap-snapshots/test/check/gitignore.js.test.cjs @@ -0,0 +1,214 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/check/gitignore.js TAP will report tracked files in gitignore > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following files are tracked by git but matching a pattern in .gitignore: + + ignorethis + +To correct it: move files to not match one of the following patterns: + + /* + !/.eslintrc.local.* + !**/.gitignore + !/docs/ + !/tap-snapshots/ + !/test/ + !/map.js + !/scripts/ + !/README* + !/LICENSE* + !/CHANGELOG* + !/.commitlintrc.js + !/.eslintrc.js + !/.github/ + !/.gitignore + !/.npmrc + !/SECURITY.md + !/bin/ + !/lib/ + !/package.json + +------------------------------------------------------------------- +` + +exports[`test/check/gitignore.js TAP will report tracked files in gitignore workspace > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following files are tracked by git but matching a pattern in .gitignore: + + ignorethis + +To correct it: move files to not match one of the following patterns: + + /* + !/.eslintrc.local.* + !**/.gitignore + !/docs/ + !/tap-snapshots/ + !/test/ + !/map.js + !/scripts/ + !/README* + !/LICENSE* + !/CHANGELOG* + !/.commitlintrc.js + !/.eslintrc.js + !/.github/ + !/.gitignore + !/.npmrc + !/SECURITY.md + !/bin/ + !/lib/ + !/package.json + !/workspaces/ + +------------------------------------------------------------------- + +The following files are tracked by git but matching a pattern in workspaces/a/.gitignore: + + workspaces/a/wsafile + +To correct it: move files to not match one of the following patterns: + + /* + !/.eslintrc.local.* + !**/.gitignore + !/docs/ + !/tap-snapshots/ + !/test/ + !/map.js + !/scripts/ + !/README* + !/LICENSE* + !/CHANGELOG* + !/.eslintrc.js + !/.gitignore + !/bin/ + !/lib/ + !/package.json + +------------------------------------------------------------------- + +The following files are tracked by git but matching a pattern in workspaces/b/.gitignore: + + workspaces/b/wsbfile + +To correct it: move files to not match one of the following patterns: + + /* + !/.eslintrc.local.* + !**/.gitignore + !/docs/ + !/tap-snapshots/ + !/test/ + !/map.js + !/scripts/ + !/README* + !/LICENSE* + !/CHANGELOG* + !/.eslintrc.js + !/.gitignore + !/bin/ + !/lib/ + !/package.json + +------------------------------------------------------------------- +` + +exports[`test/check/gitignore.js TAP works with workspaces in separate dirs > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following files are tracked by git but matching a pattern in .gitignore: + + ignorethis + +To correct it: move files to not match one of the following patterns: + + /* + !/.eslintrc.local.* + !**/.gitignore + !/docs/ + !/tap-snapshots/ + !/test/ + !/map.js + !/scripts/ + !/README* + !/LICENSE* + !/CHANGELOG* + !/.commitlintrc.js + !/.eslintrc.js + !/.github/ + !/.gitignore + !/.npmrc + !/SECURITY.md + !/bin/ + !/lib/ + !/package.json + !/workspace-a + !/workspace-b + +------------------------------------------------------------------- + +The following files are tracked by git but matching a pattern in workspace-a/.gitignore: + + workspace-a/wsafile + +To correct it: move files to not match one of the following patterns: + + /* + !/.eslintrc.local.* + !**/.gitignore + !/docs/ + !/tap-snapshots/ + !/test/ + !/map.js + !/scripts/ + !/README* + !/LICENSE* + !/CHANGELOG* + !/.eslintrc.js + !/.gitignore + !/bin/ + !/lib/ + !/package.json + +------------------------------------------------------------------- + +The following files are tracked by git but matching a pattern in workspace-b/.gitignore: + + workspace-b/wsbfile + +To correct it: move files to not match one of the following patterns: + + /* + !/.eslintrc.local.* + !**/.gitignore + !/docs/ + !/tap-snapshots/ + !/test/ + !/map.js + !/scripts/ + !/README* + !/LICENSE* + !/CHANGELOG* + !/.eslintrc.js + !/.gitignore + !/bin/ + !/lib/ + !/package.json + +------------------------------------------------------------------- +` diff --git a/tap-snapshots/test/check/index.js.test.cjs b/tap-snapshots/test/check/index.js.test.cjs new file mode 100644 index 00000000..39e5e82c --- /dev/null +++ b/tap-snapshots/test/check/index.js.test.cjs @@ -0,0 +1,284 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/check/index.js TAP check empty dir > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following repo files need to be added: + + .commitlintrc.js + .github/workflows/ci.yml + .github/ISSUE_TEMPLATE/bug.yml + .github/ISSUE_TEMPLATE/config.yml + .github/CODEOWNERS + .github/dependabot.yml + .github/workflows/audit.yml + .github/workflows/codeql-analysis.yml + .github/workflows/post-dependabot.yml + .github/workflows/pull-request.yml + .github/workflows/release-please.yml + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following module files need to be added: + + .eslintrc.js + .gitignore + .npmrc + SECURITY.md + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The module file package.json needs to be updated: + + package.json + ======================================== + "author" is missing, expected "GitHub Inc." + "engines" is missing, expected { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + "files" is missing, expected [ + "bin/", + "lib/" + ] + "scripts" is missing, expected { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + } + "templateOSS" is missing, expected { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following required devDependencies were not found: + + @npmcli/template-oss@* + @npmcli/eslint-config@^3.0.0 + tap@^15.0.0 + +To correct it: npm rm @npmcli/template-oss @npmcli/eslint-config tap && npm i @npmcli/template-oss@* @npmcli/eslint-config@^3.0.0 tap@^15.0.0 --save-dev + +------------------------------------------------------------------- +` + +exports[`test/check/index.js TAP workspaces with empty dir > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following repo files need to be added: + + .commitlintrc.js + .github/workflows/ci.yml + .github/ISSUE_TEMPLATE/bug.yml + .github/ISSUE_TEMPLATE/config.yml + .github/CODEOWNERS + .github/dependabot.yml + .github/workflows/audit.yml + .github/workflows/codeql-analysis.yml + .github/workflows/post-dependabot.yml + .github/workflows/pull-request.yml + .github/workflows/release-please.yml + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following module files need to be added: + + .eslintrc.js + .gitignore + .npmrc + SECURITY.md + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The module file package.json needs to be updated: + + package.json + ======================================== + "author" is missing, expected "GitHub Inc." + "engines" is missing, expected { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + "files" is missing, expected [ + "bin/", + "lib/" + ] + "scripts" is missing, expected { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + } + "templateOSS" is missing, expected { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following required devDependencies were not found: + + @npmcli/template-oss@* + @npmcli/eslint-config@^3.0.0 + tap@^15.0.0 + +To correct it: npm rm @npmcli/template-oss @npmcli/eslint-config tap && npm i @npmcli/template-oss@* @npmcli/eslint-config@^3.0.0 tap@^15.0.0 --save-dev + +------------------------------------------------------------------- + +The following repo files need to be added: + + .github/workflows/release-please-name-aaaa.yml + .github/workflows/ci-name-aaaa.yml + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following module files need to be added: + + workspaces/a/.eslintrc.js + workspaces/a/.gitignore + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The module file package.json needs to be updated: + + workspaces/a/package.json + ======================================== + "author" is missing, expected "GitHub Inc." + "engines" is missing, expected { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + "files" is missing, expected [ + "bin/", + "lib/" + ] + "scripts" is missing, expected { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + } + "templateOSS" is missing, expected { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following required devDependencies were not found: + + @npmcli/template-oss@* + @npmcli/eslint-config@^3.0.0 + tap@^15.0.0 + +To correct it: npm rm @npmcli/template-oss @npmcli/eslint-config tap && npm i @npmcli/template-oss@* @npmcli/eslint-config@^3.0.0 tap@^15.0.0 --save-dev + +------------------------------------------------------------------- + +The following repo files need to be added: + + .github/workflows/release-please-bbb.yml + .github/workflows/ci-bbb.yml + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following module files need to be added: + + workspaces/b/.eslintrc.js + workspaces/b/.gitignore + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The module file package.json needs to be updated: + + workspaces/b/package.json + ======================================== + "author" is missing, expected "GitHub Inc." + "engines" is missing, expected { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + "files" is missing, expected [ + "bin/", + "lib/" + ] + "scripts" is missing, expected { + "lint": "eslint /"**/*.js/"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "snap": "tap", + "test": "tap", + "posttest": "npm run lint" + } + "templateOSS" is missing, expected { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "2.9.2" + } + +To correct it: npx template-oss-apply --force + +------------------------------------------------------------------- + +The following required devDependencies were not found: + + @npmcli/template-oss@* + @npmcli/eslint-config@^3.0.0 + tap@^15.0.0 + +To correct it: npm rm @npmcli/template-oss @npmcli/eslint-config tap && npm i @npmcli/template-oss@* @npmcli/eslint-config@^3.0.0 tap@^15.0.0 --save-dev + +------------------------------------------------------------------- +` diff --git a/tap-snapshots/test/check/unwanted.js.test.cjs b/tap-snapshots/test/check/unwanted.js.test.cjs new file mode 100644 index 00000000..b4fb97e7 --- /dev/null +++ b/tap-snapshots/test/check/unwanted.js.test.cjs @@ -0,0 +1,20 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/check/unwanted.js TAP unwanted > expect resolving Promise 1`] = ` +Some problems were detected: + +------------------------------------------------------------------- + +The following unwanted packages were found: + + eslint + +To correct it: npm rm eslint + +------------------------------------------------------------------- +` diff --git a/tap-snapshots/test/postlint/check-gitignore.js.test.cjs b/tap-snapshots/test/postlint/check-gitignore.js.test.cjs deleted file mode 100644 index 760270b2..00000000 --- a/tap-snapshots/test/postlint/check-gitignore.js.test.cjs +++ /dev/null @@ -1,27 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/postlint/check-gitignore.js TAP no warnings > must match snapshot 1`] = ` -Array [] -` - -exports[`test/postlint/check-gitignore.js TAP will report tracked files in gitignore > must match snapshot 1`] = ` -Array [ - Object { - "message": String( - The following files are tracked by git but matching a pattern in .gitignore: - willIgnore1 - willIgnore2 - ), - "solution": String( - Move files to not match one of these patterns: - ignored - willIgnore* - ), - }, -] -` diff --git a/tap-snapshots/test/postlint/check-package.js.test.cjs b/tap-snapshots/test/postlint/check-package.js.test.cjs deleted file mode 100644 index 1fab8d8e..00000000 --- a/tap-snapshots/test/postlint/check-package.js.test.cjs +++ /dev/null @@ -1,90 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/postlint/check-package.js TAP checks a package.json incorrect fields > problems 1`] = ` -Array [ - Object { - "message": String( - The following package.json fields are incorrect: - Field: "author" Expected: "GitHub Inc." Found: "Bob" - Field: "files" Expected: ["bin","lib"] Found: undefined - Field: "scripts" Expected: {"lint":"eslint \\\\"**/*.js\\\\"","postlint":"npm-template-check","template-copy":"npm-template-copy --force","lintfix":"npm run lint -- --fix","preversion":"npm test","postversion":"npm publish","prepublishOnly":"git push origin --follow-tags","snap":"tap","test":"tap","posttest":"npm run lint"} Found: undefined - Field: "engines" Expected: {"node":"^12.13.0 || ^14.15.0 || >=16"} Found: undefined - ), - "solution": "npm rm @npmcli/template-oss && npm i -D @npmcli/template-oss", - }, -] -` - -exports[`test/postlint/check-package.js TAP checks a package.json incorrect object fields > problems 1`] = ` -Array [ - Object { - "message": String( - The following package.json fields are incorrect: - Field: "author" Expected: "GitHub Inc." Found: undefined - Field: "files" Expected: ["bin","lib"] Found: undefined - Field: "scripts.lint" Expected: "eslint \\\\"**/*.js\\\\"" Found: undefined - Field: "scripts.postlint" Expected: "npm-template-check" Found: undefined - Field: "scripts.template-copy" Expected: "npm-template-copy --force" Found: undefined - Field: "scripts.lintfix" Expected: "npm run lint -- --fix" Found: undefined - Field: "scripts.preversion" Expected: "npm test" Found: undefined - Field: "scripts.postversion" Expected: "npm publish" Found: undefined - Field: "scripts.prepublishOnly" Expected: "git push origin --follow-tags" Found: undefined - Field: "scripts.snap" Expected: "tap" Found: undefined - Field: "scripts.test" Expected: "tap" Found: undefined - Field: "scripts.posttest" Expected: "npm run lint" Found: undefined - Field: "engines" Expected: {"node":"^12.13.0 || ^14.15.0 || >=16"} Found: undefined - ), - "solution": "npm rm @npmcli/template-oss && npm i -D @npmcli/template-oss", - }, -] -` - -exports[`test/postlint/check-package.js TAP checks a package.json missing fields > problems 1`] = ` -Array [ - Object { - "message": String( - The following package.json fields are incorrect: - Field: "author" Expected: "GitHub Inc." Found: undefined - Field: "files" Expected: ["bin","lib"] Found: undefined - Field: "scripts" Expected: {"lint":"eslint \\\\"**/*.js\\\\"","postlint":"npm-template-check","template-copy":"npm-template-copy --force","lintfix":"npm run lint -- --fix","preversion":"npm test","postversion":"npm publish","prepublishOnly":"git push origin --follow-tags","snap":"tap","test":"tap","posttest":"npm run lint"} Found: undefined - Field: "engines" Expected: {"node":"^12.13.0 || ^14.15.0 || >=16"} Found: undefined - ), - "solution": "npm rm @npmcli/template-oss && npm i -D @npmcli/template-oss", - }, -] -` - -exports[`test/postlint/check-package.js TAP checks a package.json unwanted deps > problems 1`] = ` -Array [ - Object { - "message": String( - The following package.json fields are incorrect: - Field: "author" Expected: "GitHub Inc." Found: undefined - Field: "files" Expected: ["bin","lib"] Found: undefined - Field: "scripts" Expected: {"lint":"eslint \\\\"**/*.js\\\\"","postlint":"npm-template-check","template-copy":"npm-template-copy --force","lintfix":"npm run lint -- --fix","preversion":"npm test","postversion":"npm publish","prepublishOnly":"git push origin --follow-tags","snap":"tap","test":"tap","posttest":"npm run lint"} Found: undefined - Field: "engines" Expected: {"node":"^12.13.0 || ^14.15.0 || >=16"} Found: undefined - ), - "solution": "npm rm @npmcli/template-oss && npm i -D @npmcli/template-oss", - }, - Object { - "message": String( - The following unwanted packages were found: - @npmcli/lint - eslint-plugin-promise - eslint-plugin-standard - eslint-plugin-import - standard - ), - "solution": "npm rm @npmcli/lint eslint-plugin-promise eslint-plugin-standard eslint-plugin-import standard", - }, -] -` - -exports[`test/postlint/check-package.js TAP checks a package.json unwanted deps > problems 2`] = ` -Array [] -` diff --git a/test/apply/full-content.js b/test/apply/full-content.js new file mode 100644 index 00000000..944ea187 --- /dev/null +++ b/test/apply/full-content.js @@ -0,0 +1,43 @@ +const t = require('tap') +const setup = require('../setup.js') + +t.cleanSnapshot = setup.clean +t.formatSnapshot = setup.format.readdirSource + +t.test('default', async (t) => { + const s = await setup(t) + await s.apply() + await t.resolveMatchSnapshot(s.readdirSource()) +}) + +t.test('workspaces + everything', async (t) => { + const s = await setup(t, { + workspaces: { a: '@name/aaaa', b: 'bbb' }, + testdir: { + '.eslintrc.json': 'DELETE', + '.eslintrc.local.yml': 'KEEP', + workspaces: { + a: { + '.npmrc': 'DELETE', + '.eslintrc.json': 'DELETE', + '.eslintrc.local.yml': 'KEEP', + }, + b: { + '.npmrc': 'DELETE', + '.eslintrc.json': 'DELETE', + '.eslintrc.local.yml': 'KEEP', + }, + }, + }, + }) + await s.apply() + await t.resolveMatchSnapshot(s.readdirSource()) +}) + +t.test('with empty content', async (t) => { + const s = await setup(t, { content: {} }) + await s.apply() + t.strictSame(await s.readdirSource(), { + 'package.json': JSON.stringify({ name: 'testpkg' }, null, 2), + }) +}) diff --git a/test/apply/index.js b/test/apply/index.js new file mode 100644 index 00000000..9e37407b --- /dev/null +++ b/test/apply/index.js @@ -0,0 +1,73 @@ +const t = require('tap') +const setup = require('../setup.js') + +t.cleanSnapshot = setup.clean +t.formatSnapshot = setup.format.readdir + +t.test('turn off repo', async (t) => { + const s = await setup(t, { + package: { + templateOSS: { + rootRepo: false, + }, + }, + }) + await s.apply() + await t.resolveMatchSnapshot(s.readdir()) +}) + +t.test('turn off module', async (t) => { + const s = await setup(t, { + package: { + templateOSS: { + rootModule: false, + }, + }, + }) + await s.apply() + await t.resolveMatchSnapshot(s.readdir()) +}) + +t.test('turn off all', async (t) => { + const s = await setup(t, { + package: { + templateOSS: { + rootRepo: false, + rootModule: false, + }, + }, + }) + await s.apply() + await t.resolveMatchSnapshot(s.readdir()) +}) + +t.test('workspaces', async (t) => { + const s = await setup(t, { + package: { + templateOSS: { + rootRepo: false, + rootModule: false, + workspaceRepo: false, + workspaces: ['@aaa/aaa', '@bbb/bbb', 'd'], + }, + }, + workspaces: { + a: '@aaa/aaa', + b: '@bbb/bbb', + c: { + templateOSS: { + // this has no effect since its filtered out at root + workspaceRepo: true, + }, + }, + d: { + templateOSS: { + // turn on repo to override root config + workspaceRepo: true, + }, + }, + }, + }) + await s.apply() + await t.resolveMatchSnapshot(s.readdir()) +}) diff --git a/test/bin/apply.js b/test/bin/apply.js new file mode 100644 index 00000000..40f958fe --- /dev/null +++ b/test/bin/apply.js @@ -0,0 +1,54 @@ +const t = require('tap') + +const templateApply = (mocks) => t.mock('../../bin/apply.js', mocks && { + '../../lib/apply/index.js': async () => mocks(), +}) + +const _console = console +const _global = process.env.npm_config_global +const _prefix = process.env.npm_config_local_prefix +const errors = [] + +t.beforeEach(() => { + delete process.env.npm_config_global + delete process.env.npm_config_local_prefix + console.error = (...args) => errors.push(...args) + console.log = () => { + throw 'nolog' + } +}) + +t.afterEach(() => { + process.env.npm_config_global = _global + process.env.npm_config_local_prefix = _prefix + global.console = _console + delete process.exitCode +}) + +t.test('when npm_config_local_prefix is unset, does nothing', async (t) => { + await templateApply() + t.equal(process.exitCode, undefined, 'exitCode is unset') +}) + +t.test('when npm_config_global is true, does nothing', async (t) => { + process.env.npm_config_global = 'true' + + await templateApply() + t.equal(process.exitCode, undefined, 'exitCode is unset') +}) + +t.test('with mocks', async (t) => { + process.env.npm_config_local_prefix = 'heynow' + + await templateApply(() => {}) + t.equal(process.exitCode, undefined, 'exitCode is unset') +}) + +t.test('error', async (t) => { + process.env.npm_config_local_prefix = 'heynow' + + await templateApply(() => { + throw new Error('apply') + }) + t.equal(process.exitCode, 1, 'exitCode is unset') +}) diff --git a/test/bin/check.js b/test/bin/check.js new file mode 100644 index 00000000..6841ceea --- /dev/null +++ b/test/bin/check.js @@ -0,0 +1,58 @@ +const t = require('tap') + +const templateCheck = (mocks) => t.mock('../../bin/check.js', mocks && { + '../../lib/check/index.js': async () => mocks(), +}) + +const _console = console +const _prefix = process.env.npm_config_local_prefix +const errors = [] + +t.beforeEach(() => { + delete process.env.npm_config_local_prefix + console.error = (...args) => errors.push(...args) + console.log = () => { + throw 'nolog' + } +}) + +t.afterEach(() => { + process.env.npm_config_local_prefix = _prefix + errors.length = 0 + global.console = _console + delete process.exitCode +}) + +t.test('no local prefix', async (t) => { + await templateCheck() + + t.equal(process.exitCode, 1, 'exit code') + t.match(errors, ['Error: This package requires npm'], 'errors') + t.equal(errors.length, 1) +}) + +t.test('problems', async (t) => { + process.env.npm_config_local_prefix = t.testdir() + + await templateCheck(() => [{ + title: 'message1', + body: ['a', 'b'], + solution: 'solution1', + }, { + title: 'message2', + body: ['c'], + solution: 'solution2', + }]) + + t.equal(process.exitCode, 1, 'exit code') + t.matchSnapshot(errors.join('\n')) +}) + +t.test('no problems', async (t) => { + process.env.npm_config_local_prefix = t.testdir() + + await templateCheck(() => []) + + t.equal(process.exitCode, undefined, 'exit code') + t.strictSame(errors, [], 'errors') +}) diff --git a/test/bin/npm-template-check.js b/test/bin/npm-template-check.js deleted file mode 100644 index 94d5bcd4..00000000 --- a/test/bin/npm-template-check.js +++ /dev/null @@ -1,121 +0,0 @@ -const t = require('tap') - -// t.mock instead of require so the cache doesn't interfere -const check = (mocks) => t.mock('../../bin/npm-template-check.js', mocks && { - '../../lib/postlint/check-package.js': async () => mocks.package(), - '../../lib/postlint/check-gitignore.js': async () => mocks.gitignore(), -}) - -const _console = console -const _prefix = process.env.npm_config_local_prefix -let errors = [] -let logs = [] - -t.beforeEach(() => { - delete process.env.npm_config_local_prefix - // eslint-disable-next-line no-global-assign - console = { - ..._console, - error: (...args) => errors.push(...args), - log: (...args) => logs.push(...args), - } -}) - -t.afterEach(() => { - process.env.npm_config_local_prefix = _prefix - errors = [] - logs = [] - global.console = _console - delete process.exitCode -}) - -t.test('no local prefix', async (t) => { - await check() - - t.equal(process.exitCode, 1, 'exit code') - t.strictSame(logs, [], 'logs') - t.match(errors, ['Error: This package requires npm'], 'errors') - t.equal(errors.length, 1) -}) - -// We have 100% coverage via the coverage map -// so this is only for how the bin script formats -// error logs -t.test('with mocks', (t) => { - t.plan(2) - - t.test('problems', async (t) => { - process.env.npm_config_local_prefix = t.testdir() - - await check({ - package: () => [{ - message: 'message1', - solution: 'solution1', - }], - gitignore: () => [{ - message: 'message2', - solution: 'solution2', - }], - }) - - t.equal(process.exitCode, 1, 'exit code') - t.strictSame(logs, [], 'logs') - t.strictSame(errors, [ - 'Some problems were detected:', - 'message1\nmessage2', - 'To correct them:', - 'solution1\nsolution2', - ], 'errors') - }) - - t.test('no problems', async (t) => { - process.env.npm_config_local_prefix = t.testdir() - - await check({ - package: () => [], - gitignore: () => [], - }) - - t.equal(process.exitCode, undefined, 'exit code') - t.strictSame(logs, [], 'logs') - t.strictSame(errors, [], 'errors') - }) -}) - -t.test('workspace without root module files', async (t) => { - const pkgWithWorkspaces = { - 'package.json': JSON.stringify({ - name: 'testpkg', - templateOSS: { - applyRootRepoFiles: false, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: false, - - workspaces: ['amazinga'], - }, - }), - workspace: { - a: { - 'package.json': JSON.stringify({ - name: 'amazinga', - }), - }, - }, - } - const localPrefix = t.testdir(pkgWithWorkspaces) - process.env.npm_config_local_prefix = localPrefix - - await check({ - package: (path, root, config) => [{ - message: 'package', - solution: `${path} ${root} ${config.applyRootModuleFiles}`, - }], - gitignore: (path, root, config) => [{ - message: 'gitignore', - solution: `${path} ${root} ${config.applyRootRepoFiles}`, - }], - }) - - t.strictSame(logs, [], 'logs') - t.strictSame(errors, [], 'errors') -}) diff --git a/test/bin/postinstall.js b/test/bin/postinstall.js deleted file mode 100644 index c8bda298..00000000 --- a/test/bin/postinstall.js +++ /dev/null @@ -1,124 +0,0 @@ -const t = require('tap') - -// t.mock instead of require so the cache doesn't interfere -const postinstall = (mocks) => t.mock('../../bin/postinstall.js', mocks && { - '../../lib/postinstall/update-package.js': async () => mocks.package(), - '../../lib/postinstall/copy-content.js': async () => mocks.content(), -}) - -const _global = process.env.npm_config_global -const _prefix = process.env.npm_config_local_prefix -const _console = console -let errors = [] -let logs = [] - -t.beforeEach(() => { - delete process.env.npm_config_global - delete process.env.npm_config_local_prefix - // eslint-disable-next-line no-global-assign - console = { - ..._console, - error: (...args) => errors.push(...args), - logs: (...args) => logs.push(...args), - } -}) - -t.afterEach(() => { - process.env.npm_config_global = _global - process.env.npm_config_local_prefix = _prefix - errors = [] - logs = [] - global.console = _console - delete process.exitCode -}) - -t.test('when npm_config_local_prefix is unset, does nothing', async (t) => { - await postinstall() - t.equal(process.exitCode, undefined, 'exitCode is unset') -}) - -t.test('when npm_config_global is true, does nothing', async (t) => { - process.env.npm_config_global = 'true' - - await postinstall() - t.equal(process.exitCode, undefined, 'exitCode is unset') -}) - -// We have 100% coverage via the coverage map -// so this is only for how the bin script control flow -t.test('with mocks', (t) => { - t.plan(4) - - t.test('when patchPackage returns false no further action is taken', async (t) => { - t.plan(2) - - process.env.npm_config_local_prefix = 'heynow' - - await postinstall({ - package: () => { - t.pass('package is called') - return false - }, - content: () => { - t.fail('not called') - }, - }) - - t.equal(process.exitCode, undefined, 'exitCode is unset') - }) - - t.test('when patchPackage returns true content is called', async (t) => { - t.plan(3) - - process.env.npm_config_local_prefix = 'heynow' - - await postinstall({ - package: () => { - t.pass('package is called') - return true - }, - content: () => { - t.pass('content is called') - }, - }) - - t.equal(process.exitCode, undefined, 'exitCode is unset') - }) - - t.test('sets code and errors when throwing in content', async (t) => { - process.env.npm_config_local_prefix = 'heynow' - - let stack - - await postinstall({ - package: () => true, - content: () => { - const e = new Error('whoops') - stack = e.stack - throw e - }, - }) - - t.strictSame(logs, [], 'logs') - t.match(errors, [stack], 'errors') - t.equal(process.exitCode, 1, 'exitCode is 1') - }) - - t.test('sets code and errors when throwing in package', async (t) => { - process.env.npm_config_local_prefix = 'heynow' - - let stack - - await postinstall({ - package: () => { - const e = new Error('whoops') - stack = e.stack - throw e - }, - }) - - t.strictSame(logs, [], 'logs') - t.match(errors, [stack], 'errors') - t.equal(process.exitCode, 1, 'exitCode is 1') - }) -}) diff --git a/test/check/changelog.js b/test/check/changelog.js new file mode 100644 index 00000000..317c11c0 --- /dev/null +++ b/test/check/changelog.js @@ -0,0 +1,17 @@ +const t = require('tap') +const setup = require('../setup.js') + +t.cleanSnapshot = setup.clean +t.formatSnapshot = setup.format.checks + +t.test('will report incorrect changelog', async (t) => { + const s = await setup(t, { + ok: true, + testdir: { + 'CHANGELOG.md': '# changelorg\n\n#', + }, + }) + + await s.apply() + await t.resolveMatchSnapshot(s.check()) +}) diff --git a/test/check/diffs.js b/test/check/diffs.js new file mode 100644 index 00000000..478b6dd6 --- /dev/null +++ b/test/check/diffs.js @@ -0,0 +1,163 @@ +const t = require('tap') +const { join } = require('path') +const setup = require('../setup.js') + +t.cleanSnapshot = setup.clean +t.formatSnapshot = (a) => Array.isArray(a) + ? setup.format.checks(a) + : setup.format.readdirSource(a) + +t.test('update, remove, errors', async (t) => { + const s = await setup(t, { ok: true }) + + await s.apply() + + await s.unlink('.npmrc') + + const ciPath = join('.github', 'workflows', 'ci.yml') + const ci = await s.readFile(ciPath) + await s.writeFile(ciPath, ci.split('\n').slice(0, -21).join('\n')) + + await s.appendFile( + join('.github', 'workflows', 'audit.yml'), + '>>>>I HOPE THIS IS NOT VALID YAML<<<<<<<<<<<' + ) + + await s.writeFile('.eslintrc.json', 'this has to be deleted') + + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('workspaces', async (t) => { + const s = await setup(t, { + ok: true, + workspaces: { a: 'a', b: 'b' }, + }) + + await s.apply() + + await s.writeFile(join(s.workspaces.a, '.npmrc'), 'no workspace npmrc') + await s.writeFile(join(s.workspaces.b, '.npmrc'), 'no workspace npmrc') + + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('will diff json', async (t) => { + const s = await setup(t, { ok: true }) + await s.apply() + + const pkg = await s.readJson('package.json') + pkg.author = 'heynow' + pkg.files.pop() + pkg.files.push('x') + pkg.scripts.preversion = 'x' + pkg.scripts['lint:fix'] = 'x' + pkg.scripts.engines = { node: '15' } + pkg.templateVersion = '1' + pkg.standard = { config: 'x ' } + await s.writeJson('package.json', pkg) + + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('json overwrite', async (t) => { + const s = await setup(t, { + testdir: { + 'target.json': JSON.stringify({ a: 1 }), + content: { + 'source.json': JSON.stringify({ b: 1 }), + 'index.js': `module.exports={rootRepo:{add:{'target.json':'source.json'}}}`, + }, + }, + content: 'content', + }) + + await t.resolveMatchSnapshot(s.check(), 'initial check') + await s.apply() + t.strictSame(await s.check(), []) + await t.resolveMatchSnapshot(s.readdirSource(), 'source after apply') +}) + +t.test('json merge', async (t) => { + const s = await setup(t, { + testdir: { + 'target.json': JSON.stringify({ a: 1 }), + content: { + 'source.json': JSON.stringify({ b: 1 }), + 'index.js': await setup.fixture('json-merge.js'), + }, + }, + content: 'content', + }) + + await t.resolveMatchSnapshot(s.check(), 'initial check') + await s.apply() + t.strictSame(await s.check(), []) + await t.resolveMatchSnapshot(s.readdirSource(), 'source after apply') +}) + +t.test('json delete', async (t) => { + const s = await setup(t, { + testdir: { + 'target.json': JSON.stringify({ a: 1 }), + content: { + 'source.json': JSON.stringify({ a: '__DELETE__', b: 2 }), + 'index.js': await setup.fixture('json-delete.js'), + }, + }, + content: 'content', + }) + + await t.resolveMatchSnapshot(s.check(), 'initial check') + await s.apply() + t.strictSame(await s.check(), []) + await t.resolveMatchSnapshot(s.readdirSource(), 'source after apply') +}) + +t.test('node 10', async (t) => { + const s = await setup(t, { ok: true }) + await s.apply() + + t.strictSame(await s.check(), []) + + const pkg = await s.readJson('package.json') + pkg.templateOSS.ciVersions = [...setup.content.ciVersions, '10'] + await s.writeJson('package.json', pkg) + + await s.apply() + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('different headers', async (t) => { + const s = await setup(t, { + testdir: { + content: { + 'source.txt': 'source', + 'index.js': await setup.fixture('header.js'), + }, + }, + content: 'content', + }) + + await t.resolveMatchSnapshot(s.check(), 'initial check') + await s.apply() + t.strictSame(await s.check(), []) + await t.resolveMatchSnapshot(s.readdirSource(), 'source after apply') +}) + +t.test('unknown file type', async (t) => { + const s = await setup(t, { + testdir: { + content: { + 'source.txt': 'source', + 'index.js': `module.exports={rootRepo:{add:{'target.txt':'source.txt'}}}`, + }, + }, + content: 'content', + }) + + await t.resolveMatchSnapshot(s.check(), 'initial check') + await s.apply() + t.strictSame(await s.check(), []) + await t.resolveMatchSnapshot(s.readdirSource(), 'source after apply') +}) diff --git a/test/check/dogfood.js b/test/check/dogfood.js new file mode 100644 index 00000000..51ff024b --- /dev/null +++ b/test/check/dogfood.js @@ -0,0 +1,9 @@ +const t = require('tap') +const { resolve } = require('path') +const check = require('../../lib/check/index.js') + +t.test('this repo passes all checks', async (t) => { + const root = resolve(__dirname, '..', '..') + const res = await check(root, resolve(root, 'lib', 'content')) + t.equal(res.length, 0) +}) diff --git a/test/check/gitignore.js b/test/check/gitignore.js new file mode 100644 index 00000000..bd5f249d --- /dev/null +++ b/test/check/gitignore.js @@ -0,0 +1,65 @@ +const { join } = require('path') +const t = require('tap') +const setup = require('../setup.js') + +t.cleanSnapshot = setup.clean +t.formatSnapshot = setup.format.checks + +t.test('will report tracked files in gitignore', async (t) => { + const s = await setup.git(t, { ok: true }) + + await s.writeFile('ignorethis', 'empty') + + await s.gca() + await s.apply() + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('will report tracked files in gitignore workspace', async (t) => { + const s = await setup.git(t, { + ok: true, + workspaces: { + a: '@aaa/a', + b: 'b', + }, + }) + + await s.writeFile('ignorethis', 'empty') + await s.writeFile(join(s.workspaces.a, 'wsafile'), 'empty') + await s.writeFile(join(s.workspaces.b, 'wsbfile'), 'empty') + + await s.gca() + await s.apply() + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('works with workspaces in separate dirs', async (t) => { + const s = await setup.git(t, { + ok: true, + package: { + workspaces: ['workspace-a', 'workspace-b'], + }, + testdir: { + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'a', + ...setup.okPackage(), + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'b', + ...setup.okPackage(), + }), + }, + }, + }) + + await s.writeFile('ignorethis', 'empty') + await s.writeFile(join('workspace-a', 'wsafile'), 'empty') + await s.writeFile(join('workspace-b', 'wsbfile'), 'empty') + + await s.gca() + await s.apply() + await t.resolveMatchSnapshot(s.check()) +}) diff --git a/test/check/index.js b/test/check/index.js new file mode 100644 index 00000000..369b5511 --- /dev/null +++ b/test/check/index.js @@ -0,0 +1,22 @@ +const t = require('tap') +const setup = require('../setup.js') + +t.cleanSnapshot = setup.clean +t.formatSnapshot = setup.format.checks + +t.test('check empty dir', async (t) => { + const s = await setup(t) + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('workspaces with empty dir', async (t) => { + const s = await setup(t, { + workspaces: { a: '@name/aaaa', b: 'bbb' }, + }) + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('with empty content', async (t) => { + const s = await setup(t, { content: {} }) + t.same(await s.check(), []) +}) diff --git a/test/check/unwanted.js b/test/check/unwanted.js new file mode 100644 index 00000000..c145f550 --- /dev/null +++ b/test/check/unwanted.js @@ -0,0 +1,38 @@ +const t = require('tap') +const setup = require('../setup.js') + +t.cleanSnapshot = setup.clean +t.formatSnapshot = setup.format.checks + +t.test('unwanted', async (t) => { + const s = await setup(t, { + ok: true, + package: { + dependencies: { + eslint: '^8.0.0', + }, + }, + }) + + await s.apply() + await t.resolveMatchSnapshot(s.check()) +}) + +t.test('unwanted can be overriden with allow', async (t) => { + const s = await setup(t, { + ok: true, + package: { + dependencies: { + eslint: '^8.0.0', + }, + templateOSS: { + allowedPackages: [ + 'eslint', + ], + }, + }, + }) + + await s.apply() + t.strictSame(await s.check(), []) +}) diff --git a/test/fixtures/header.js b/test/fixtures/header.js new file mode 100644 index 00000000..335c0bbe --- /dev/null +++ b/test/fixtures/header.js @@ -0,0 +1,25 @@ + +module.exports = { + rootRepo: { + add: { + 'header.txt': { + file: 'source.txt', + parser: (p) => class extends p.Base { + static header = 'Different header' + }, + }, + 'noheader.txt': { + file: 'source.txt', + parser: (p) => class extends p.Base { + static header = null + }, + }, + 'nocomment.txt': { + file: 'source.txt', + parser: (p) => class extends p.Base { + comment = null + }, + }, + }, + }, +} diff --git a/test/fixtures/json-delete.js b/test/fixtures/json-delete.js new file mode 100644 index 00000000..d35a2408 --- /dev/null +++ b/test/fixtures/json-delete.js @@ -0,0 +1,10 @@ +module.exports = { + rootRepo: { + add: { + 'target.json': { + file: 'source.json', + parser: (p) => p.Json, + }, + }, + }, +} diff --git a/test/fixtures/json-merge.js b/test/fixtures/json-merge.js new file mode 100644 index 00000000..69d78c27 --- /dev/null +++ b/test/fixtures/json-merge.js @@ -0,0 +1,10 @@ +module.exports = { + rootRepo: { + add: { + 'target.json': { + file: 'source.json', + parser: (p) => p.JsonMerge, + }, + }, + }, +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..0783d3f1 --- /dev/null +++ b/test/index.js @@ -0,0 +1,32 @@ +const t = require('tap') +const setup = require('./setup.js') + +t.test('apply and check is ok', async (t) => { + const s = await setup(t, { ok: true }) + t.same(await s.runAll(), []) +}) + +t.test('apply and check multiple is ok', async (t) => { + const s = await setup(t, { ok: true }) + t.same(await s.runAll(), []) + t.same(await s.runAll(), []) +}) + +t.test('empty content is ok', async (t) => { + const s = await setup(t, { content: {} }) + t.same(await s.runAll(), []) +}) + +t.test('empty file objects', async (t) => { + const s = await setup.git(t, { + ok: true, + workspaces: { a: 'a', b: 'b' }, + content: { + rootRepo: {}, + rootModule: {}, + workspaceRepo: {}, + workspaceModule: {}, + }, + }) + t.same(await s.runAll(), []) +}) diff --git a/test/postinstall/copy-content.js b/test/postinstall/copy-content.js deleted file mode 100644 index d80e537b..00000000 --- a/test/postinstall/copy-content.js +++ /dev/null @@ -1,250 +0,0 @@ -const { join } = require('path') -const fs = require('@npmcli/fs') -const t = require('tap') -const getConfig = require('../../lib/config.js') - -const copyContent = require('../../lib/postinstall/copy-content.js') - -t.test('copies content', async (t) => { - const root = t.testdir() - - await copyContent(root, root) - for (let target of Object.keys(copyContent.moduleFiles)) { - target = join(root, target) - await t.resolves(fs.stat(target)) - } - for (let target of Object.keys(copyContent.repoFiles)) { - target = join(root, target) - await t.resolves(fs.stat(target)) - } -}) - -t.test('removes files', async (t) => { - const content = { - '.eslintrc.json': '{}', - '.eslintrc.yml': '', - '.eslintrc.local.json': '{}', - 'something.txt': '', - } - const keepContent = [ - '.eslintrc.local.json', - 'something.txt', - ] - const root = t.testdir(content, { - applyRootRepoFiles: true, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: true, - }) - - await copyContent(root, root) - for (const target of Object.keys(copyContent.moduleFiles)) { - const fullTarget = join(root, target) - await t.resolves(fs.stat(fullTarget), `copied ${target}`) - } - for (const target of Object.keys(copyContent.repoFiles)) { - const fullTarget = join(root, target) - await t.resolves(fs.stat(fullTarget), `copied ${target}`) - } - - for (const target in content) { - const fullTarget = join(root, target) - if (keepContent.find((f) => f === target)) { - await t.resolves(fs.stat(fullTarget), `left existing ${target}`) - } else { - await t.rejects(fs.stat(fullTarget), { code: 'ENOENT' }, `removed ${target}`) - } - } -}) - -t.test('handles workspaces', async (t) => { - const pkgWithWorkspaces = { - 'package.json': JSON.stringify({ - name: 'testpkg', - templateOSS: { - applyRootRepoFiles: true, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: true, - workspaces: ['workspace/a', 'workspace/b'], - }, - }), - workspace: { - a: { - 'package.json': JSON.stringify({ - name: 'amazinga', - }), - }, - b: { - 'package.json': JSON.stringify({ - name: '@somenamespace/amazingb', - }), - }, - }, - } - const root = t.testdir(pkgWithWorkspaces) - const workspacea = join(root, 'workspace', 'a') - const config = await getConfig(root) - await copyContent(workspacea, root, config) - - // change should have applied to the workspace, not the root - await t.resolves(fs.stat(join(root, 'workspace', 'a', '.eslintrc.js'))) - await t.rejects(fs.stat(join(root, 'workspace', 'b', '.eslintrc.js'))) - - await t.rejects(fs.stat(join(root, '.eslintrc.js'))) - await copyContent(root, root, config) - await t.resolves(fs.stat(join(root, '.eslintrc.js'))) - // should have made the workspace action in the root - await t.resolves(fs.stat(join(root, '.github', 'workflows', 'ci-amazinga.yml'))) - await t.resolves(fs.stat(join(root, '.github', 'workflows', 'release-please-amazinga.yml'))) - await t.resolves(fs.stat(join(root, '.github', 'ISSUE_TEMPLATE', 'bug.yml'))) - - const workspaceb = join(root, 'workspace', 'b') - await copyContent(workspaceb, root, config) - - const workspacebCi = join(root, '.github', 'workflows', 'ci-somenamespace-amazingb.yml') - await t.resolves(fs.stat(workspacebCi)) - const workspacebCiContent = await fs.readFile(workspacebCi, { encoding: 'utf-8' }) - t.match(workspacebCiContent, '@somenamespace/amazingb') - t.match(workspacebCiContent, join('workspace', 'b')) - t.notMatch(workspacebCiContent, '%%') -}) - -t.test('handles workspaces with no root repo files', async (t) => { - const pkgWithWorkspaces = { - 'package.json': JSON.stringify({ - name: 'testpkg', - templateOSS: { - applyRootRepoFiles: false, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: true, - - workspaces: ['workspace/a', 'workspace/b'], - }, - }), - workspace: { - a: { - 'package.json': JSON.stringify({ - name: 'amazinga', - }), - }, - b: { - 'package.json': JSON.stringify({ - name: 'amazingb', - }), - }, - }, - } - - const root = t.testdir(pkgWithWorkspaces) - const workspacea = join(root, 'workspace', 'a') - const config = await getConfig(root) - await copyContent(workspacea, root, config) - await copyContent(root, root, config) - - await t.resolves(fs.stat(join(root, '.github', 'workflows', 'ci-amazinga.yml'))) - await t.rejects(fs.stat(join(root, '.github', 'ISSUE_TEMPLATE', 'bug.yml'))) - await t.resolves(fs.stat(join(root, 'workspace', 'a', '.eslintrc.js'))) - await t.rejects(fs.stat(join(root, 'workspace', 'b', '.eslintrc.js'))) - await t.resolves(fs.stat(join(root, '.eslintrc.js'))) -}) - -t.test('handles workspaces with no root repo and repo files', async (t) => { - const pkgWithWorkspaces = { - 'package.json': JSON.stringify({ - name: 'testpkg', - templateOSS: { - applyRootRepoFiles: false, - applyWorkspaceRepoFiles: false, - applyRootModuleFiles: true, - - workspaces: ['workspace/a', 'workspace/b'], - }, - }), - workspace: { - a: { - 'package.json': JSON.stringify({ - name: 'amazinga', - }), - }, - b: { - 'package.json': JSON.stringify({ - name: 'amazingb', - }), - }, - }, - } - - const root = t.testdir(pkgWithWorkspaces) - const workspacea = join(root, 'workspace', 'a') - const config = await getConfig(root) - await copyContent(workspacea, root, config) - - await t.rejects(fs.stat(join(root, '.github', 'workflows', 'ci-amazinga.yml'))) - await t.rejects(fs.stat(join(root, '.github', 'ISSUE_TEMPLATE', 'bug.yml'))) - await t.resolves(fs.stat(join(root, 'workspace', 'a', '.eslintrc.js'))) - await t.rejects(fs.stat(join(root, 'workspace', 'b', '.eslintrc.js'))) - - await t.rejects(fs.stat(join(root, '.eslintrc.js'))) - await copyContent(root, root, config) - await t.resolves(fs.stat(join(root, '.eslintrc.js'))) - await t.rejects(fs.stat(join(root, '.github', 'ISSUE_TEMPLATE', 'bug.yml'))) -}) - -t.test('handles workspaces with no root files', async (t) => { - const pkgWithWorkspaces = { - 'package.json': JSON.stringify({ - name: 'testpkg', - templateOSS: { - applyRootRepoFiles: false, - applyWorkspaceRepoFiles: false, - applyRootModuleFiles: false, - - workspaces: ['workspace/a', 'workspace/b'], - }, - }), - workspace: { - a: { - 'package.json': JSON.stringify({ - name: 'amazinga', - }), - }, - b: { - 'package.json': JSON.stringify({ - name: 'amazingb', - }), - }, - }, - } - - const root = t.testdir(pkgWithWorkspaces) - const workspacea = join(root, 'workspace', 'a') - const config = await getConfig(root) - await copyContent(workspacea, root, config) - - await t.rejects(fs.stat(join(root, '.github', 'workflows', 'ci-amazinga.yml'))) - await t.rejects(fs.stat(join(root, '.github', 'ISSUE_TEMPLATE', 'bug.yml'))) - await t.resolves(fs.stat(join(root, 'workspace', 'a', '.eslintrc.js'))) - await t.rejects(fs.stat(join(root, 'workspace', 'b', '.eslintrc.js'))) - - await t.rejects(fs.stat(join(root, '.eslintrc.js'))) - await copyContent(root, root, config) - await t.rejects(fs.stat(join(root, '.eslintrc.js'))) - await t.rejects(fs.stat(join(root, '.github', 'workflows', 'ci-amazinga.yml'))) - await t.rejects(fs.stat(join(root, '.github', 'ISSUE_TEMPLATE', 'bug.yml'))) -}) - -t.test('no windows ci', async (t) => { - const pkgWithNoWindowsCI = { - 'package.json': JSON.stringify({ - name: 'testpkg', - templateOSS: { - windowsCI: false, - }, - }), - } - const root = t.testdir(pkgWithNoWindowsCI) - const config = await getConfig(root) - await copyContent(root, root, config) - const target = join(root, '.github', 'workflows', 'ci.yml') - const contents = await fs.readFile(target, 'utf8') - await t.notMatch(/windows/, contents, 'no windows ci') -}) diff --git a/test/postinstall/update-package.js b/test/postinstall/update-package.js deleted file mode 100644 index 5608f9b3..00000000 --- a/test/postinstall/update-package.js +++ /dev/null @@ -1,189 +0,0 @@ -const { join } = require('path') -const fs = require('@npmcli/fs') -const t = require('tap') - -const { - version: TEMPLATE_VERSION, - name: TEMPLATE_NAME, -} = require('../../package.json') - -const patchPackage = require('../../lib/postinstall/update-package.js') - -t.test('can patch a package.json', async (t) => { - const pkg = { - name: '@npmcli/foo', - version: '1.0.0', - author: 'someone else', - files: [], - license: 'MIT', - scripts: { - foo: 'echo bar', - }, - } - - const project = t.testdir({ - 'package.json': JSON.stringify(pkg, null, 2), - }) - - const needsAction = await patchPackage(project) - t.equal(needsAction, true, 'returned true') - - const contents = await fs.readFile(join(project, 'package.json'), { - encoding: 'utf8', - }) - const parsed = JSON.parse(contents) - t.match(parsed, patchPackage.changes, 'all changes were applied') - t.equal(parsed.scripts.foo, 'echo bar', 'did not remove existing script') -}) - -t.test('returns false when templateVersion matches own version', async (t) => { - const pkg = { - name: '@npmcli/foo', - templateOSS: { - version: TEMPLATE_VERSION, - }, - version: '1.0.0', - author: 'someone else', - files: [], - license: 'MIT', - } - - const project = t.testdir({ - 'package.json': JSON.stringify(pkg, null, 2), - }) - - const needsAction = await patchPackage(project) - t.equal(needsAction, false, 'returned false') - - const contents = await fs.readFile(join(project, 'package.json'), { - encoding: 'utf8', - }) - t.notMatch(JSON.parse(contents), patchPackage.changes, 'changes were NOT applied') -}) - -t.test('returns true when templateVersion matches own version when forced', async (t) => { - const pkg = { - name: '@npmcli/foo', - templateOSS: { - version: TEMPLATE_VERSION, - }, - version: '1.0.0', - author: 'someone else', - files: [], - license: 'MIT', - } - - const project = t.testdir({ - 'package.json': JSON.stringify(pkg, null, 2), - }) - - const needsAction = await patchPackage(project, undefined, { force: true }) - t.equal(needsAction, true, 'returned true') - - const contents = await fs.readFile(join(project, 'package.json'), { - encoding: 'utf8', - }) - const parsed = JSON.parse(contents) - t.match(parsed, patchPackage.changes, 'all changes were applied') -}) - -t.test('doesnt set templateVersion on own repo', async (t) => { - const pkg = { - name: TEMPLATE_NAME, - } - - const project = t.testdir({ - 'package.json': JSON.stringify(pkg, null, 2), - }) - - const needsAction = await patchPackage(project) - t.equal(needsAction, true, 'needs action') - - const contents = JSON.parse(await fs.readFile(join(project, 'package.json'), { - encoding: 'utf8', - })) - const version = (contents.templateOSS) ? - contents.templateOSS.version : contents.templateVersion - t.equal(version, undefined, 'did not get template version') -}) - -t.test('only sets templateVersion on root pkg when configured', async (t) => { - const pkgWithWorkspaces = { - 'package.json': JSON.stringify({ - name: 'testpkg', - templateOSS: { - applyRootRepoFiles: false, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: false, - - workspaces: ['amazinga'], - }, - }), - workspace: { - a: { - 'package.json': JSON.stringify({ - name: 'amazinga', - }), - }, - }, - } - const root = t.testdir(pkgWithWorkspaces) - await patchPackage(root, root, { - applyRootRepoFiles: false, - applyWorkspaceRepoFiles: true, - applyRootModuleFiles: false, - }) - - const contents = JSON.parse(await fs.readFile(join(root, 'package.json'), { - encoding: 'utf8', - })) - - t.not(contents.templateOSS.version, undefined, 'should set templateVersion') - t.equal(contents.author, undefined, 'should not set other fields') -}) - -t.test('converts template Version', async (t) => { - const pkg = { - name: 'testpkg', - templateVersion: '2.0.0', - } - - const project = t.testdir({ - 'package.json': JSON.stringify(pkg, null, 2), - }) - - const needsAction = await patchPackage(project) - t.equal(needsAction, true, 'needs action') - - const contents = JSON.parse(await fs.readFile(join(project, 'package.json'), { - encoding: 'utf8', - })) - t.equal(contents.templateVersion, undefined, 'did not get template version') - t.equal(contents.templateOSS.version, TEMPLATE_VERSION, 'did not get template version') -}) - -t.test('removes standard', async (t) => { - const pkg = { - name: 'testpkg', - scripts: { - test: 'test', - 'lint:fix': 'something', - }, - standard: { - ignore: [], - }, - } - - const project = t.testdir({ - 'package.json': JSON.stringify(pkg, null, 2), - }) - - const needsAction = await patchPackage(project) - t.equal(needsAction, true, 'needs action') - - const contents = JSON.parse(await fs.readFile(join(project, 'package.json'), { - encoding: 'utf8', - })) - t.equal(contents.standard, undefined, 'removed standard') - t.equal(contents.scripts['lint:fix'], undefined, 'removes lint:fix script') -}) diff --git a/test/postlint/check-gitignore.js b/test/postlint/check-gitignore.js deleted file mode 100644 index 3372583a..00000000 --- a/test/postlint/check-gitignore.js +++ /dev/null @@ -1,76 +0,0 @@ -const t = require('tap') -const { sync: which } = require('which') -const { spawnSync } = require('child_process') -const fs = require('fs') -const { resolve } = require('path') - -const check = require('../../lib/postlint/check-gitignore.js') - -const gitCwd = (cwd) => (args) => - spawnSync(which('git'), args.split(' '), { encoding: 'utf-8', cwd }).stdout - -const gitTestdir = (t, files) => { - const path = t.testdir(files) - const git = gitCwd(path) - - git('init') - git('add -A .') - git('commit -m "init"') - - const appendGitignore = (append) => { - fs.appendFileSync(resolve(path, '.gitignore'), append) - git('add -A .') - git('commit -m "update gitignore"') - } - - return { path, appendGitignore } -} - -t.test('will report tracked files in gitignore', async (t) => { - const { path, appendGitignore } = gitTestdir(t, { - '.gitignore': '# comment\n\n\n\nignored\n', - ignored: '', - tracked: '', - willIgnore1: '', - willIgnore2: '', - }) - - appendGitignore('willIgnore*\n') - - const problems = await check(path) - - t.equal(problems.length, 1) - t.strictSame(problems[0].message.match(/\bwillIgnore1\b/), ['willIgnore1']) - t.strictSame(problems[0].message.match(/\bwillIgnore2\b/), ['willIgnore2']) - t.matchSnapshot(problems) -}) - -t.test('no warnings', async (t) => { - const { path, appendGitignore } = gitTestdir(t, { - '.gitignore': 'ignored\n', - ignored: '', - tracked: '', - willIgnore1: '', - willIgnore2: '', - }) - - appendGitignore('wontIgnore*\n') - - const problems = await check(path) - - t.equal(problems.length, 0) - t.strictSame(problems, []) - t.matchSnapshot(problems) -}) - -t.test('noop with no .git', async (t) => { - const path = t.testdir() - t.strictSame(await check(path), []) -}) - -t.test('error with .git but no .gitignore', async (t) => { - const path = t.testdir({ - '.git': '', - }) - t.rejects(check(path), /\.gitignore must exist/) -}) diff --git a/test/postlint/check-package.js b/test/postlint/check-package.js deleted file mode 100644 index 1e3befec..00000000 --- a/test/postlint/check-package.js +++ /dev/null @@ -1,96 +0,0 @@ -const t = require('tap') - -const check = require('../../lib/postlint/check-package.js') -const { changes } = require('../../lib/postinstall/update-package.js') -const { name } = require('../../package.json') - -t.cleanSnapshot = (snapshot) => { - return snapshot.replace( - /("version" Expected: ").*(" Found)/g, - '$1$TEMPLATE_VERSION$2' - ) -} - -t.test('checks a package.json', (t) => { - t.plan(5) - - t.test('missing fields', async (t) => { - const project = t.testdir({ - 'package.json': '{}', - }) - - const problems = await check(project) - t.matchSnapshot(problems, 'problems') - t.equal(problems.length, 1) - }) - - t.test('incorrect fields', async (t) => { - const project = t.testdir({ - 'package.json': JSON.stringify({ - author: 'Bob', - }), - }) - - const problems = await check(project) - t.matchSnapshot(problems, 'problems') - t.equal(problems.length, 1) - }) - - t.test('incorrect object fields', async (t) => { - const project = t.testdir({ - 'package.json': JSON.stringify({ - scripts: {}, - }), - }) - - const problems = await check(project) - t.matchSnapshot(problems, 'problems') - t.equal(problems.length, 1) - }) - - t.test('unwanted deps', async (t) => { - const project = t.testdir({ - 'package.json': JSON.stringify({ - dependencies: { - ...check.unwantedPackages.reduce((acc, k) => { - acc[k] = '1' - return acc - }, {}), - }, - }), - }) - - const problems = await check(project) - t.matchSnapshot(problems, 'problems') - t.equal(problems.length, 2) - }) - - t.test('unwanted deps', async (t) => { - const project = t.testdir({ - 'package.json': JSON.stringify(changes), - }) - - const problems = await check(project) - t.matchSnapshot(problems, 'problems') - t.equal(problems.length, 0) - }) - - t.end() -}) - -t.test('this repo doesnt get version', async (t) => { - const pkg = { - ...changes, - name, - } - - delete pkg.templateVersion - delete pkg.templateOSS - - const project = t.testdir({ - 'package.json': JSON.stringify(pkg), - }) - - const needsAction = await check(project) - t.same(needsAction, []) -}) diff --git a/test/sequential/no-local-changes.js b/test/sequential/no-local-changes.js deleted file mode 100644 index 65c5fc6a..00000000 --- a/test/sequential/no-local-changes.js +++ /dev/null @@ -1,42 +0,0 @@ -const promiseSpawn = require('@npmcli/promise-spawn') -const t = require('tap') - -const spawn = (name, args) => promiseSpawn(name, args, { - stdioString: true, - shell: true, -}) - -// Check if the repo is clean, ignoring the tests -const isClean = () => spawn('git', - ['status', '--porcelain', '.', '--', ':!test/']) - -// When the tests are run via `npm run preversion` it should fail -// if any of the changes haven't already been applied to this repo. -// Also note that this test has to be sequential so that there are -// no test/tap-testdir-* being created. -t.test('commands dont change repo', async (t) => { - const startClean = await isClean() - - if (startClean.stdout) { - t.equal(startClean.stdout, '', 'git status must be clean') - const diff = await spawn('git', - ['--no-pager', 'diff', '--word-diff=porcelain']) - t.comment(`git diff: ${diff}`) - t.end() - return - } - - const postinstall = await spawn('npm', ['run', 'postinstall', '-s']) - const postlint = await spawn('npm', ['run', 'postlint', '-s']) - const clean = await isClean() - - t.equal(postinstall.stdout, '', 'postinstall stdout') - t.equal(postinstall.stderr, '', 'postinstall stderr') - t.equal(postinstall.code, 0, 'postinstall code') - - t.equal(postlint.stdout, '', 'postlint stdout') - t.equal(postlint.stderr, '', 'postlint stderr') - t.equal(postlint.code, 0, 'postlint code') - - t.equal(clean.stdout, '', 'git status is clean') -}) diff --git a/test/sequential/tap-parallel-not-ok b/test/sequential/tap-parallel-not-ok deleted file mode 100644 index e69de29b..00000000 diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 00000000..9331dca4 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,178 @@ +const t = require('tap') +const { join, isAbsolute, resolve } = require('path') +const { merge, defaults } = require('lodash') +const fs = require('@npmcli/fs') +const Git = require('@npmcli/git') +const output = require('../lib/util/output.js') +const apply = require('../lib/apply/index.js') +const check = require('../lib/check/index.js') +const CONTENT = require('..') + +const createPackageJson = (pkg) => ({ + 'package.json': JSON.stringify(pkg, null, 2), +}) + +const pkgWithName = (name, defName) => { + const pkg = typeof name === 'string' ? { name } : name + if (!pkg.name) { + pkg.name = defName + } + return pkg +} + +// minimum package.json for no errors +const okPackage = () => ({ ...CONTENT.requiredPackages }) + +const setupRoot = async (root, content) => { + const rootPath = (...p) => join(root, ...p) + + // fs methods for reading from the root + const rootFs = Object.fromEntries(Object.entries({ + readdir: fs.readdir, + readFile: (p) => fs.readFile(p, { encoding: 'utf-8' }), + writeFile: (p, d) => fs.writeFile(p, d, { encoding: 'utf-8' }), + appendFile: fs.appendFile, + stat: fs.stat, + unlink: fs.unlink, + }).map(([k, fn]) => [k, (p, ...rest) => fn(rootPath(p), ...rest)])) + + // Returns a recurisve list of relative file + // paths in the testdir root + const readdir = async (p = '') => { + const paths = [] + const rootRes = await rootFs.readdir(p) + for (const source of rootRes) { + const nextPath = join(p, source) + if (nextPath === '.git') { + continue + } + if ((await rootFs.stat(nextPath)).isDirectory()) { + paths.push(...await readdir(nextPath)) + } else { + paths.push(nextPath) + } + } + return paths.sort() + } + + // returns an object where the keys are relative file + // paths and the values are the full contents + const readdirSource = async (p = '') => { + const files = await readdir(p) + const contents = await Promise.all(files.map((f) => rootFs.readFile(f))) + return Object.fromEntries(files.map((f, i) => [f, contents[i]])) + } + + return { + root, + ...rootFs, + readdirSource, + readdir, + readJson: async (f) => JSON.parse(await rootFs.readFile(f)), + writeJson: (p, d) => rootFs.writeFile(p, JSON.stringify(d, null, 2)), + join: rootPath, + // use passed in content path or allow overriding per method call for tests + apply: () => apply(root, content), + check: () => check(root, content), + runAll: () => apply(root, content).then(() => check(root, content)), + } +} + +const setup = async (t, { + package = {}, + workspaces = {}, + testdir = {}, + content, + ok, +} = {}) => { + const wsLookup = {} + const pkg = merge( + pkgWithName(package, 'testpkg'), + ok ? okPackage() : {} + ) + + // convenience for passing in workspaces as an object + // and getting those converted to a proper workspaces array + // and adding the workspaces to the testdir + if (Object.keys(workspaces).length) { + const wsEntries = Object.entries(workspaces) + const wsDir = 'workspaces' + defaults(pkg, { workspaces: [] }) + merge(testdir, { [wsDir]: {} }) + + for (const [wsBase, wsPkgName] of wsEntries) { + const wsPkg = merge( + pkgWithName(wsPkgName, wsBase), + ok ? okPackage() : {} + ) + const wsPath = join(wsDir, wsBase) + // obj to lookup workspaces by path in tests + wsLookup[wsBase] = wsPath + merge(testdir[wsDir], { [wsBase]: createPackageJson(wsPkg) }) + pkg.workspaces.push(wsPath) + } + } + + // creates dir with a root package.json and + // package.json files for each workspace + const root = t.testdir(merge( + createPackageJson(pkg), + testdir + )) + + if (typeof content === 'string') { + // default content path is absolute but tests can either setup their + // own relative path or pass in objects. But if it is a path it has + // to be absolute to be passed in + content = isAbsolute(content) ? content : join(root, content) + } + + return { + ...(await setupRoot(root, content)), + workspaces: wsLookup, + } +} + +const setupGit = async (...args) => { + const s = await setup(...args) + const git = (arg) => Git.spawn(arg.split(' '), { cwd: s.root }) + + const gca = async () => { + await git('add -A .') + await git('commit -m "init"') + } + + await git('init') + await git('remote add origin git@github.com:testuser/myrepo.git') + await gca() + + return { + ...s, + git, + gca, + } +} + +const cleanSnapshot = (str) => str.replace(/\\+/g, '/').replace(/\r\n/g, '\n') +const formatSnapshots = { + readdir: (arr) => arr.join('\n').trim(), + readdirSource: (obj) => Object.entries(obj).map(([file, content]) => + [file, '='.repeat(40), content].join('\n').trim()).join('\n\n').trim(), + checks: (arr) => output(arr).trim(), +} + +module.exports = setup +module.exports.git = setupGit +module.exports.content = CONTENT +module.exports.clean = cleanSnapshot +module.exports.format = formatSnapshots +module.exports.okPackage = okPackage +module.exports.fixture = (f) => fs.readFile(resolve(__dirname, 'fixtures', f), 'utf-8') +module.exports.log = (t, f = () => true) => { + const cb = (...args) => f(...args) && console.error(...args) + process.on('log', cb) + t.teardown(() => process.off('log', cb)) +} + +// make tap not report this as skipping tests +t.ok(1) diff --git a/test/util/parse-ci-versions.js b/test/util/parse-ci-versions.js new file mode 100644 index 00000000..85226e76 --- /dev/null +++ b/test/util/parse-ci-versions.js @@ -0,0 +1,30 @@ +const t = require('tap') +const parse = require('../../lib/util/parse-ci-versions.js') + +const targets = [ + [[], ''], + [undefined, ''], + [['12.1.0'], '>=12.1.0'], + [['12.1.0', '12.2.0', '12'], '>=12.1.0'], + [['12'], '>=12.0.0'], + [['~12.5'], '>=12.5.0'], + [['12.99.0', '12.1.1'], '>=12.1.1'], + [['12.1.0', '12.x', '18.5.0'], '^12.1.0 || >=18.5.0'], + [['12.1.0', '12.x', '^18'], '^12.1.0 || >=18.0.0', '^18'], + [['12.1.0', '12.2.0', '~12.5'], '>=12.1.0', '~12.5'], + [['12.13.0', '12.x', '14.15.0', '14.x', '16.0.0', '16.x'], + '^12.13.0 || ^14.15.0 || >=16.0.0', '16.x'], + // this is to test current behavior but these go against + // the assumption that provided versions will always be lower + // then the upper bound of the range + [['12.99.0', '12.1.x'], '>=12.99.0'], + [['12.99.0', '12.100.0', '12.50.0', '12.2.x', '12.5.x'], '>=12.50.0'], +] + +targets.forEach(([target, engine, max]) => { + const found = parse(target) + t.equal(found.engines, engine, `${target} => ${engine}`) + if (max) { + t.equal(found.targets[found.targets.length - 1], max, 'sorted max version') + } +})