Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: current
# setup-node caches npm when package.json has devEngines, and caching
# needs a lockfile, which this repo doesn't keep
package-manager-cache: false
- name: Run npm install
run: npm install --workspaces=false
- name: Run npm lint
Expand Down Expand Up @@ -114,6 +117,7 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: current
package-manager-cache: false
# the packages import each other, so link the workspaces
- name: Run npm install
run: npm install --workspaces --include-workspace-root
Expand Down Expand Up @@ -147,6 +151,7 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node-version }}
package-manager-cache: false
- name: Install most recent npm
run: npm install -g npm
- name: Run npm install -w ${{ matrix.package }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: lts/*
# setup-node caches npm when package.json has devEngines, and caching
# needs a lockfile, which this repo doesn't keep
package-manager-cache: false
- name: Run npm install
run: npm install --workspaces=false
- name: Build the docs
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/nightly-static-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: latest
# setup-node caches npm when package.json has devEngines, and caching
# needs a lockfile, which this repo doesn't keep
package-manager-cache: false
- name: Run npm install
run: npm install --workspaces --include-workspace-root
- name: Run build:default
Expand Down
15 changes: 15 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
package-lock=false

## Avoid installing packages published in last 3 days.
min-release-age=3

## Don't install packages from git urls.
allow-git=none

## Disable git entirely (false is a POSIX command that always fails).
git=false

## Disable installing dependencies from remote URLs.
allow-remote=none

## Pin allowed scripts to exact versions of dependencies to prevent unexpected script execution.
allow-scripts-pin=true
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ nodevu is composed of a set of modules:

## Development

Installing needs npm 11.17 or newer, which ships with Node.js 24 and later. `.npmrc` skips packages published in the last 3 days and blocks git and URL dependencies. npm only runs the dependency install scripts that `allowScripts` in `package.json` approves, and `npm approve-scripts <pkg>` adds one pinned to its installed version.

- `npm test` and `npm run coverage` run every module's tests.
- `npm run lint` checks the whole repo with oxlint and oxfmt, and checks with [docov](https://github.com/cutenode/docov) that everything the modules export has JSDoc. `docov.jsonc` sets what it scans.
- `npm run lint` checks the whole repo with oxlint and oxfmt, and checks with [docov](https://github.com/cutenode/docov) that everything the modules export has JSDoc. `docov.jsonc` sets what it scans. `npm run lint:harden` tests `tools/harden/` and runs [@lavamoat/harden](https://github.com/LavaMoat/LavaMoat/tree/main/packages/harden) to check that `.npmrc` and `package.json` keep every hardening setting, except the ones `tools/harden/decisions.json` turns off.
- `npm run types` type-checks the JSDoc and writes each module's TypeScript declarations to its `types/` folder. Commit them: CI fails when they're out of date.
- `npm run docs:dev` builds the docs from `docs/pages/` with [doc-kit](https://doc-kit.nodejs.org/) and serves them locally. `npm run docs` only builds them, into `out/`.

Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"description": "comprehensive node.js version tooling",
"main": "index.js",
"scripts": {
"lint": "oxlint && oxfmt --check && docov",
"lint": "oxlint && oxfmt --check && docov && npm run lint:harden",
"lint:write": "oxlint --fix && oxfmt",
"lint:harden": "node --test tools/harden/test/*.test.mjs && harden check -p npm -l strict --json | node tools/harden/check.mjs",
"coverage": "npm run coverage --ws --if-present",
"test": "npm run test --ws",
"updates": "npm run updates --ws",
Expand Down Expand Up @@ -43,6 +44,7 @@
"@doc-kit/cli": "1.0.5",
"@doc-kit/generator-react": "0.5.0",
"@docov/cli": "0.1.1",
"@lavamoat/harden": "0.8.1",
"@node-core/ui-components": "1.8.1",
"@oxlint/plugins": "1.89.0",
"@types/luxon": "3.7.6",
Expand All @@ -55,5 +57,15 @@
},
"overrides": {
"hast-util-raw": "9.1.0"
},
"devEngines": {
"packageManager": {
"name": "npm",
"version": ">=11.17.0",
"onFail": "error"
}
},
"allowScripts": {
"fsevents@2.3.3": false
}
}
21 changes: 21 additions & 0 deletions tools/harden/check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// `harden check` can only check a whole level, so this reads its strict
// `--json` report from stdin and fails on every opinion that isn't applied,
// except the ones decisions.json turns off.
import { readFile } from 'node:fs/promises';
import { text } from 'node:stream/consumers';

const decisions = JSON.parse(
await readFile(new URL('decisions.json', import.meta.url), 'utf8'),
);

const { checks } = JSON.parse(await text(process.stdin));

const missing = checks.filter(
({ id, satisfied }) => satisfied !== 1 && decisions[id] !== false,
);

for (const { id } of missing) {
console.error(`harden: ${id} is not applied`);
}

process.exitCode = missing.length === 0 ? 0 : 1;
6 changes: 6 additions & 0 deletions tools/harden/decisions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"n_scripts": "n_allowscripts",
"n_deny_existing_lifecycle": true,
"n_nogit": true,
"n_runner": false
}
57 changes: 57 additions & 0 deletions tools/harden/test/check.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from 'node:assert';
import { spawnSync } from 'node:child_process';
import { describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';

const script = fileURLToPath(new URL('../check.mjs', import.meta.url));

/**
* Runs check.mjs with `input` on stdin, as `harden check --json` would pipe it.
* @param {string} input
*/
function check(input) {
return spawnSync(process.execPath, [script], { input, encoding: 'utf8' });
}

/**
* Builds a `harden check --json` report where only `unapplied` aren't satisfied.
* @param {string[]} unapplied
*/
function report(unapplied) {
const checks = ['n_engines', 'n_nogit', 'n_runner'].map((id) => ({
id,
satisfied: unapplied.includes(id) ? 0 : 1,
}));

return JSON.stringify({ checks });
}

describe('check.mjs', () => {
it('passes when every opinion is applied', () => {
const result = check(report([]));

assert.strictEqual(result.status, 0);
assert.strictEqual(result.stderr, '');
});

it('fails on an opinion that is not applied', () => {
const result = check(report(['n_nogit']));

assert.strictEqual(result.status, 1);
assert.strictEqual(result.stderr, 'harden: n_nogit is not applied\n');
});

it('passes when the only unapplied opinion is declined in decisions.json', () => {
const result = check(report(['n_runner']));

assert.strictEqual(result.status, 0);
assert.strictEqual(result.stderr, '');
});

it('fails on empty input', () => {
const result = check('');

assert.notStrictEqual(result.status, 0);
assert.match(result.stderr, /SyntaxError/);
});
});
Loading