mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
* 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>
155 lines
4.2 KiB
TypeScript
155 lines
4.2 KiB
TypeScript
/**
|
|
* The svelte esbuild plugin, driven through `createBundle`.
|
|
*/
|
|
|
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import * as os from "node:os";
|
|
import { createBundle } from "../src/commands/app/bundle.ts";
|
|
|
|
let tempDir: string;
|
|
let originalCwd: string;
|
|
|
|
// Runes used as bare calls, ignoring the `$state(...)` mentions inside Svelte's
|
|
// own warning message template literals.
|
|
function bareRuneCalls(js: string): string[] {
|
|
return [...js.matchAll(/(^|[^.\w$`])\$(state|derived|effect|props)\s*\(/g)].map(
|
|
(m) => m[0]
|
|
);
|
|
}
|
|
|
|
function writeApp(files: Record<string, string>) {
|
|
for (const [name, content] of Object.entries(files)) {
|
|
fs.writeFileSync(path.join(tempDir, name), content, "utf-8");
|
|
}
|
|
}
|
|
|
|
async function bundle(entry: string): Promise<string> {
|
|
const { js } = await createBundle({
|
|
entryPoint: path.join(tempDir, entry),
|
|
minify: false,
|
|
sourcemap: false,
|
|
});
|
|
return js;
|
|
}
|
|
|
|
beforeAll(() => {
|
|
originalCwd = process.cwd();
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "raw-app-svelte-module-"));
|
|
fs.writeFileSync(
|
|
path.join(tempDir, "package.json"),
|
|
JSON.stringify({ name: "app", private: true, dependencies: { svelte: "*" } }),
|
|
"utf-8"
|
|
);
|
|
// Reuse the CLI's own svelte install instead of paying for an npm install;
|
|
// `ensureNodeModules` only checks that the directory is there.
|
|
fs.symlinkSync(
|
|
path.join(originalCwd, "node_modules"),
|
|
path.join(tempDir, "node_modules")
|
|
);
|
|
process.chdir(tempDir);
|
|
});
|
|
|
|
afterAll(() => {
|
|
process.chdir(originalCwd);
|
|
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({
|
|
"lib.svelte.ts": `export class Cycle<T> {
|
|
#options: T[] = [];
|
|
#index = $state(0);
|
|
current = $derived(this.#options[this.#index]);
|
|
|
|
constructor(options: T[]) {
|
|
this.#options = options;
|
|
}
|
|
|
|
next() {
|
|
this.#index = (this.#index + 1) % this.#options.length;
|
|
}
|
|
}
|
|
`,
|
|
"ts_entry.ts": `import { Cycle } from './lib.svelte';
|
|
|
|
const cycle = new Cycle(['a', 'b', 'c']);
|
|
cycle.next();
|
|
(globalThis as any).__cycleResult = cycle.current;
|
|
`,
|
|
});
|
|
|
|
const js = await bundle("ts_entry.ts");
|
|
|
|
expect(bareRuneCalls(js)).toEqual([]);
|
|
// Runtime is the real check: unfixed, this throws
|
|
// "ReferenceError: $state is not defined".
|
|
new Function(js)();
|
|
expect((globalThis as any).__cycleResult).toBe("b");
|
|
});
|
|
|
|
test("compiles runes in a JavaScript rune module", async () => {
|
|
writeApp({
|
|
"counter.svelte.js": `export const counter = $state({ n: 0 });
|
|
|
|
export function bump() {
|
|
counter.n += 1;
|
|
}
|
|
`,
|
|
"js_entry.ts": `import { counter, bump } from './counter.svelte.js';
|
|
|
|
bump();
|
|
bump();
|
|
(globalThis as any).__counterResult = counter.n;
|
|
`,
|
|
});
|
|
|
|
const js = await bundle("js_entry.ts");
|
|
|
|
expect(bareRuneCalls(js)).toEqual([]);
|
|
new Function(js)();
|
|
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");
|
|
});
|
|
});
|