@starci/hfs 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.1 - 2026-10-01
|
|
4
|
+
|
|
5
|
+
- Fixed: `hfs scaffold app` no longer writes a hand-made lockfile. The 4.0.0 stub held only the root entry, so `npm ci` in a new app failed with EUSAGE ("package.json and package-lock.json are not in sync"). Once the files are written, the scaffold runs `npm install --package-lock-only --ignore-scripts --no-audit --no-fund` in the new app root (registry or npm cache; no node_modules, no scripts), so the lockfile resolves every dependency of the root and its workspaces and `npm ci` accepts it. If npm cannot resolve it, the scaffold exits 2 with `HFS_SCAFFOLD_LOCK_FAILED`, names the step and removes the app it began: no stub lock and no app without a lock is left. There is no switch to skip the step.
|
|
6
|
+
- Fixed (false positive): R14 `HFS_DEP_VERSION_SKEW` does not report a bundled copy in the lockfile (`inBundle`, a package's bundleDependencies, e.g. the tslib inside `@tailwindcss/oxide-wasm32-wasi`): it ships inside its parent's tarball and nothing in the app can move it. A fresh scaffold's real lock has one.
|
|
7
|
+
|
|
3
8
|
## 4.0.0 - 2026-10-01
|
|
4
9
|
|
|
5
10
|
- Added: the scaffolded root `package.json` (and its lockfile root entry) pins `@starci/test-world` from `knowledge/hfs/canon-pins.yaml` (1.0.0), the package whose `starci-test-stack` bin the managed `test:stack` script runs.
|
package/bin/hfs.mjs
CHANGED
|
@@ -215,7 +215,7 @@ export async function main(argv, { stdout = (s) => process.stdout.write(s), stde
|
|
|
215
215
|
const [kind, name, ...extra] = opts.positional;
|
|
216
216
|
if (kind !== 'app' || !name || extra.length || opts.repo !== undefined) throw new Error('hfs scaffold takes `app <name> [--into <dir>]`');
|
|
217
217
|
const { root: created, files } = scaffoldApp({ name, into: path.resolve(opts.into ?? process.cwd()), presets: presets ?? await scaffoldPresets() });
|
|
218
|
-
stdout(`hfs scaffold app: created ${created} (${files.length} files); next: npm
|
|
218
|
+
stdout(`hfs scaffold app: created ${created} (${files.length} files); next: npm ci, then npm run lint\n`);
|
|
219
219
|
return 0;
|
|
220
220
|
}
|
|
221
221
|
if (opts.positional.length !== 1) throw new Error('hfs explain takes exactly one path');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "The HFS command line of a StarCi app (one repository: the root, be/ and fe/): hfs lint, check, scaffold app, explain, sync and work-hygiene. Self-contained: it carries the runtime files it reads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -24,19 +24,19 @@ pins:
|
|
|
24
24
|
source: packages/grammar/package.json
|
|
25
25
|
why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.8.0 (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
|
|
26
26
|
'@starci/eslint-canon-be':
|
|
27
|
-
version: 3.0.
|
|
27
|
+
version: 3.0.1
|
|
28
28
|
group: starci
|
|
29
29
|
install: registry
|
|
30
30
|
side: be
|
|
31
31
|
source: packages/eslint/be/package.json
|
|
32
|
-
why: '3.0.0: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
|
|
32
|
+
why: '3.0.1: its bundled canon-pins copy pins hfs 4.0.1. 3.0.0: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
|
|
33
33
|
'@starci/eslint-canon-fe':
|
|
34
|
-
version: 8.0.
|
|
34
|
+
version: 8.0.1
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/eslint/fe/package.json
|
|
39
|
-
why: '8.0.0: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
39
|
+
why: '8.0.1: its bundled canon-pins copy pins hfs 4.0.1. 8.0.0: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
40
40
|
'@starci/stylelint-canon':
|
|
41
41
|
version: 2.0.1
|
|
42
42
|
group: starci
|
|
@@ -71,12 +71,12 @@ pins:
|
|
|
71
71
|
source: packages/test-world/package.json
|
|
72
72
|
why: 'the shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end.'
|
|
73
73
|
'@starci/hfs':
|
|
74
|
-
version: 4.0.
|
|
74
|
+
version: 4.0.1
|
|
75
75
|
group: starci
|
|
76
76
|
install: registry
|
|
77
77
|
side: both
|
|
78
78
|
source: packages/hfs/package.json
|
|
79
|
-
why: '4.0.0: the app monorepo standard: one app repository with the root package.json, lockfile and hfs.json of kind app, and the be/ and fe/ sides; `hfs scaffold app <name>` makes it, `hfs lint` at the root lints be/** with the BE canon and fe/** with the FE canon; the standalone be/fe repository kinds and `hfs init` are deleted.'
|
|
79
|
+
why: '4.0.1: `hfs scaffold app` resolves the real lockfile with npm (`npm install --package-lock-only`) instead of writing a root-only stub that `npm ci` refuses. 4.0.0: the app monorepo standard: one app repository with the root package.json, lockfile and hfs.json of kind app, and the be/ and fe/ sides; `hfs scaffold app <name>` makes it, `hfs lint` at the root lints be/** with the BE canon and fe/** with the FE canon; the standalone be/fe repository kinds and `hfs init` are deleted.'
|
|
80
80
|
# --- tooling
|
|
81
81
|
typescript:
|
|
82
82
|
version: 5.9.3
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// - a dependency the root `overrides` pins to a version (a string that is not a `$name` reference) is declared at that version everywhere
|
|
5
5
|
// it is declared: an override the manifests disagree with is a second version in disguise;
|
|
6
6
|
// - the lockfile (package-lock.json, read, never installed) holds no nested copy of a dependency the workspace declares:
|
|
7
|
-
// `node_modules/<a>/node_modules/<name>` or `apps/<app>/node_modules/<name>` next to the hoisted `node_modules/<name>`.
|
|
7
|
+
// `node_modules/<a>/node_modules/<name>` or `apps/<app>/node_modules/<name>` next to the hoisted `node_modules/<name>`. A
|
|
8
|
+
// bundled copy (`inBundle`: it ships inside its parent's tarball, its bundleDependencies) is not one: no range, override or
|
|
9
|
+
// dedupe of the workspace can move it, so it is the parent's, not a second copy the workspace keeps.
|
|
8
10
|
import { found, readJson } from './read.mjs';
|
|
9
11
|
|
|
10
12
|
export const DEP_VERSION_SKEW = 'HFS_DEP_VERSION_SKEW';
|
|
@@ -54,7 +56,7 @@ export function depFindings({ repoRoot, files }) {
|
|
|
54
56
|
}
|
|
55
57
|
for (const [key, entry] of Object.entries(lock.packages)) {
|
|
56
58
|
const nested = NESTED.exec(key);
|
|
57
|
-
if (!nested || entry.link || HOISTED.test(key) || !declared.has(nested[1])) continue;
|
|
59
|
+
if (!nested || entry.link || entry.inBundle || HOISTED.test(key) || !declared.has(nested[1])) continue;
|
|
58
60
|
const name = nested[1];
|
|
59
61
|
findings.push(found(DEP_VERSION_SKEW, 'package-lock.json', `${key} is a nested copy of ${name}${entry.version ? ` ${entry.version}` : ''}${hoisted.has(name) ? ` next to the hoisted ${hoisted.get(name)}` : ''}; the workspace keeps one copy (align the ranges or add a root override)`, { dependency: name, lockPath: key, version: entry.version }));
|
|
60
62
|
}
|
package/scaffold/app.mjs
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The skeleton files are written once from templates/<app|be|fe>/skeleton ({{project}}, {{app}} and {{appPascal}} filled, a
|
|
13
13
|
// `__app__` folder named after the side's app); the managed files are the render of `hfs sync` (sync/index.mjs), so a fresh app
|
|
14
|
-
// is in sync by construction.
|
|
15
|
-
//
|
|
14
|
+
// is in sync by construction. The lockfile is never written by hand: once the files are written, npm resolves the real one
|
|
15
|
+
// (`npm install --package-lock-only`, no node_modules, no scripts), so `npm ci` installs the new app as it is. When npm cannot
|
|
16
|
+
// resolve it the scaffold fails (HFS_SCAFFOLD_LOCK_FAILED), names the step and removes the app it began, so no app without a lock
|
|
17
|
+
// and no stub lock is ever left. An existing directory is refused, never merged into.
|
|
16
18
|
import fs from 'node:fs';
|
|
17
19
|
import path from 'node:path';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
18
21
|
import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
|
|
19
22
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
20
23
|
import { TEMPLATES_DIR, renderTargets, writeTargets } from '../sync/index.mjs';
|
|
@@ -148,11 +151,30 @@ function skeletonOf(scope, app, vars) {
|
|
|
148
151
|
return files;
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
/** The npm step that resolves the lockfile of a new app, exactly as the error names it. */
|
|
155
|
+
export const LOCK_STEP = 'npm install --package-lock-only --ignore-scripts --no-audit --no-fund';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolves the real package-lock.json of the app at `root` with npm (the registry and the npm cache; no node_modules and no
|
|
159
|
+
* lifecycle script). `{ ok: true }` or `{ ok: false, detail }`.
|
|
160
|
+
*/
|
|
161
|
+
export function npmLock(root) {
|
|
162
|
+
// Through the shell (npm is npm.cmd on Windows, which runs only there); the command is the fixed literal LOCK_STEP.
|
|
163
|
+
const run = spawnSync(LOCK_STEP, { cwd: root, encoding: 'utf8', shell: true, windowsHide: true, timeout: 600_000 });
|
|
164
|
+
if (run.status === 0 && fs.existsSync(path.join(root, 'package-lock.json'))) return { ok: true };
|
|
165
|
+
const said = `${run.stderr ?? ''}\n${run.stdout ?? ''}`.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
166
|
+
// npm's own error lines (`npm error code ETARGET`, `npm error notarget No matching version found for x@1.2.3.`), without the log pointer.
|
|
167
|
+
const errors = said.filter(line => /^npm (?:error|ERR!)/i.test(line) && !/complete log/i.test(line)).map(line => line.replace(/^npm (?:error|ERR!)\s*/i, ''));
|
|
168
|
+
const reason = run.error ? String(run.error.message) : errors.slice(0, 2).join('; ') || said[0] || 'no output';
|
|
169
|
+
return { ok: false, detail: `exit ${run.status ?? 'none'}: ${reason}` };
|
|
170
|
+
}
|
|
171
|
+
|
|
151
172
|
/**
|
|
152
|
-
* `hfs scaffold app <name>`: writes the new app under `into
|
|
153
|
-
* is what sync loads from the installed @starci/jest-preset (the Sonar
|
|
173
|
+
* `hfs scaffold app <name>`: writes the new app under `into`, resolves its lockfile with npm (`lock`, npmLock), and returns
|
|
174
|
+
* `{ root, files }` (app-relative paths, sorted). `presets` is what sync loads from the installed @starci/jest-preset (the Sonar
|
|
175
|
+
* exclusions); the CLI passes the one it resolves. A failed lock step removes the app and throws HFS_SCAFFOLD_LOCK_FAILED.
|
|
154
176
|
*/
|
|
155
|
-
export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest() }) {
|
|
177
|
+
export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest(), lock = npmLock }) {
|
|
156
178
|
if (!NAME.test(String(name))) throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', `the app name ${name} must be kebab-case (a project name: ${NAME})`);
|
|
157
179
|
const root = path.join(into, name);
|
|
158
180
|
if (fs.existsSync(root)) throw new ScaffoldError('HFS_SCAFFOLD_EXISTS', `${root} already exists; hfs scaffold app never writes into an existing directory`);
|
|
@@ -163,7 +185,6 @@ export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest()
|
|
|
163
185
|
const files = [
|
|
164
186
|
{ path: 'hfs.json', content: jsonText(declaration) },
|
|
165
187
|
{ path: 'package.json', content: jsonText(pkg) },
|
|
166
|
-
{ path: 'package-lock.json', content: jsonText({ name, version: pkg.version, lockfileVersion: 3, requires: true, packages: { '': { name, version: pkg.version, dependencies: pkg.dependencies, devDependencies: pkg.devDependencies } } }) },
|
|
167
188
|
{ path: 'be/nest-cli.json', content: jsonText(nestCli(app)) },
|
|
168
189
|
...app.sides.fe.apps.map(entry => ({ path: `fe/apps/${entry.name}/tsconfig.json`, content: jsonText(nextAppTsconfig()) })),
|
|
169
190
|
...['app', 'be', 'fe'].flatMap(scope => skeletonOf(scope, app, { sonarGate: parseYaml(fs.readFileSync(SONAR_GATE_FILE, 'utf8')).gate.name })),
|
|
@@ -175,5 +196,10 @@ export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest()
|
|
|
175
196
|
}
|
|
176
197
|
const targets = renderTargets(declaration, presets, { manifest });
|
|
177
198
|
writeTargets(root, targets);
|
|
178
|
-
|
|
199
|
+
const locked = lock(root);
|
|
200
|
+
if (!locked.ok) {
|
|
201
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
202
|
+
throw new ScaffoldError('HFS_SCAFFOLD_LOCK_FAILED', `\`${LOCK_STEP}\` could not resolve the lockfile of ${root} (${locked.detail}); the app was removed. Check the network and the npm registry, then run hfs scaffold app ${name} again`);
|
|
203
|
+
}
|
|
204
|
+
return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), 'package-lock.json'])].sort() };
|
|
179
205
|
}
|