mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* fix(build): include shared UI stylesheets in Nx cache inputs `libs/ui/styles` held shared SCSS partials but had no `project.json`, so its files belonged to no Nx project and were absent from every task hash. Editing a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and shipped the previous CSS — a silent wrong build rather than a failure. Nx derives its project graph from TypeScript imports only, so a relative Sass `@use` that crosses a project root creates no edge. Verified directly: after adding the project but before declaring anything, `ui-styles` still had zero dependents in the graph. Make it the `ui-styles` project (no targets — it exists to be hashed) and declare `implicitDependencies` on the 8 consumers. Chosen over adding the path to `sharedGlobals`, which would put shared styles into every project's hash and make a one-line SCSS tweak mark the whole workspace affected. A styles edit now marks 15 projects affected and leaves electron-backend, website, the mock servers and the shared libs alone. `libs/ui/styles` was the only projectless directory holding files under `libs/` or `apps/`. Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every relative stylesheet import against Nx's real project graph and fails when one escapes the input closure of a build that compiles it, naming the project to declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of `apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes that file, and a lib -> app edge would make the graph cyclic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(build): spawn git without a shell in the stylesheet check `execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where single quotes are literal characters rather than quoting. Git received the pathspec with the quotes intact, matched nothing and exited 0, so `styles:inputs:validate` reported success after checking zero stylesheets — silently disabling the check for Windows developers while staying green. Spawn with `execFileSync` so no shell is involved and git expands its own pathspec; verified to return the identical 133 files. Both this and the eslint glob trap next to it in the docs report success while covering nothing, so also make an empty scan fail rather than pass: the workspace always contains SCSS, and a listing that returns none means the scan broke. Reported by Codex review on #1360. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(styles): move nav-list partial into ui-styles (#1361) * fix(build): count every target of a comma-separated Sass @import `@import` is the only rule that takes a list, and the scan read just its first target. A later entry crossing an Nx project boundary escaped the cache key while the check still reported success — the same silent-pass failure the tool exists to prevent. Parse every target of an `@import` list. The obvious "read all quoted strings" fix trades one silent gap for a phantom one, so the rule decides: `@use`/`@forward` load exactly one module and a quoted string after it is `with (...)` configuration, and `url(...)` stays a plain CSS import the browser resolves at runtime. Neither is a module Sass compiles. The workspace has no relative `@import` at all today, so the scan still finds the same 42 imports across 133 files; this closes the gap before someone writes one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
191 lines
6.2 KiB
JavaScript
191 lines
6.2 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
|
|
import {
|
|
extractRelativeImports,
|
|
resolveStylesheet,
|
|
stripScssComments,
|
|
validateScanCoverage,
|
|
validateStylesheetInputs,
|
|
} from './check-stylesheet-inputs.mjs';
|
|
|
|
/**
|
|
* Mirrors the real workspace shape: one app with a build target that depends on
|
|
* a UI library, plus a shared stylesheet directory the library imports by
|
|
* relative path.
|
|
*/
|
|
function graphWithSharedStyles({ stylesProjectDeclared }) {
|
|
const nodes = {
|
|
web: { data: { root: 'apps/web', targets: { build: {} } } },
|
|
components: { data: { root: 'libs/ui/components' } },
|
|
};
|
|
const dependencies = { web: [{ target: 'components' }], components: [] };
|
|
|
|
if (stylesProjectDeclared) {
|
|
nodes['ui-styles'] = { data: { root: 'libs/ui/styles' } };
|
|
dependencies['ui-styles'] = [];
|
|
dependencies.components = [{ target: 'ui-styles' }];
|
|
}
|
|
|
|
return { nodes, dependencies };
|
|
}
|
|
|
|
const sharedStyleImport = (targetProject) => ({
|
|
sourceFile: 'libs/ui/components/src/lib/a.component.scss',
|
|
specifier: '../../../../styles/detail-view',
|
|
targetFile: 'libs/ui/styles/_detail-view.scss',
|
|
sourceProject: 'components',
|
|
targetProject,
|
|
});
|
|
|
|
test('flags a shared stylesheet that belongs to no Nx project', () => {
|
|
const diagnostics = validateStylesheetInputs({
|
|
imports: [sharedStyleImport(null)],
|
|
graph: graphWithSharedStyles({ stylesProjectDeclared: false }),
|
|
});
|
|
|
|
assert.equal(diagnostics.length, 1);
|
|
assert.match(diagnostics[0], /belongs to no Nx project/);
|
|
assert.match(diagnostics[0], /libs\/ui\/styles\/_detail-view\.scss/);
|
|
});
|
|
|
|
test('flags a stylesheet project outside the consuming build closure', () => {
|
|
const graph = graphWithSharedStyles({ stylesProjectDeclared: true });
|
|
graph.dependencies.components = [];
|
|
|
|
const diagnostics = validateStylesheetInputs({
|
|
imports: [sharedStyleImport('ui-styles')],
|
|
graph,
|
|
});
|
|
|
|
assert.equal(diagnostics.length, 1);
|
|
assert.match(diagnostics[0], /web:build compiles/);
|
|
assert.match(diagnostics[0], /stale cache/);
|
|
assert.match(
|
|
diagnostics[0],
|
|
/"implicitDependencies": \["ui-styles"\] to the "components" project/
|
|
);
|
|
});
|
|
|
|
test('accepts a stylesheet project inside the consuming build closure', () => {
|
|
const diagnostics = validateStylesheetInputs({
|
|
imports: [sharedStyleImport('ui-styles')],
|
|
graph: graphWithSharedStyles({ stylesProjectDeclared: true }),
|
|
});
|
|
|
|
assert.deepEqual(diagnostics, []);
|
|
});
|
|
|
|
test('accepts a library importing a stylesheet owned by the build itself', () => {
|
|
const diagnostics = validateStylesheetInputs({
|
|
imports: [
|
|
{
|
|
sourceFile: 'libs/ui/components/src/lib/groups-view.scss',
|
|
specifier: '../../../../../../../apps/web/src/nav-list.scss',
|
|
targetFile: 'apps/web/src/nav-list.scss',
|
|
sourceProject: 'components',
|
|
targetProject: 'web',
|
|
},
|
|
],
|
|
graph: graphWithSharedStyles({ stylesProjectDeclared: true }),
|
|
});
|
|
|
|
assert.deepEqual(diagnostics, []);
|
|
});
|
|
|
|
test('flags a stylesheet whose own directory belongs to no Nx project', () => {
|
|
const diagnostics = validateStylesheetInputs({
|
|
imports: [
|
|
{
|
|
sourceFile: 'libs/ui/styles/_index.scss',
|
|
specifier: './portal-layout',
|
|
targetFile: 'libs/ui/styles/_portal-layout.scss',
|
|
sourceProject: null,
|
|
targetProject: null,
|
|
},
|
|
],
|
|
graph: graphWithSharedStyles({ stylesProjectDeclared: false }),
|
|
});
|
|
|
|
assert.equal(diagnostics.length, 1);
|
|
assert.match(diagnostics[0], /belongs to no Nx project/);
|
|
assert.match(diagnostics[0], /project\.json/);
|
|
});
|
|
|
|
test('fails instead of passing when the scan finds no stylesheets', () => {
|
|
const diagnostics = validateScanCoverage([]);
|
|
|
|
assert.equal(diagnostics.length, 1);
|
|
assert.match(diagnostics[0], /No stylesheets were scanned/);
|
|
});
|
|
|
|
test('reports no scan-coverage problem once stylesheets are found', () => {
|
|
assert.deepEqual(
|
|
validateScanCoverage(['libs/ui/styles/_detail-view.scss']),
|
|
[]
|
|
);
|
|
});
|
|
|
|
test('ignores relative @use examples written inside comments', () => {
|
|
const source = [
|
|
'// @use "../../../../../../ui/styles/portal-layout" as portal;',
|
|
'/* @use "../../nope/from-block-comment"; */',
|
|
"@use '../real/partial' as real;",
|
|
"@use 'sass:math';",
|
|
].join('\n');
|
|
|
|
assert.deepEqual(extractRelativeImports(source), ['../real/partial']);
|
|
});
|
|
|
|
test('collects every target of a comma-separated @import list', () => {
|
|
const source = "@import './local', '../../shared/theme', 'sass:math';";
|
|
|
|
assert.deepEqual(extractRelativeImports(source), [
|
|
'./local',
|
|
'../../shared/theme',
|
|
]);
|
|
});
|
|
|
|
test('treats a @use configuration value as a value, not a second import', () => {
|
|
const source = [
|
|
"@use '../../styles/theme' with ($font: 'Inter', $mode: './dark');",
|
|
"@forward '../../styles/panel-header' with ($gap: './nope');",
|
|
].join('\n');
|
|
|
|
assert.deepEqual(extractRelativeImports(source), [
|
|
'../../styles/theme',
|
|
'../../styles/panel-header',
|
|
]);
|
|
});
|
|
|
|
test('ignores a url() import the browser resolves at runtime', () => {
|
|
assert.deepEqual(extractRelativeImports('@import url("./plain.css");'), []);
|
|
});
|
|
|
|
test('keeps protocol slashes intact when stripping line comments', () => {
|
|
const stripped = stripScssComments(
|
|
"$font: url('https://example.test/f.woff2'); // trailing note"
|
|
);
|
|
|
|
assert.match(stripped, /https:\/\/example\.test/);
|
|
assert.doesNotMatch(stripped, /trailing note/);
|
|
});
|
|
|
|
test('resolves a specifier to its Sass partial file', () => {
|
|
const existing = new Set([
|
|
path.resolve('/repo/libs/ui/styles/_detail-view.scss'),
|
|
]);
|
|
|
|
const resolved = resolveStylesheet(
|
|
'/repo/libs/ui/components/src/a.scss',
|
|
'../../styles/detail-view',
|
|
(candidate) => existing.has(candidate)
|
|
);
|
|
|
|
assert.equal(
|
|
resolved,
|
|
path.resolve('/repo/libs/ui/styles/_detail-view.scss')
|
|
);
|
|
});
|