Skip to content

Commit 3ecf3c3

Browse files
makhnatkinclaude
andcommitted
test(shadow-styles): enforce import list sync + soft-fail optional peers
Add a regression script that diffs SHADOW_STYLE_IMPORTS against non-relative *.css imports under packages/editor/src/**, so a future extension addition cannot silently leave shadow-styles cssText stale behind a green CI. Wired as a separate ci:test:shadow-styles job mirroring the circular-deps check. Wrap require.resolve() for shadow-styles externals in a try/catch: optional peer packages (per peerDependenciesMeta) are skipped with a warning instead of crashing the build for consumers that don't install them. Required peers still throw. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 429c972 commit 3ecf3c3

5 files changed

Lines changed: 125 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,25 @@ jobs:
7777

7878
- name: Check circular dependencies
7979
run: pnpm ci:test:circular-deps
80+
81+
check_shadow_styles_imports:
82+
name: Check Shadow Styles Imports
83+
runs-on: ubuntu-latest
84+
steps:
85+
- name: Checkout
86+
uses: actions/checkout@v6
87+
88+
- name: Setup pnpm
89+
uses: pnpm/action-setup@v5
90+
91+
- name: Setup Node
92+
uses: actions/setup-node@v6
93+
with:
94+
node-version-file: '.nvmrc'
95+
cache: pnpm
96+
97+
- name: Install dependencies
98+
run: pnpm run ci:deps
99+
100+
- name: Check shadow styles imports
101+
run: pnpm ci:test:shadow-styles

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525
"ci:test:unit": "nx run-many -t test --verbose",
2626
"ci:test:esbuild": "nx run-many -t test:esbuild --verbose",
2727
"ci:test:circular-deps": "nx run-many -t test:circular-deps --verbose",
28+
"ci:test:shadow-styles": "nx run-many -t test:shadow-styles --verbose",
2829
"start": "nx sb:start @markdown-editor/demo",
2930
"clean": "nx run-many -t clean",
3031
"build": "nx run-many -t build",
3132
"typecheck": "nx run-many -t typecheck",
32-
"test": "nx run-many -t test,test:esbuild,test:circular-deps",
33+
"test": "nx run-many -t test,test:esbuild,test:circular-deps,test:shadow-styles",
3334
"test:e2e": "nx playwright:docker @markdown-editor/demo",
3435
"test:e2e:report": "nx playwright:docker:report @markdown-editor/demo",
3536
"lint": "run-p -cs lint:*",

packages/editor/gulpfile.mjs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const require = createRequire(import.meta.url);
1414
const BUILD_DIR = resolve('build');
1515
const NODE_MODULES_DIR = resolve(__dirname, 'node_modules');
1616
// Keep this list aligned with non-relative CSS imports required by the default editor setup.
17-
const SHADOW_STYLE_IMPORTS = Object.freeze([
17+
// Drift is enforced by scripts/check-shadow-styles-imports.js.
18+
export const SHADOW_STYLE_IMPORTS = Object.freeze([
1819
'@diplodoc/transform/dist/css/base.css',
1920
'@diplodoc/transform/dist/css/_yfm-only.css',
2021
'@diplodoc/cut-extension/runtime/styles.css',
@@ -24,6 +25,12 @@ const SHADOW_STYLE_IMPORTS = Object.freeze([
2425
'@diplodoc/folding-headings-extension/runtime/styles.css',
2526
]);
2627

28+
const OPTIONAL_PEERS = new Set(
29+
Object.entries(pkg.peerDependenciesMeta ?? {})
30+
.filter(([, meta]) => meta?.optional)
31+
.map(([name]) => name),
32+
);
33+
2734
registerBuildTasks({
2835
version: pkg.version,
2936
buildDir: BUILD_DIR,
@@ -53,9 +60,28 @@ task('build', series(parallel('ts', 'json', 'scss'), 'shadow-styles'));
5360
task('default', series('clean', 'build'));
5461

5562
function readExternalShadowStyles() {
56-
return SHADOW_STYLE_IMPORTS.map((cssImport) =>
57-
readFileSync(require.resolve(cssImport), 'utf8'),
58-
).join('\n');
63+
return SHADOW_STYLE_IMPORTS.map((cssImport) => {
64+
try {
65+
return readFileSync(require.resolve(cssImport), 'utf8');
66+
} catch (err) {
67+
if (err?.code === 'MODULE_NOT_FOUND' && isOptionalPeerImport(cssImport)) {
68+
console.warn(
69+
`[shadow-styles] Skipping optional peer CSS '${cssImport}' (package not installed).`,
70+
);
71+
return '';
72+
}
73+
throw err;
74+
}
75+
})
76+
.filter(Boolean)
77+
.join('\n');
78+
}
79+
80+
function isOptionalPeerImport(cssImport) {
81+
const pkgName = cssImport.startsWith('@')
82+
? cssImport.split('/', 2).join('/')
83+
: cssImport.split('/', 1)[0];
84+
return OPTIONAL_PEERS.has(pkgName);
5985
}
6086

6187
function createShadowStylesModule(value) {

packages/editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"test:watch": "jest --watchAll",
2828
"test:esbuild": "node tests/esbuild-test/esbuild-tester.js",
2929
"test:circular-deps": "node scripts/check-circular-deps.js 0",
30+
"test:shadow-styles": "node scripts/check-shadow-styles-imports.js",
3031
"prepack": "cp ../../README.md ./README.md",
3132
"postpack": "rm -f ./README.md",
3233
"prepublishOnly": "pnpm run lint && pnpm run clean && pnpm run build"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* eslint-disable no-console, no-undef */
2+
const fs = require('node:fs');
3+
const path = require('node:path');
4+
const {pathToFileURL} = require('node:url');
5+
6+
const SRC_DIR = path.resolve(__dirname, '..', 'src');
7+
const GULPFILE_URL = pathToFileURL(path.resolve(__dirname, '..', 'gulpfile.mjs')).href;
8+
9+
// Bare `import 'pkg/path/file.css';` — non-relative, scoped or unscoped, ending in .css.
10+
const CSS_IMPORT_RE = /(?:^|\s)import\s+['"]((?:@[^'"\s/]+\/)?[^'"\s.][^'"\s]*\.css)['"]/gm;
11+
12+
const EXCLUDED_SCOPES = ['@gravity-ui/'];
13+
14+
async function main() {
15+
const {SHADOW_STYLE_IMPORTS} = await import(GULPFILE_URL);
16+
if (!Array.isArray(SHADOW_STYLE_IMPORTS)) {
17+
console.error('Failed to import SHADOW_STYLE_IMPORTS from gulpfile.mjs');
18+
process.exit(1);
19+
}
20+
21+
const declared = new Set(SHADOW_STYLE_IMPORTS);
22+
const actual = collectCssImports(SRC_DIR);
23+
24+
const missing = [...actual].filter((x) => !declared.has(x)).sort();
25+
const stale = [...declared].filter((x) => !actual.has(x)).sort();
26+
27+
if (missing.length || stale.length) {
28+
console.error('Shadow styles imports drift detected:');
29+
if (missing.length) {
30+
console.error(' Missing in SHADOW_STYLE_IMPORTS (found in src, not in list):');
31+
for (const item of missing) console.error(` + ${item}`);
32+
}
33+
if (stale.length) {
34+
console.error(' Stale in SHADOW_STYLE_IMPORTS (in list, not used in src):');
35+
for (const item of stale) console.error(` - ${item}`);
36+
}
37+
console.error('Update SHADOW_STYLE_IMPORTS in packages/editor/gulpfile.mjs.');
38+
process.exit(1);
39+
}
40+
41+
console.log(`Shadow styles imports check passed (count: ${declared.size})`);
42+
process.exit(0);
43+
}
44+
45+
function collectCssImports(dir) {
46+
const result = new Set();
47+
walk(dir, (filePath) => {
48+
if (!/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(filePath)) return;
49+
const content = fs.readFileSync(filePath, 'utf8');
50+
for (const match of content.matchAll(CSS_IMPORT_RE)) {
51+
const spec = match[1];
52+
if (EXCLUDED_SCOPES.some((scope) => spec.startsWith(scope))) continue;
53+
result.add(spec);
54+
}
55+
});
56+
return result;
57+
}
58+
59+
function walk(dir, visit) {
60+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
61+
const full = path.join(dir, entry.name);
62+
if (entry.isDirectory()) walk(full, visit);
63+
else if (entry.isFile()) visit(full);
64+
}
65+
}
66+
67+
main().catch((err) => {
68+
console.error(err);
69+
process.exit(1);
70+
});

0 commit comments

Comments
 (0)