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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"forwardPorts": [3000],
"containerUser": "vscode",
"postCreateCommand": "npm install",
"postCreateCommand": "npm ci",
"waitFor": "postCreateCommand", // otherwise automated jest tests fail
"features": {
"node": {
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ fail the PR.

## Build & Test

- **Install**: `npm install`
- **Install**: `npm ci` — installs the exact versions in `package-lock.json`, as
CI does. Use `npm install <package>` only to add or upgrade a dependency on
purpose; it re-resolves the semver ranges and rewrites the lockfile.
- **Start Dev Server**: `npm run docus:start`
- **Build**: `npm run build`
- **Unit Tests**: `npm run test:unit` — required "Validate repository" CI check
Expand Down
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ This document covers the development workflow and the data contribution model.
Prerequisites: Node.js 22+ (LTS recommended) and npm.

```bash
npm install
npm ci
npm run docus:start
```

`npm ci` installs the exact, integrity-checked versions in `package-lock.json`,
matching CI. Reach for `npm install <package>` only to add or upgrade a
dependency, and commit the resulting lockfile change deliberately.

The dev server runs at `http://localhost:3000`. In the devcontainer:

```bash
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ Ensure you have the following installed:
### Install dependencies

```bash
npm install
npm ci
```

`npm ci` installs exactly the versions recorded in `package-lock.json` and
verifies their integrity hashes, which is what CI runs. Use
`npm install <package>` only when you intend to add or upgrade a dependency;
that re-resolves the ranges in `package.json` and rewrites the lockfile, so the
change belongs in its own reviewed commit.

### Run the site

```bash
Expand Down
49 changes: 49 additions & 0 deletions tests/dev-environment.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,55 @@ test('every npm script invoked by devcontainer lifecycle commands exists', () =>
);
});

// CI installs with `npm ci`, the only npm command that treats
// package-lock.json as an instruction: exact versions, integrity hashes
// verified, lockfile left alone. A bare `npm install` re-resolves the caret
// ranges in package.json against the registry, runs the lifecycle scripts of
// whatever it picked, and rewrites the lockfile in the contributor's tree — so
// a doc that prescribes it hands every contributor an unreviewed dependency
// graph and lets an upgrade ride into an unrelated PR. `npm install <pkg>` is
// still the right way to add a dependency, so only the bare form is rejected.
const BARE_NPM_INSTALL = /\bnpm install\s*(?=$|[\n`'"&|;])/;

test('no developer doc or devcontainer command prescribes a bare npm install', () => {
const offenders = [];

for (const doc of ['README.md', 'CONTRIBUTING.md', 'AGENTS.md']) {
const lines = readFileSync(join(root, doc), 'utf8').split('\n');
for (const [index, line] of lines.entries()) {
if (BARE_NPM_INSTALL.test(line)) offenders.push(`${doc}:${index + 1}`);
}
}

const devcontainer = JSON.parse(stripJsonComments(devcontainerRaw));
for (const key of [
'initializeCommand',
'onCreateCommand',
'updateContentCommand',
'postCreateCommand',
'postStartCommand',
'postAttachCommand',
]) {
const value = devcontainer[key];
const commands =
typeof value === 'string'
? [value]
: Array.isArray(value)
? [value.join(' ')]
: value && typeof value === 'object'
? Object.values(value).map((entry) => String(entry))
: [];
if (commands.some((command) => BARE_NPM_INSTALL.test(command)))
offenders.push(`devcontainer.${key}`);
}

assert.deepEqual(
offenders,
[],
`these prescribe a bare \`npm install\`, which ignores package-lock.json; use \`npm ci\`: ${offenders.join(', ')}`,
);
});

// The dev port the devcontainer forwards has to be the one `just serve`
// actually opens, or the recipe starts a server nobody outside the container
// can reach.
Expand Down