Merge pull request '[E00-S01-T11] apps/packages/extensions separated' (#378) from feature/164 into main
CI / Frozen lockfile install (push) Successful in 52s
CI / Frozen lockfile install (push) Successful in 52s
This commit was merged in pull request #378.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Workspace layout test — locks in the [E00-S01-T11] apps/packages/extensions
|
||||
* separation for the workspace.
|
||||
*
|
||||
* Acceptance criteria covered (each test fails without the committed layout):
|
||||
* - "apps, packages and extensions are separated in the workspace layout" →
|
||||
* the repo root contains exactly the three top-level package group
|
||||
* directories apps/, packages/, extensions/; every workspace package.json
|
||||
* lives exactly one level below one of them (never at the root, never in
|
||||
* another directory, never nested deeper); `pnpm-workspace.yaml` declares
|
||||
* one glob per group and `dependency-boundaries.json` maps the same three
|
||||
* groups to the same three directories.
|
||||
* - "each package sits in the correct top-level directory" → the bootstrap
|
||||
* packages are found where their group says: `@personal-blog/server` in
|
||||
* apps/, `@personal-blog/core` in packages/, `@personal-blog/example-
|
||||
* extension` in extensions/; the discovered workspace package set is
|
||||
* exactly the expected one, and every discovered package has a lockfile
|
||||
* importer (layout ↔ lockfile parity).
|
||||
*
|
||||
* Run: `node --test tests/workspace-layout.test.mjs`
|
||||
* (node:test — built into Node >= 18; no dependencies, lockfile untouched.)
|
||||
*/
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
|
||||
|
||||
/** The three top-level package group directories of the separated layout. */
|
||||
const TOP_LEVEL_GROUPS = ['apps', 'packages', 'extensions'];
|
||||
|
||||
/** Expected bootstrap packages: package name -> top-level directory it must sit in. */
|
||||
const EXPECTED_PACKAGES = {
|
||||
'@personal-blog/server': 'apps/server',
|
||||
'@personal-blog/core': 'packages/core',
|
||||
'@personal-blog/example-extension': 'extensions/example',
|
||||
};
|
||||
|
||||
/** Directories never scanned as package sources. */
|
||||
const IGNORED_DIRS = new Set(['node_modules', 'dist', 'coverage', '.git', '.pnpm-store']);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isDirectory(p) {
|
||||
try {
|
||||
return statSync(p).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collects every package.json path under a directory, as a
|
||||
* repo-root-relative posix path (the root workspace manifest itself is
|
||||
* included as "package.json").
|
||||
*/
|
||||
function collectPackageJsonPaths(dir) {
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
if (!IGNORED_DIRS.has(entry.name)) {
|
||||
results.push(...collectPackageJsonPaths(path.join(dir, entry.name)));
|
||||
}
|
||||
} else if (entry.name === 'package.json') {
|
||||
results.push(path.relative(REPO_ROOT, path.join(dir, entry.name)).split(path.sep).join('/'));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the placement of a repo-root-relative package.json path. A
|
||||
* workspace package must be exactly `<group>/<name>/package.json` under one of
|
||||
* the three top-level groups; the root workspace manifest ("package.json") is
|
||||
* the one allowed exception. Returns an error message, or null when the
|
||||
* placement is correct.
|
||||
*/
|
||||
function placementError(relPath) {
|
||||
if (relPath === 'package.json') return null; // workspace root manifest, not a package
|
||||
const parts = relPath.split('/');
|
||||
if (parts.length !== 3 || parts[2] !== 'package.json') {
|
||||
return `"${relPath}" must be <group>/<name>/package.json (exactly one level below a top-level group directory)`;
|
||||
}
|
||||
if (!TOP_LEVEL_GROUPS.includes(parts[0])) {
|
||||
return `"${relPath}" must sit under apps/, packages/ or extensions/ (got "${parts[0]}")`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('the workspace root contains exactly the three separated top-level group directories', () => {
|
||||
for (const group of TOP_LEVEL_GROUPS) {
|
||||
assert.ok(
|
||||
isDirectory(path.join(REPO_ROOT, group)),
|
||||
`top-level directory "${group}/" must exist (apps/packages/extensions separated)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('every workspace package.json sits exactly one level below its top-level group directory', () => {
|
||||
const manifests = collectPackageJsonPaths(REPO_ROOT);
|
||||
const packageManifests = manifests.filter((rel) => rel !== 'package.json');
|
||||
assert.ok(packageManifests.length > 0, 'the workspace must contain at least one package manifest');
|
||||
for (const rel of packageManifests) {
|
||||
const error = placementError(rel);
|
||||
assert.equal(error, null, error);
|
||||
}
|
||||
});
|
||||
|
||||
test('each expected package sits in the correct top-level directory', () => {
|
||||
for (const [name, dir] of Object.entries(EXPECTED_PACKAGES)) {
|
||||
const manifest = JSON.parse(read(path.join(dir, 'package.json')));
|
||||
assert.equal(
|
||||
manifest.name,
|
||||
name,
|
||||
`"${dir}/package.json" must declare name "${name}" (each package sits in its correct top-level directory)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the discovered workspace package set is exactly the expected bootstrap set', () => {
|
||||
const manifests = collectPackageJsonPaths(REPO_ROOT).filter((rel) => rel !== 'package.json');
|
||||
const discoveredDirs = new Set(manifests.map((rel) => rel.replace(/\/package\.json$/, '')));
|
||||
assert.deepEqual(discoveredDirs, new Set(Object.values(EXPECTED_PACKAGES)));
|
||||
});
|
||||
|
||||
test('pnpm-workspace.yaml declares one group glob per top-level group directory', () => {
|
||||
const workspace = read('pnpm-workspace.yaml');
|
||||
const packagesSection = workspace.slice(workspace.indexOf('packages:'));
|
||||
const globs = [...packagesSection.matchAll(/^\s*-\s*'([^']+)'\s*$/gm)].map((m) => m[1]);
|
||||
assert.deepEqual(
|
||||
globs,
|
||||
TOP_LEVEL_GROUPS.map((g) => `${g}/*`),
|
||||
'pnpm-workspace.yaml must declare exactly apps/*, packages/* and extensions/*',
|
||||
);
|
||||
});
|
||||
|
||||
test('dependency-boundaries.json maps the three groups to the same top-level directories', () => {
|
||||
const rule = JSON.parse(read('dependency-boundaries.json'));
|
||||
for (const group of TOP_LEVEL_GROUPS) {
|
||||
assert.equal(
|
||||
rule.groups?.[group]?.path,
|
||||
`${group}/`,
|
||||
`dependency-boundaries.json must map group "${group}" to path "${group}/"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('every discovered workspace package has a lockfile importer (layout ↔ lockfile parity)', () => {
|
||||
const lockfile = read('pnpm-lock.yaml');
|
||||
const manifests = collectPackageJsonPaths(REPO_ROOT).filter((rel) => rel !== 'package.json');
|
||||
for (const rel of manifests) {
|
||||
const importer = rel.replace(/\/package\.json$/, '');
|
||||
assert.ok(
|
||||
lockfile.includes(` ${importer}:`),
|
||||
`pnpm-lock.yaml must contain an importer for "${importer}" (every package in the layout must be a workspace member)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the placement validation flags mis-placed packages (non-vacuous probe)', () => {
|
||||
const valid = [
|
||||
'apps/server/package.json',
|
||||
'packages/core/package.json',
|
||||
'extensions/example/package.json',
|
||||
'package.json', // workspace root manifest
|
||||
];
|
||||
for (const rel of valid) {
|
||||
assert.equal(placementError(rel), null, `"${rel}" must be a valid placement`);
|
||||
}
|
||||
const invalid = [
|
||||
'lib/shared/package.json', // no top-level group at all
|
||||
'apps/package.json', // manifest directly in a group dir (no package name level)
|
||||
'packages/a/b/package.json', // nested deeper than one level
|
||||
'docs/package.json', // outside the three groups
|
||||
];
|
||||
for (const rel of invalid) {
|
||||
assert.notEqual(placementError(rel), null, `"${rel}" must be flagged as mis-placed`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user