fix(cli): keep svelte component styles in the raw-app bundle (#10838)

* fix(cli): keep svelte component styles in the raw-app bundle

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: fold svelte style guard into the plugin test file

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: record the editor-parity constraint on the svelte css option

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(cli): pin esbuild's service cwd before any test file chdirs

esbuild's node API captures process.cwd() when its module is first
imported and spawns its service with that cwd on every (re)start.
createBundle stops the service after each bundle, so the cwd is reused
across the whole run.

Several test files chdir into a temp dir and delete it afterwards. The
first one to bundle therefore pinned the service to a directory that
stopped existing, and the next test to reach esbuild died with

  The service was stopped: ENOENT: no such file or directory,
  posix_spawn '.../@esbuild/linux-x64/bin/esbuild'

The binary is present; ENOENT is posix_spawn rejecting the missing cwd.

Which file tripped it depended on bun's readdir order, so renaming an
unrelated test file was enough to surface it. Importing esbuild from the
preload pins the service to a cwd that outlives the run, independent of
file ordering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G88YF3sZFnJZUvTLVjqhZc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
hugocasa
2026-08-26 20:24:34 +02:00
committed by GitHub
co-authored by Claude Opus 5 Ruben Fiszel
parent ffdf17ef8d
commit b8bf539c3f
3 changed files with 54 additions and 5 deletions
+8 -1
View File
@@ -191,7 +191,14 @@ function createSveltePlugin(appDir: string): any {
// Convert Svelte syntax to JavaScript
try {
const { js, warnings } = svelte.compile(source, { filename });
// The raw-app editor's in-browser bundler compiles with
// `css: "injected"`, so this must too, or the same app renders
// styled there and unstyled once the CLI builds it: Svelte's default
// ("external") hands the <style> back on a `css` field nothing emits.
const { js, warnings } = svelte.compile(source, {
filename,
css: "injected",
});
const contents = js.code + `//# sourceMappingURL=` + js.map.toUrl();
return { contents, warnings: warnings.map(convertMessage) };
} catch (e: any) {
@@ -1,8 +1,5 @@
/**
* `lib.svelte.ts` / `lib.svelte.js` modules are plain modules that may use
* runes. They need `svelte.compileModule`; without it esbuild happily bundles
* `$state(...)` as an ordinary call and the app dies at runtime with
* "ReferenceError: $state is not defined".
* The svelte esbuild plugin, driven through `createBundle`.
*/
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
@@ -59,6 +56,12 @@ afterAll(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
/**
* `lib.svelte.ts` / `lib.svelte.js` modules are plain modules that may use
* runes. They need `svelte.compileModule`; without it esbuild happily bundles
* `$state(...)` as an ordinary call and the app dies at runtime with
* "ReferenceError: $state is not defined".
*/
describe("svelte plugin: .svelte.ts modules", () => {
test("compiles runes in a TypeScript rune module and the bundle runs", async () => {
writeApp({
@@ -116,3 +119,36 @@ bump();
expect((globalThis as any).__counterResult).toBe(2);
});
});
/**
* Svelte's default `css: "external"` hands a component's <style> back on a
* field the plugin never emits, so the markup keeps its `svelte-<hash>` class
* while the rule matching it disappears no build error, just an app that
* renders unstyled from the CLI and styled in the editor.
*/
describe("svelte plugin: component styles", () => {
test("a <style> block reaches the bundle under the class its markup carries", async () => {
writeApp({
"Styled.svelte": `<main>
<h1>Hello</h1>
</main>
<style>
h1 {
font-size: 1.5rem;
}
</style>
`,
"styles_entry.ts": `import Styled from './Styled.svelte';
export default Styled;
`,
});
const js = await bundle("styles_entry.ts");
const scopeClass = js.match(/<h1 class="(svelte-[a-z0-9]+)"/)?.[1];
expect(scopeClass).toBeDefined();
expect(js).toContain(`h1.${scopeClass}`);
expect(js).toContain("font-size");
});
});
+6
View File
@@ -10,6 +10,12 @@
* bear the startup cost inside their per-test timeout window.
*/
// esbuild's node API pins the cwd it spawns its service with to process.cwd() at
// module import, and test files chdir into temp dirs they later delete. Import it
// here, from a cwd that outlives the run, or the first file to bundle pins the
// service to a directory that stops existing and every later build fails ENOENT.
import "esbuild";
if (process.env["UNIT_ONLY"]) {
// Nothing to do — unit tests don't need backend setup
} else {