wgsl-edit 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -9
- package/dist/SaveEndpoint.d.mts +5 -0
- package/dist/SaveEndpoint.mjs +5 -0
- package/dist/SaveMiddleware.d.mts +9 -0
- package/dist/SaveMiddleware.mjs +49 -0
- package/dist/{WgslEdit-ByXfb3R9.js → WgslEdit-CNK80480.js} +252 -118
- package/dist/WgslEdit.d.ts +45 -1
- package/dist/WgslEdit.js +1 -1
- package/dist/autosave.d.mts +11 -0
- package/dist/autosave.mjs +18 -0
- package/dist/index.js +1 -1
- package/dist/jsx-preact.d.ts +12 -0
- package/dist/jsx-preact.js +0 -0
- package/dist/wgsl-edit.js +291 -501
- package/package.json +24 -6
- package/src/SaveEndpoint.ts +2 -0
- package/src/SaveMiddleware.ts +71 -0
- package/src/WgslEdit.ts +288 -157
- package/src/autosave.ts +24 -0
- package/src/jsx-preact.ts +15 -0
- package/src/test/Autosave.e2e.ts +100 -0
- package/src/test/E2eUtil.ts +6 -0
- package/src/test/Undo.e2e.ts +91 -0
- package/src/test/WgslEdit.e2e.ts +1 -4
package/src/autosave.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { pendingSaves, weslSaveMiddleware } from "./SaveMiddleware.ts";
|
|
3
|
+
|
|
4
|
+
export interface WgslEditAutosaveOptions {
|
|
5
|
+
/** Disable the save endpoint without removing the plugin (default: enabled). */
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Vite dev plugin: lets <wgsl-edit autosave> persist edits to disk via the save endpoint. */
|
|
10
|
+
export default function wgslEditAutosave(
|
|
11
|
+
options?: WgslEditAutosaveOptions,
|
|
12
|
+
): Plugin {
|
|
13
|
+
return {
|
|
14
|
+
name: "wgsl-edit-autosave",
|
|
15
|
+
apply: "serve",
|
|
16
|
+
configureServer(server) {
|
|
17
|
+
if (options?.disabled) return;
|
|
18
|
+
server.middlewares.use(weslSaveMiddleware(server.config.root));
|
|
19
|
+
},
|
|
20
|
+
hotUpdate({ file }) {
|
|
21
|
+
if (pendingSaves.delete(file)) return [];
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Preact JSX augmentation for `<wgsl-edit>`.
|
|
2
|
+
* Side-effect import: `import "wgsl-edit/jsx-preact"` once anywhere in your TS source. */
|
|
3
|
+
|
|
4
|
+
import type { HTMLAttributes } from "preact";
|
|
5
|
+
import type { WgslEdit, WgslEditAttrs } from "./WgslEdit.ts";
|
|
6
|
+
|
|
7
|
+
type WgslEditTag = HTMLAttributes<WgslEdit> & WgslEditAttrs;
|
|
8
|
+
|
|
9
|
+
declare module "preact" {
|
|
10
|
+
namespace JSX {
|
|
11
|
+
interface IntrinsicElements {
|
|
12
|
+
"wgsl-edit": WgslEditTag;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { expect, test } from "@playwright/test";
|
|
4
|
+
import { saveEndpoint } from "../SaveEndpoint.ts";
|
|
5
|
+
import { waitForWgslEdit } from "./E2eUtil.ts";
|
|
6
|
+
|
|
7
|
+
// Tests share filesystem state, so run serially
|
|
8
|
+
test.describe.configure({ mode: "serial" });
|
|
9
|
+
|
|
10
|
+
const shadersDir = path.resolve(import.meta.dirname, "../../test-page/shaders");
|
|
11
|
+
const shaderPath = path.join(shadersDir, "save-test.wesl");
|
|
12
|
+
const autoPath = path.join(shadersDir, "auto-test.wesl");
|
|
13
|
+
|
|
14
|
+
const originalContent = `fn original() -> f32 {
|
|
15
|
+
return 1.0;
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const autoContent = `fn auto_original() -> f32 {
|
|
20
|
+
return 1.0;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
test.beforeEach(() => {
|
|
25
|
+
fs.writeFileSync(shaderPath, originalContent);
|
|
26
|
+
fs.writeFileSync(autoPath, autoContent);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.afterEach(() => {
|
|
30
|
+
fs.writeFileSync(shaderPath, originalContent);
|
|
31
|
+
fs.writeFileSync(autoPath, autoContent);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("autosave writes edits to disk", async ({ page }) => {
|
|
35
|
+
await page.goto("/");
|
|
36
|
+
await waitForWgslEdit(page);
|
|
37
|
+
|
|
38
|
+
// type into the editor's CodeMirror instance inside shadow DOM
|
|
39
|
+
await page.evaluate(() => {
|
|
40
|
+
const el = document.querySelector("#editor4") as any;
|
|
41
|
+
el.source = `fn edited() -> f32 {\n return 2.0;\n}\n`;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// wait for debounce (500ms) + network round-trip
|
|
45
|
+
await expect
|
|
46
|
+
.poll(() => fs.readFileSync(shaderPath, "utf-8"), { timeout: 5000 })
|
|
47
|
+
.toContain("fn edited()");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("autosave does not write on tab switch", async ({ page }) => {
|
|
51
|
+
await page.goto("/");
|
|
52
|
+
await waitForWgslEdit(page);
|
|
53
|
+
|
|
54
|
+
// add a second file, which triggers a tab switch
|
|
55
|
+
await page.evaluate(() => {
|
|
56
|
+
const el = document.querySelector("#editor4") as any;
|
|
57
|
+
el.addFile("other.wesl", "fn other() {}");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// wait past the debounce window
|
|
61
|
+
await page.waitForTimeout(1000);
|
|
62
|
+
|
|
63
|
+
const content = fs.readFileSync(shaderPath, "utf-8");
|
|
64
|
+
expect(content).toBe(originalContent);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("autosave POST returns 403 for path traversal", async ({ page }) => {
|
|
68
|
+
await page.goto("/");
|
|
69
|
+
await waitForWgslEdit(page);
|
|
70
|
+
|
|
71
|
+
const status = await page.evaluate(async (endpoint: string) => {
|
|
72
|
+
const body = JSON.stringify({
|
|
73
|
+
root: "../../../",
|
|
74
|
+
file: "etc/evil.wesl",
|
|
75
|
+
content: "pwned",
|
|
76
|
+
});
|
|
77
|
+
const headers = { "Content-Type": "application/json" };
|
|
78
|
+
const res = await fetch(endpoint, { method: "POST", headers, body });
|
|
79
|
+
return res.status;
|
|
80
|
+
}, saveEndpoint);
|
|
81
|
+
|
|
82
|
+
expect(status).toBe(403);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("autosave saves when shaderRoot set via project property", async ({
|
|
86
|
+
page,
|
|
87
|
+
}) => {
|
|
88
|
+
await page.goto("/");
|
|
89
|
+
await waitForWgslEdit(page);
|
|
90
|
+
|
|
91
|
+
// editor5 has autosave attr; shaderRoot is set via project property in main.ts
|
|
92
|
+
await page.evaluate(() => {
|
|
93
|
+
const el = document.querySelector("#editor5") as any;
|
|
94
|
+
el.source = `fn auto_edited() -> f32 {\n return 2.0;\n}\n`;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await expect
|
|
98
|
+
.poll(() => fs.readFileSync(autoPath, "utf-8"), { timeout: 5000 })
|
|
99
|
+
.toContain("fn auto_edited()");
|
|
100
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { expect, type Page, test } from "@playwright/test";
|
|
2
|
+
import { waitForWgslEdit } from "./E2eUtil.ts";
|
|
3
|
+
|
|
4
|
+
async function appendToActiveFile(
|
|
5
|
+
page: Page,
|
|
6
|
+
id: string,
|
|
7
|
+
text: string,
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
await page.evaluate(
|
|
10
|
+
([sel, s]) => {
|
|
11
|
+
const view = (document.querySelector(sel) as any).editorView;
|
|
12
|
+
const end = view.state.doc.length;
|
|
13
|
+
view.dispatch({
|
|
14
|
+
changes: { from: end, insert: s },
|
|
15
|
+
selection: { anchor: end + s.length },
|
|
16
|
+
userEvent: "input.type",
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
[id, text] as const,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getFileSource(
|
|
24
|
+
page: Page,
|
|
25
|
+
id: string,
|
|
26
|
+
file: string,
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
return page.evaluate(
|
|
29
|
+
([sel, f]) => {
|
|
30
|
+
const el = document.querySelector(sel) as any;
|
|
31
|
+
const saved = el.activeFile;
|
|
32
|
+
el.activeFile = f;
|
|
33
|
+
const src = el.source as string;
|
|
34
|
+
el.activeFile = saved;
|
|
35
|
+
return src;
|
|
36
|
+
},
|
|
37
|
+
[id, file] as const,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function setActiveFile(
|
|
42
|
+
page: Page,
|
|
43
|
+
id: string,
|
|
44
|
+
file: string,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
await page.evaluate(
|
|
47
|
+
([sel, f]) => {
|
|
48
|
+
(document.querySelector(sel) as any).activeFile = f;
|
|
49
|
+
},
|
|
50
|
+
[id, file] as const,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function focusEditor(page: Page, id: string): Promise<void> {
|
|
55
|
+
await page.evaluate(sel => {
|
|
56
|
+
const host = document.querySelector(sel) as HTMLElement | null;
|
|
57
|
+
const content = host?.shadowRoot?.querySelector(
|
|
58
|
+
".cm-content",
|
|
59
|
+
) as HTMLElement | null;
|
|
60
|
+
content?.focus();
|
|
61
|
+
}, id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test("undo is scoped to the active file", async ({ page }) => {
|
|
65
|
+
await page.goto("/");
|
|
66
|
+
await waitForWgslEdit(page);
|
|
67
|
+
|
|
68
|
+
const mainOrig = await getFileSource(page, "#editor6", "main.wesl");
|
|
69
|
+
const shapeOrig = await getFileSource(page, "#editor6", "shape.wesl");
|
|
70
|
+
|
|
71
|
+
await setActiveFile(page, "#editor6", "main.wesl");
|
|
72
|
+
await appendToActiveFile(page, "#editor6", " // main-edit");
|
|
73
|
+
|
|
74
|
+
await setActiveFile(page, "#editor6", "shape.wesl");
|
|
75
|
+
await appendToActiveFile(page, "#editor6", " // shape-edit");
|
|
76
|
+
|
|
77
|
+
await setActiveFile(page, "#editor6", "main.wesl");
|
|
78
|
+
await focusEditor(page, "#editor6");
|
|
79
|
+
await page.keyboard.press("ControlOrMeta+z");
|
|
80
|
+
|
|
81
|
+
const active = await page.evaluate(
|
|
82
|
+
() => (document.querySelector("#editor6") as any).activeFile,
|
|
83
|
+
);
|
|
84
|
+
expect(active).toBe("main.wesl");
|
|
85
|
+
|
|
86
|
+
const mainNow = await getFileSource(page, "#editor6", "main.wesl");
|
|
87
|
+
const shapeNow = await getFileSource(page, "#editor6", "shape.wesl");
|
|
88
|
+
expect(mainNow).toBe(mainOrig);
|
|
89
|
+
expect(shapeNow).toContain("// shape-edit");
|
|
90
|
+
expect(shapeNow).not.toBe(shapeOrig);
|
|
91
|
+
});
|
package/src/test/WgslEdit.e2e.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { expect, type Page, test } from "@playwright/test";
|
|
2
|
-
|
|
3
|
-
async function waitForWgslEdit(page: Page) {
|
|
4
|
-
await page.waitForFunction(() => customElements.get("wgsl-edit"));
|
|
5
|
-
}
|
|
2
|
+
import { waitForWgslEdit } from "./E2eUtil.ts";
|
|
6
3
|
|
|
7
4
|
const lintErrorSelector = ".cm-lintRange-error";
|
|
8
5
|
|