Skip to content

Commit 4ad582a

Browse files
authored
Fix dist CSS containing unprocessed Tailwind directives and e2e test flakiness (#16)
The build's merge-simulator-css plugin was copying raw source CSS (with @import "tailwindcss") into dist/chatgpt/globals.css. Consumers couldn't resolve these directives, breaking sunpeak dev. Fixed by using the already-processed dist/style.css instead. Added validateDistCss() to the validate pipeline to catch unprocessed CSS directives before publishing. Replaced getPortSync TOCTOU port allocation in playwright configs with fixed preferred ports, eliminating ERR_CONNECTION_REFUSED flakiness in e2e tests caused by ephemeral ports being reclaimed between discovery and server bind.
1 parent 6972d27 commit 4ad582a

11 files changed

Lines changed: 65 additions & 139 deletions

File tree

examples/albums-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"clsx": "^2.1.1",
1616
"embla-carousel-react": "^8.6.0",
1717
"embla-carousel-wheel-gestures": "^8.1.0",
18-
"sunpeak": "^0.17.0",
18+
"sunpeak": "^0.17.2",
1919
"tailwind-merge": "^3.5.0",
2020
"zod": "^4.3.6"
2121
},

examples/albums-example/playwright.config.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,9 @@
11
import { defineConfig, devices } from '@playwright/test';
2-
import { execSync } from 'child_process';
32

4-
/**
5-
* Find an available port synchronously.
6-
* Spawns a tiny Node script that binds, prints the port, and exits.
7-
*/
8-
function getPortSync(preferred: number): number {
9-
const script = `
10-
const s = require("net").createServer();
11-
s.listen(${preferred}, () => {
12-
process.stdout.write(String(s.address().port));
13-
s.close();
14-
});
15-
s.on("error", () => {
16-
const f = require("net").createServer();
17-
f.listen(0, () => {
18-
process.stdout.write(String(f.address().port));
19-
f.close();
20-
});
21-
});
22-
`;
23-
return Number(execSync(`node -e '${script}'`, { encoding: 'utf-8' }).trim());
24-
}
25-
26-
const port = Number(process.env.SUNPEAK_TEST_PORT) || getPortSync(6776);
27-
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || getPortSync(24680);
3+
// Use fixed preferred ports. If in use, `reuseExistingServer` (local) reuses
4+
// the running server. In CI, validate.mjs assigns unique ports via env vars.
5+
const port = Number(process.env.SUNPEAK_TEST_PORT) || 6776;
6+
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || 24680;
287

298
export default defineConfig({
309
globalSetup: './tests/e2e/global-setup.ts',

examples/carousel-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"clsx": "^2.1.1",
1616
"embla-carousel-react": "^8.6.0",
1717
"embla-carousel-wheel-gestures": "^8.1.0",
18-
"sunpeak": "^0.17.0",
18+
"sunpeak": "^0.17.2",
1919
"tailwind-merge": "^3.5.0",
2020
"zod": "^4.3.6"
2121
},

examples/carousel-example/playwright.config.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,9 @@
11
import { defineConfig, devices } from '@playwright/test';
2-
import { execSync } from 'child_process';
32

4-
/**
5-
* Find an available port synchronously.
6-
* Spawns a tiny Node script that binds, prints the port, and exits.
7-
*/
8-
function getPortSync(preferred: number): number {
9-
const script = `
10-
const s = require("net").createServer();
11-
s.listen(${preferred}, () => {
12-
process.stdout.write(String(s.address().port));
13-
s.close();
14-
});
15-
s.on("error", () => {
16-
const f = require("net").createServer();
17-
f.listen(0, () => {
18-
process.stdout.write(String(f.address().port));
19-
f.close();
20-
});
21-
});
22-
`;
23-
return Number(execSync(`node -e '${script}'`, { encoding: 'utf-8' }).trim());
24-
}
25-
26-
const port = Number(process.env.SUNPEAK_TEST_PORT) || getPortSync(6776);
27-
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || getPortSync(24680);
3+
// Use fixed preferred ports. If in use, `reuseExistingServer` (local) reuses
4+
// the running server. In CI, validate.mjs assigns unique ports via env vars.
5+
const port = Number(process.env.SUNPEAK_TEST_PORT) || 6776;
6+
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || 24680;
287

298
export default defineConfig({
309
globalSetup: './tests/e2e/global-setup.ts',

examples/map-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"clsx": "^2.1.1",
1616
"embla-carousel-react": "^8.6.0",
1717
"mapbox-gl": "^3.20.0",
18-
"sunpeak": "^0.17.0",
18+
"sunpeak": "^0.17.2",
1919
"tailwind-merge": "^3.5.0",
2020
"zod": "^4.3.6"
2121
},

examples/map-example/playwright.config.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,9 @@
11
import { defineConfig, devices } from '@playwright/test';
2-
import { execSync } from 'child_process';
32

4-
/**
5-
* Find an available port synchronously.
6-
* Spawns a tiny Node script that binds, prints the port, and exits.
7-
*/
8-
function getPortSync(preferred: number): number {
9-
const script = `
10-
const s = require("net").createServer();
11-
s.listen(${preferred}, () => {
12-
process.stdout.write(String(s.address().port));
13-
s.close();
14-
});
15-
s.on("error", () => {
16-
const f = require("net").createServer();
17-
f.listen(0, () => {
18-
process.stdout.write(String(f.address().port));
19-
f.close();
20-
});
21-
});
22-
`;
23-
return Number(execSync(`node -e '${script}'`, { encoding: 'utf-8' }).trim());
24-
}
25-
26-
const port = Number(process.env.SUNPEAK_TEST_PORT) || getPortSync(6776);
27-
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || getPortSync(24680);
3+
// Use fixed preferred ports. If in use, `reuseExistingServer` (local) reuses
4+
// the running server. In CI, validate.mjs assigns unique ports via env vars.
5+
const port = Number(process.env.SUNPEAK_TEST_PORT) || 6776;
6+
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || 24680;
287

298
export default defineConfig({
309
globalSetup: './tests/e2e/global-setup.ts',

examples/review-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"dependencies": {
1515
"clsx": "^2.1.1",
16-
"sunpeak": "^0.17.0",
16+
"sunpeak": "^0.17.2",
1717
"tailwind-merge": "^3.5.0",
1818
"zod": "^4.3.6"
1919
},

examples/review-example/playwright.config.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,9 @@
11
import { defineConfig, devices } from '@playwright/test';
2-
import { execSync } from 'child_process';
32

4-
/**
5-
* Find an available port synchronously.
6-
* Spawns a tiny Node script that binds, prints the port, and exits.
7-
*/
8-
function getPortSync(preferred: number): number {
9-
const script = `
10-
const s = require("net").createServer();
11-
s.listen(${preferred}, () => {
12-
process.stdout.write(String(s.address().port));
13-
s.close();
14-
});
15-
s.on("error", () => {
16-
const f = require("net").createServer();
17-
f.listen(0, () => {
18-
process.stdout.write(String(f.address().port));
19-
f.close();
20-
});
21-
});
22-
`;
23-
return Number(execSync(`node -e '${script}'`, { encoding: 'utf-8' }).trim());
24-
}
25-
26-
const port = Number(process.env.SUNPEAK_TEST_PORT) || getPortSync(6776);
27-
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || getPortSync(24680);
3+
// Use fixed preferred ports. If in use, `reuseExistingServer` (local) reuses
4+
// the running server. In CI, validate.mjs assigns unique ports via env vars.
5+
const port = Number(process.env.SUNPEAK_TEST_PORT) || 6776;
6+
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || 24680;
287

298
export default defineConfig({
309
globalSetup: './tests/e2e/global-setup.ts',

packages/sunpeak/scripts/validate.mjs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,41 @@ function validateExportsMap() {
142142
printSuccess(`Exports map verified (${exportKeys.length} entry points)`);
143143
}
144144

145+
/**
146+
* Verify dist CSS files don't contain unprocessed Tailwind/PostCSS directives
147+
* that consumers can't resolve (e.g. @import "tailwindcss", @source, @custom-variant).
148+
*/
149+
function validateDistCss() {
150+
console.log('Validating dist CSS files...');
151+
152+
const forbiddenPatterns = [
153+
{ regex: /@import\s+["']tailwindcss["']/, label: '@import "tailwindcss"' },
154+
{ regex: /@source\s+/, label: '@source' },
155+
{ regex: /@custom-variant\s+/, label: '@custom-variant' },
156+
{ regex: /@utility\s+/, label: '@utility' },
157+
];
158+
159+
const cssFiles = ['dist/style.css', 'dist/chatgpt/globals.css'];
160+
const errors = [];
161+
162+
for (const file of cssFiles) {
163+
const fullPath = join(PACKAGE_ROOT, file);
164+
if (!existsSync(fullPath)) continue;
165+
const content = readFileSync(fullPath, 'utf-8');
166+
for (const { regex, label } of forbiddenPatterns) {
167+
if (regex.test(content)) {
168+
errors.push(` ${file} contains unprocessed ${label}`);
169+
}
170+
}
171+
}
172+
173+
if (errors.length > 0) {
174+
throw new Error(`Dist CSS contains unprocessed directives:\n${errors.join('\n')}`);
175+
}
176+
177+
printSuccess(`Dist CSS files verified (no unprocessed directives)`);
178+
}
179+
145180
/**
146181
* Verify every page referenced in docs.json has a corresponding .mdx file.
147182
* Also checks that every exported hook has a docs page.
@@ -787,6 +822,7 @@ try {
787822
printSuccess('pnpm build');
788823

789824
validateExportsMap();
825+
validateDistCss();
790826

791827
console.log('\nRunning: pnpm typecheck');
792828
if (!runCommand('pnpm typecheck', PACKAGE_ROOT)) throw new Error('pnpm typecheck failed');

packages/sunpeak/template/playwright.config.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,9 @@
11
import { defineConfig, devices } from '@playwright/test';
2-
import { execSync } from 'child_process';
32

4-
/**
5-
* Find an available port synchronously.
6-
* Spawns a tiny Node script that binds, prints the port, and exits.
7-
*/
8-
function getPortSync(preferred: number): number {
9-
const script = `
10-
const s = require("net").createServer();
11-
s.listen(${preferred}, () => {
12-
process.stdout.write(String(s.address().port));
13-
s.close();
14-
});
15-
s.on("error", () => {
16-
const f = require("net").createServer();
17-
f.listen(0, () => {
18-
process.stdout.write(String(f.address().port));
19-
f.close();
20-
});
21-
});
22-
`;
23-
return Number(execSync(`node -e '${script}'`, { encoding: 'utf-8' }).trim());
24-
}
25-
26-
const port = Number(process.env.SUNPEAK_TEST_PORT) || getPortSync(6776);
27-
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || getPortSync(24680);
3+
// Use fixed preferred ports. If in use, `reuseExistingServer` (local) reuses
4+
// the running server. In CI, validate.mjs assigns unique ports via env vars.
5+
const port = Number(process.env.SUNPEAK_TEST_PORT) || 6776;
6+
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || 24680;
287

298
export default defineConfig({
309
globalSetup: './tests/e2e/global-setup.ts',

0 commit comments

Comments
 (0)