-
Notifications
You must be signed in to change notification settings - Fork 234
Expand file tree
/
Copy pathbuild-theme.prose.test.mjs
More file actions
141 lines (127 loc) · 5.34 KB
/
Copy pathbuild-theme.prose.test.mjs
File metadata and controls
141 lines (127 loc) · 5.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Copyright (c) Meta Platforms, Inc. and affiliates.
/**
* @file Regression test for prose output of `astryx theme build`.
*
* `astryx theme build` has a single CSS-generation path — @astryxdesign/core's generator,
* the same one the <Theme> runtime uses (generateThemeCSS). It always emits
* prose element defaults (h1–h6, p, small, code, hr) so unstyled HTML inherits
* the theme's typography, exactly like the runtime. There is intentionally no
* way to omit them (the old `--no-prose` flag was removed: it let the build
* emit something the runtime never would, breaking the build⇄runtime parity).
*
* Covers:
* - prose defaults ship in `@layer reset` (zero-specificity :where()), NOT
* `@layer astryx-theme`, so component/Markdown StyleX always wins;
* - no raw element margins are emitted (reset.css zeroes them and the
* components own block spacing — see the docsite Markdown regression);
* - paragraphs use the body font, not the heading font.
*
* Building `astryx theme build` requires a compiled @astryxdesign/core (there is no in-CLI
* fallback generator), so this suite builds core once in beforeAll — mirroring
* scripts/build-css.test.mjs — to stay self-sufficient regardless of CI job
* ordering.
*/
import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
import {execFileSync} from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import {fileURLToPath} from 'node:url';
import {withRepoBuildLock} from '../../../../scripts/repo-build-lock.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CLI_BIN = path.resolve(__dirname, '../../bin/astryx.mjs');
const REPO_ROOT = path.resolve(__dirname, '../../../..');
const CORE_THEME_ENTRY = path.join(
REPO_ROOT,
'packages/core/dist/theme/index.js',
);
function runCli(args, cwd) {
try {
const out = execFileSync('node', [CLI_BIN, ...args], {
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
env: {...process.env, FORCE_COLOR: '0'},
});
return {code: 0, stdout: out, stderr: ''};
} catch (e) {
return {
code: e.status ?? 1,
stdout: e.stdout?.toString() ?? '',
stderr: e.stderr?.toString() ?? '',
};
}
}
function writeTheme(dir, name) {
fs.mkdirSync(dir, {recursive: true});
// The CLI writes <basename>.css next to the source file, so use the
// theme name as the filename for unambiguous fixtures.
const file = path.join(dir, `${name}.mjs`);
fs.writeFileSync(
file,
`export default { name: ${JSON.stringify(name)}, tokens: { '--color-bg': '#fff' } };\n`,
);
return file;
}
// `astryx theme build` imports the compiled @astryxdesign/core/theme entry. Build core
// once if it isn't already present so the suite works in any CI job. The build runs
// under the repo build lock and re-checks inside it: other test files also build
// into packages/core/dist from parallel workers, and unsynchronized builds race
// each other's rimraf/output (#3479).
beforeAll(() => {
withRepoBuildLock(() => {
if (!fs.existsSync(CORE_THEME_ENTRY)) {
execFileSync('pnpm', ['-F', '@astryxdesign/core', 'build'], {
cwd: REPO_ROOT,
stdio: 'pipe',
timeout: 180_000,
});
}
});
}, 500_000);
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-prose-'));
});
afterEach(() => {
fs.rmSync(tmpDir, {recursive: true, force: true});
});
describe('theme build prose output', () => {
it('always emits prose mappings in @layer reset', () => {
const project = path.join(tmpDir, 'project');
const themesDir = path.join(project, 'themes');
const themeFile = writeTheme(themesDir, 'with-prose');
const result = runCli(
['theme', 'build', path.relative(project, themeFile)],
project,
);
expect(result.code).toBe(0);
const cssPath = path.join(themesDir, 'with-prose.css');
expect(fs.existsSync(cssPath)).toBe(true);
const css = fs.readFileSync(cssPath, 'utf-8');
// Prose defaults are zero-specificity :where() rules in @layer reset,
// NOT @layer astryx-theme, so Markdown/component StyleX always wins.
expect(css).toMatch(/@layer reset/);
expect(css).toMatch(/:where\(h1, h2, h3, h4, h5, h6\)/);
// Paragraphs use the body font (regression: they used the heading font).
expect(css).toMatch(/:where\(p\)[^}]*font-family: var\(--font-family-body\)/);
// No raw element margins — reset.css + component StyleX own spacing.
const proseBlock = css.slice(
css.indexOf('@layer reset'),
css.indexOf('@layer astryx-theme'),
);
expect(proseBlock).not.toMatch(/margin/);
// Layer placement: prose (reset) must come before component overrides
// (astryx-theme) so the cascade resolves correctly.
const resetIndex = css.indexOf('@layer reset');
const themeIndex = css.indexOf('@layer astryx-theme');
expect(resetIndex).toBeGreaterThanOrEqual(0);
expect(themeIndex).toBeGreaterThan(resetIndex);
expect(css.indexOf(':where(p)')).toBeLessThan(themeIndex);
});
it('has no --no-prose flag (prose is non-optional)', () => {
const result = runCli(['theme', 'build', '--help'], process.cwd());
// The flag was removed; build always emits prose to match the runtime.
expect(result.stdout + result.stderr).not.toContain('--no-prose');
});
});