diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml new file mode 100644 index 0000000..c72a62d --- /dev/null +++ b/.github/workflows/node-ci.yml @@ -0,0 +1,161 @@ +name: Node CI + +# Installs a Node project once and runs its own package.json scripts in order. +# +# The scripts are the repo's, not this workflow's. A script named in `scripts` +# that the root package.json does not define is skipped and noted in the job +# summary, the same way rust-ci.yml falls back when a Makefile target is +# missing, so one default list serves a monorepo with turbo and a single +# package with plain tsc alike. +# +# The package manager comes from the lockfile: pnpm-lock.yaml means pnpm, +# anything else means npm. pnpm's version comes from the "packageManager" field +# in package.json unless `pnpm-version` overrides it, so CI runs the pnpm the +# lockfile was written with. + +on: + workflow_call: + inputs: + working-directory: + description: 'Directory holding the root package.json (and the lockfile).' + type: string + default: '.' + node-version: + description: >- + Node major. 24 by default, matching npm-publish.yml: npm 10 (Node 20 + and 22) mishandles lockfiles carrying a nested esbuild. + type: string + default: '24' + pnpm-version: + description: >- + pnpm version, for a repo without a "packageManager" field. Leave it + empty when the field exists: pnpm/action-setup refuses to run when the + two are both set and disagree. + type: string + default: '' + scripts: + description: >- + JSON array of root package.json scripts to run, in order. A script the + manifest does not define is skipped, not failed. + type: string + default: '["typecheck","lint","test","build"]' + env-vars: + description: >- + Extra environment for every script, one KEY=value per line. For values + a build needs at compile time, e.g. FORGE_DASHBOARD_URL=http://localhost:7901. + Not for secrets: these land in the job log. + type: string + default: '' + +jobs: + check: + name: Check (node ${{ inputs.node-version }}) + runs-on: ubuntu-latest + permissions: + contents: read + defaults: + run: + shell: bash + working-directory: ${{ inputs.working-directory }} + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Detect package manager + id: pm + run: | + set -euo pipefail + if [ ! -f package.json ]; then + echo "::error::no package.json in ${PWD}" + exit 1 + fi + if [ -f pnpm-lock.yaml ]; then + echo 'name=pnpm' >> "$GITHUB_OUTPUT" + echo 'lockfile=pnpm-lock.yaml' >> "$GITHUB_OUTPUT" + elif [ -f package-lock.json ]; then + echo 'name=npm' >> "$GITHUB_OUTPUT" + echo 'lockfile=package-lock.json' >> "$GITHUB_OUTPUT" + else + echo 'name=npm' >> "$GITHUB_OUTPUT" + echo 'lockfile=' >> "$GITHUB_OUTPUT" + fi + + - name: Set up pnpm + if: steps.pm.outputs.name == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.pnpm-version }} + package_json_file: ${{ inputs.working-directory }}/package.json + + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version: ${{ inputs.node-version }} + # Caching needs a lockfile to key on. Without one there is nothing + # stable to cache against, so leave it off. + cache: ${{ steps.pm.outputs.lockfile != '' && steps.pm.outputs.name || '' }} + cache-dependency-path: ${{ steps.pm.outputs.lockfile != '' && format('{0}/{1}', inputs.working-directory, steps.pm.outputs.lockfile) || '' }} + + - name: Export env-vars + env: + ENV_VARS: ${{ inputs.env-vars }} + run: | + set -euo pipefail + # Each line is checked before it reaches GITHUB_ENV. A line that is + # not KEY=value could otherwise smuggle a second variable in through + # an embedded delimiter. + while IFS= read -r line; do + [ -z "$line" ] && continue + if ! printf '%s' "$line" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*='; then + echo "::error::env-vars line is not KEY=value: $line" + exit 1 + fi + printf '%s\n' "$line" >> "$GITHUB_ENV" + echo "- env: \`${line%%=*}\`" >> "$GITHUB_STEP_SUMMARY" + done <<< "$ENV_VARS" + + - name: Install dependencies + env: + PM: ${{ steps.pm.outputs.name }} + LOCKFILE: ${{ steps.pm.outputs.lockfile }} + run: | + set -euo pipefail + if [ "$PM" = pnpm ]; then + pnpm install --frozen-lockfile + elif [ -n "$LOCKFILE" ]; then + npm ci + else + npm install + fi + printf -- '- install: %s%s\n' "$PM" "${LOCKFILE:+ (from $LOCKFILE)}" >> "$GITHUB_STEP_SUMMARY" + + - name: Run scripts + env: + PM: ${{ steps.pm.outputs.name }} + SCRIPTS: ${{ inputs.scripts }} + run: | + set -euo pipefail + if ! printf '%s' "$SCRIPTS" | jq -e 'type == "array"' >/dev/null; then + echo "::error::scripts must be a JSON array of script names" + exit 1 + fi + + ran=0 + while read -r script; do + if jq -e --arg s "$script" '.scripts[$s] // empty' package.json >/dev/null; then + echo "::group::$PM run $script" + "$PM" run "$script" + echo "::endgroup::" + printf -- '- %s: ran\n' "$script" >> "$GITHUB_STEP_SUMMARY" + ran=$((ran + 1)) + else + printf -- '- %s: skipped, not in package.json\n' "$script" >> "$GITHUB_STEP_SUMMARY" + fi + done < <(printf '%s' "$SCRIPTS" | jq -r '.[]') + + # A green run that ran nothing is almost always a typo in `scripts` + # or the wrong working-directory, so say so loudly. + if [ "$ran" -eq 0 ]; then + echo "::error::none of the requested scripts exist in package.json" + exit 1 + fi diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b128604..f019774 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -8,6 +8,14 @@ name: NPM Publish # the core first, and the adapters must build against the core's freshly built # dist rather than against whatever the registry last served. One job with an # ordered list gives both; a matrix gives neither. +# +# `package-manager: pnpm` is for pnpm workspaces, where packages point at each +# other with `workspace:*`. Plain npm cannot resolve that protocol and fails in +# `npm install` before anything is published. In pnpm mode the workspace is +# installed once, every listed package gets its version before any of them is +# packed, and `pnpm pack` turns each `workspace:` range into the real version. +# The tarball is checked for a leftover `workspace:` range and then handed to +# `npm publish`, so what reaches the registry is exactly what was checked. on: workflow_call: @@ -56,6 +64,25 @@ on: registry-url: type: string default: 'https://registry.npmjs.org' + package-manager: + description: >- + npm or pnpm. npm installs and builds each package on its own. pnpm + installs the workspace at `workspace-directory` once and packs with + pnpm, which rewrites `workspace:` ranges. See the header. + type: string + default: 'npm' + workspace-directory: + description: 'pnpm mode only: the workspace root holding pnpm-lock.yaml. `packages` stay relative to the repository root.' + type: string + default: '.' + skip-existing: + description: >- + Skip a package whose name@version is already on the registry instead + of failing on it. Lets a re-run finish a release that died half way + through the list. Off by default, because with it on a forgotten + version bump passes silently. + type: boolean + default: false secrets: NPM_TOKEN: description: 'Automation token with publish rights. Not required for a dry run.' @@ -77,6 +104,12 @@ jobs: - name: Checkout uses: actions/checkout@v7 + - name: Set up pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + package_json_file: ${{ inputs.workspace-directory }}/package.json + - name: Set up Node uses: actions/setup-node@v7 with: @@ -88,9 +121,18 @@ jobs: env: PACKAGES: ${{ inputs.packages }} VERSION: ${{ inputs.version }} + PM: ${{ inputs.package-manager }} run: | set -euo pipefail + case "$PM" in + npm|pnpm) ;; + *) + echo "::error::package-manager must be npm or pnpm, got '$PM'" + exit 1 + ;; + esac + # Fail on a malformed list here rather than half way through a publish # loop, where the packages before the bad entry are already public. if ! printf '%s' "$PACKAGES" | jq -e 'type == "array" and length > 0' >/dev/null; then @@ -122,6 +164,48 @@ jobs: exit 1 fi + - name: Prepare pnpm workspace + if: inputs.package-manager == 'pnpm' + shell: bash + env: + PACKAGES: ${{ inputs.packages }} + VERSION: ${{ inputs.version }} + WORKSPACE: ${{ inputs.workspace-directory }} + run: | + set -euo pipefail + + (cd "$WORKSPACE" && pnpm install --frozen-lockfile) + + # A runtime dependency on a workspace package that is not in the list + # packs as a pinned version nobody ever publishes, and every install + # of the dependent package then fails. devDependencies are exempt: + # consumers never install them. + listed=$(while read -r dir; do jq -r .name "$dir/package.json"; done < <(printf '%s' "$PACKAGES" | jq -r '.[]') | jq -R . | jq -sc .) + while read -r dir; do + missing=$(jq -r --argjson listed "$listed" ' + [.dependencies, .peerDependencies, .optionalDependencies] + | map(select(. != null) | to_entries[]) + | map(select((.value | tostring | startswith("workspace:")) and ((.key as $k | any($listed[]; . == $k)) | not)) | .key) + | join(", ")' "$dir/package.json") + if [ -n "$missing" ]; then + echo "::error::$dir depends on workspace package(s) that are not being published: $missing" + exit 1 + fi + done < <(printf '%s' "$PACKAGES" | jq -r '.[]') + + # Every version is stamped before anything is packed. pnpm resolves a + # `workspace:` range from the target package's package.json at pack + # time, so stamping inside the publish loop would pin early packages + # to the old version of the ones that come later. + if [ -n "$VERSION" ]; then + while read -r dir; do + tmp=$(mktemp) + jq --arg v "$VERSION" '.version = $v' "$dir/package.json" > "$tmp" + cat "$tmp" > "$dir/package.json" + rm -f "$tmp" + done < <(printf '%s' "$PACKAGES" | jq -r '.[]') + fi + - name: Publish id: publish shell: bash @@ -131,41 +215,82 @@ jobs: DRY_RUN: ${{ inputs.dry-run }} ACCESS: ${{ inputs.access }} BUILD_SCRIPT: ${{ inputs.build-script }} + PM: ${{ inputs.package-manager }} + SKIP_EXISTING: ${{ inputs.skip-existing }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | set -euo pipefail PUBLISHED='[]' + PACK_ROOT="$RUNNER_TEMP/npm-publish-pack" while read -r dir; do name=$(jq -r .name "$dir/package.json") echo "::group::$name ($dir)" pushd "$dir" >/dev/null - # `npm ci` when a lockfile is committed, `npm install` otherwise. - # `ci` is the stricter of the two and additionally fails when the - # lockfile and package.json disagree, so prefer it whenever it can - # run at all. - if [ -f package-lock.json ]; then npm ci; else npm install; fi + if [ "$PM" = pnpm ]; then + # Installed and stamped once, in the step above. + if [ -n "$BUILD_SCRIPT" ]; then + pnpm run "$BUILD_SCRIPT" + fi + else + # `npm ci` when a lockfile is committed, `npm install` otherwise. + # `ci` is the stricter of the two and additionally fails when the + # lockfile and package.json disagree, so prefer it whenever it can + # run at all. + if [ -f package-lock.json ]; then npm ci; else npm install; fi - if [ -n "$BUILD_SCRIPT" ]; then - npm run "$BUILD_SCRIPT" - fi + if [ -n "$BUILD_SCRIPT" ]; then + npm run "$BUILD_SCRIPT" + fi - # --no-git-tag-version: the caller's tag is the source of truth and - # this workspace is thrown away. Committing or tagging here would - # push a second tag for the release that triggered the run. - if [ -n "$VERSION" ]; then - npm version "$VERSION" --no-git-tag-version --allow-same-version + # --no-git-tag-version: the caller's tag is the source of truth and + # this workspace is thrown away. Committing or tagging here would + # push a second tag for the release that triggered the run. + if [ -n "$VERSION" ]; then + npm version "$VERSION" --no-git-tag-version --allow-same-version + fi fi published_version=$(jq -r .version package.json) + if [ "$SKIP_EXISTING" = "true" ] && npm view "$name@$published_version" version >/dev/null 2>&1; then + echo "$name@$published_version is already on the registry, skipping" + popd >/dev/null + echo "::endgroup::" + continue + fi + + # npm mode publishes the directory. pnpm mode publishes the tarball + # pnpm packed, after checking that no `workspace:` range survived. + target=. + if [ "$PM" = pnpm ]; then + dest="$PACK_ROOT/$(printf '%s' "$name" | tr '@/' '__')" + rm -rf "$dest" + mkdir -p "$dest" + pnpm pack --pack-destination "$dest" >/dev/null + target=$(find "$dest" -maxdepth 1 -name '*.tgz' -print -quit) + if [ -z "$target" ]; then + echo "::error::pnpm pack produced no tarball for $name" + exit 1 + fi + leaked=$(tar -xOzf "$target" package/package.json | jq -r ' + [.dependencies, .peerDependencies, .optionalDependencies, .devDependencies] + | map(select(. != null) | to_entries[]) + | map(select(.value | tostring | startswith("workspace:")) | .key) + | join(", ")') + if [ -n "$leaked" ]; then + echo "::error::$name packed with workspace: ranges still in it: $leaked" + exit 1 + fi + fi + if [ "$DRY_RUN" = "true" ]; then - npm publish --dry-run --access "$ACCESS" + npm publish "$target" --dry-run --access "$ACCESS" echo "would publish $name@$published_version" else - npm publish --access "$ACCESS" + npm publish "$target" --access "$ACCESS" PUBLISHED=$(printf '%s' "$PUBLISHED" | jq -c --arg e "$name@$published_version" '. + [$e]') fi diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index 72fdeb5..c27303d 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -134,6 +134,56 @@ jobs: working-directory: testdata/fixture-rust skip-audit: false + smoke-node-pnpm: + name: 'Smoke: Node CI, pnpm workspace' + permissions: + contents: read + uses: ./.github/workflows/node-ci.yml + with: + working-directory: testdata/fixture-node-pnpm + # The fixture's build script exits 1 unless this arrives, and it has no + # typecheck script, so one run covers env-vars and the skip path. + env-vars: | + FIXTURE_FLAG=on + + smoke-node-npm: + name: 'Smoke: Node CI, npm without a lockfile' + permissions: + contents: read + uses: ./.github/workflows/node-ci.yml + with: + working-directory: testdata/fixture-node-npm + + smoke-npm-publish-pnpm: + name: 'Smoke: npm publish, pnpm workspace dry run' + permissions: + contents: read + uses: ./.github/workflows/npm-publish.yml + with: + package-manager: pnpm + workspace-directory: testdata/fixture-node-pnpm + # adapter needs core at runtime and plugin peers on it, so a packed + # manifest with a leftover workspace: range fails this job. + packages: >- + ["testdata/fixture-node-pnpm/packages/core", + "testdata/fixture-node-pnpm/packages/adapter", + "testdata/fixture-node-pnpm/packages/plugin"] + version: '1.2.3' + build-script: '' + skip-existing: true + dry-run: true + + smoke-npm-publish-npm: + name: 'Smoke: npm publish, single package dry run' + permissions: + contents: read + uses: ./.github/workflows/npm-publish.yml + with: + packages: '["testdata/fixture-node-npm"]' + version: '1.2.3' + build-script: '' + dry-run: true + # A single stable check for branch protection to require. # # Requiring the individual jobs does not work here: their check names carry @@ -159,6 +209,10 @@ jobs: - smoke-go-node - smoke-rust-make - smoke-rust + - smoke-node-pnpm + - smoke-node-npm + - smoke-npm-publish-pnpm + - smoke-npm-publish-npm runs-on: ubuntu-latest steps: - name: Check every gating job succeeded diff --git a/.gitignore b/.gitignore index 8282e0f..a397a1f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ RELEASE_NOTES.md semantic-release.log target/ +node_modules/ diff --git a/README.md b/README.md index d521ea2..a45f7b8 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ jobs: | `go-binary-release.yml` | GoReleaser cross-platform binaries | | `codeql.yml` | CodeQL analysis for Go | | `rust-ci.yml` | lint, security audit, build + test for Rust | +| `node-ci.yml` | one install, then the repo's own typecheck, lint, test and build scripts | +| `npm-publish.yml` | ordered npm publishing, with a pnpm workspace mode | | `semantic-release.yml` | conventional-commit versioning and GitHub releases | ## Pinned tool versions @@ -339,3 +341,51 @@ Rust crates frequently need native build dependencies. Pass them with `dist-workspace.toml`, and `farp-rust` is released by `farp`'s semantic-release because its version is `farp`'s version. Both work; neither should be replaced by a shared workflow. + +## Node track + +`node-ci.yml` installs your project once and runs the root package.json scripts +you list, in order. The default list is typecheck, lint, test and build. A +script your manifest doesn't define is skipped, and the job summary says so, +which lets one call serve a turbo monorepo and a single package with plain tsc. +If none of the listed scripts exist the job fails, because a green run that ran +nothing is nearly always a typo or the wrong `working-directory`. + +The lockfile picks the package manager. `pnpm-lock.yaml` means pnpm, at the +version in your `packageManager` field. Anything else means npm: `npm ci` with a +`package-lock.json`, `npm install` without one. + +```yaml +jobs: + ci: + uses: xraph/workflows/.github/workflows/node-ci.yml@v1 + with: + scripts: '["typecheck","lint","test:ci","build"]' + env-vars: | + FORGE_DASHBOARD_URL=http://localhost:7901/dashboard +``` + +`env-vars` takes one `KEY=value` per line and gives it to every script. It ends +up in the job log, so keep secrets out of it. + +### Publishing a pnpm workspace + +By default `npm-publish.yml` installs each package with plain npm, and plain npm +can't resolve `workspace:*`. Set `package-manager: pnpm` and the workflow +installs the workspace once, stamps `version` on every listed package before it +packs any of them, and packs with pnpm. pnpm turns `workspace:*` into the +release version and `workspace:^` into a caret range on it, so give internal +peer dependencies `workspace:^` and they follow every release. Each tarball is +checked for a leftover `workspace:` range before `npm publish` sees it. + +The workflow also refuses a list that leaves out a package another one needs. A +runtime, peer or optional dependency on an unlisted workspace package would pack +as a version nobody ever publishes. devDependencies don't count, since consumers +never install them. + +`skip-existing: true` skips any `name@version` the registry already has, so you +can re-run a release that died half way through a long list. It's off by +default because it hides a forgotten version bump just as quietly. + +`examples/node-library` has a CI caller and a tag-driven release that publishes +a pnpm workspace. diff --git a/examples/node-library/ci.yml b/examples/node-library/ci.yml new file mode 100644 index 0000000..d1ace94 --- /dev/null +++ b/examples/node-library/ci.yml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + ci: + uses: xraph/workflows/.github/workflows/node-ci.yml@v1 + # Runs typecheck, lint, test and build from the root package.json, skipping + # any the manifest doesn't define. Pass `scripts` to change the list. diff --git a/examples/node-library/release.yml b/examples/node-library/release.yml new file mode 100644 index 0000000..b9d070d --- /dev/null +++ b/examples/node-library/release.yml @@ -0,0 +1,65 @@ +name: Release + +# Push a vX.Y.Z tag and every package below goes to npm at X.Y.Z, after CI +# passes on the tagged commit. Dispatch it by hand to rehearse: dry-run is on +# unless you switch it off. + +on: + push: + tags: ['v[0-9]+.[0-9]+.[0-9]+'] + workflow_dispatch: + inputs: + dry-run: + type: boolean + default: true + version: + description: 'Version to stamp, without a leading v. Empty keeps each package.json version.' + type: string + default: '' + +permissions: + contents: read + +jobs: + ci: + uses: xraph/workflows/.github/workflows/node-ci.yml@v1 + + # The expression language has no string replace, and npm-publish.yml refuses + # a version with a leading v, so the tag is trimmed here. + prepare: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.resolve.outputs.version }} + dry-run: ${{ steps.resolve.outputs.dry_run }} + steps: + - id: resolve + shell: bash + env: + EVENT: ${{ github.event_name }} + REF_NAME: ${{ github.ref_name }} + INPUT_VERSION: ${{ inputs.version }} + INPUT_DRY_RUN: ${{ inputs.dry-run }} + run: | + set -euo pipefail + if [ "$EVENT" = push ]; then + echo "version=${REF_NAME#v}" >> "$GITHUB_OUTPUT" + echo "dry_run=false" >> "$GITHUB_OUTPUT" + else + echo "version=${INPUT_VERSION#v}" >> "$GITHUB_OUTPUT" + echo "dry_run=${INPUT_DRY_RUN:-true}" >> "$GITHUB_OUTPUT" + fi + + publish: + needs: [ci, prepare] + uses: xraph/workflows/.github/workflows/npm-publish.yml@v1 + with: + package-manager: pnpm + # Dependencies first. Internal peer ranges should be `workspace:^` so they + # pack as a caret range on this release. + packages: '["packages/core","packages/react"]' + version: ${{ needs.prepare.outputs.version }} + dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} + build-script: build + skip-existing: true + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/testdata/fixture-node-npm/index.js b/testdata/fixture-node-npm/index.js new file mode 100644 index 0000000..4ba52ba --- /dev/null +++ b/testdata/fixture-node-npm/index.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/testdata/fixture-node-npm/package.json b/testdata/fixture-node-npm/package.json new file mode 100644 index 0000000..06d63fb --- /dev/null +++ b/testdata/fixture-node-npm/package.json @@ -0,0 +1,9 @@ +{ + "name": "@xraph-fixture/single", + "version": "0.0.0", + "main": "index.js", + "files": ["index.js"], + "scripts": { + "test": "node -e \"require('./index.js')\"" + } +} diff --git a/testdata/fixture-node-pnpm/package.json b/testdata/fixture-node-pnpm/package.json new file mode 100644 index 0000000..85377c0 --- /dev/null +++ b/testdata/fixture-node-pnpm/package.json @@ -0,0 +1,10 @@ +{ + "name": "fixture-node-pnpm", + "private": true, + "packageManager": "pnpm@10.21.0", + "scripts": { + "lint": "node -e \"process.exit(0)\"", + "test": "pnpm -r run test", + "build": "node -e \"if (process.env.FIXTURE_FLAG !== 'on') { console.error('FIXTURE_FLAG did not reach the script'); process.exit(1) }\"" + } +} diff --git a/testdata/fixture-node-pnpm/packages/adapter/index.js b/testdata/fixture-node-pnpm/packages/adapter/index.js new file mode 100644 index 0000000..a107cad --- /dev/null +++ b/testdata/fixture-node-pnpm/packages/adapter/index.js @@ -0,0 +1 @@ +module.exports = { core: require('@xraph-fixture/core').name } diff --git a/testdata/fixture-node-pnpm/packages/adapter/package.json b/testdata/fixture-node-pnpm/packages/adapter/package.json new file mode 100644 index 0000000..392956e --- /dev/null +++ b/testdata/fixture-node-pnpm/packages/adapter/package.json @@ -0,0 +1,12 @@ +{ + "name": "@xraph-fixture/adapter", + "version": "0.0.0", + "main": "index.js", + "files": ["index.js"], + "scripts": { + "test": "node -e \"if (require('./index.js').core !== 'core') process.exit(1)\"" + }, + "dependencies": { + "@xraph-fixture/core": "workspace:*" + } +} diff --git a/testdata/fixture-node-pnpm/packages/core/index.js b/testdata/fixture-node-pnpm/packages/core/index.js new file mode 100644 index 0000000..d4890e3 --- /dev/null +++ b/testdata/fixture-node-pnpm/packages/core/index.js @@ -0,0 +1 @@ +module.exports = { name: 'core' } diff --git a/testdata/fixture-node-pnpm/packages/core/package.json b/testdata/fixture-node-pnpm/packages/core/package.json new file mode 100644 index 0000000..8702f9c --- /dev/null +++ b/testdata/fixture-node-pnpm/packages/core/package.json @@ -0,0 +1,9 @@ +{ + "name": "@xraph-fixture/core", + "version": "0.0.0", + "main": "index.js", + "files": ["index.js"], + "scripts": { + "test": "node -e \"require('./index.js')\"" + } +} diff --git a/testdata/fixture-node-pnpm/packages/plugin/index.js b/testdata/fixture-node-pnpm/packages/plugin/index.js new file mode 100644 index 0000000..f92032c --- /dev/null +++ b/testdata/fixture-node-pnpm/packages/plugin/index.js @@ -0,0 +1 @@ +module.exports = { plugin: true } diff --git a/testdata/fixture-node-pnpm/packages/plugin/package.json b/testdata/fixture-node-pnpm/packages/plugin/package.json new file mode 100644 index 0000000..2e7a4b0 --- /dev/null +++ b/testdata/fixture-node-pnpm/packages/plugin/package.json @@ -0,0 +1,15 @@ +{ + "name": "@xraph-fixture/plugin", + "version": "0.0.0", + "main": "index.js", + "files": ["index.js"], + "scripts": { + "test": "node -e \"require('./index.js')\"" + }, + "peerDependencies": { + "@xraph-fixture/core": "workspace:^" + }, + "devDependencies": { + "@xraph-fixture/core": "workspace:*" + } +} diff --git a/testdata/fixture-node-pnpm/pnpm-lock.yaml b/testdata/fixture-node-pnpm/pnpm-lock.yaml new file mode 100644 index 0000000..e49d7d5 --- /dev/null +++ b/testdata/fixture-node-pnpm/pnpm-lock.yaml @@ -0,0 +1,23 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + packages/adapter: + dependencies: + '@xraph-fixture/core': + specifier: workspace:* + version: link:../core + + packages/core: {} + + packages/plugin: + devDependencies: + '@xraph-fixture/core': + specifier: workspace:* + version: link:../core diff --git a/testdata/fixture-node-pnpm/pnpm-workspace.yaml b/testdata/fixture-node-pnpm/pnpm-workspace.yaml new file mode 100644 index 0000000..dee51e9 --- /dev/null +++ b/testdata/fixture-node-pnpm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*"