Update project tooling
This commit is contained in:
parent
f0fb4fc86b
commit
3c21291d72
14 changed files with 1469 additions and 198 deletions
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"enabledPlugins": {
|
|
||||||
"frontend-design@claude-plugins-official": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
name: Deploy to Pages
|
name: Check & deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
@ -25,20 +25,38 @@ jobs:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: |
|
||||||
|
npm ci
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Lint
|
- name: Test
|
||||||
run: npm run lint -- --check || true
|
run: |
|
||||||
|
npm run lint:check
|
||||||
|
npm run format:check
|
||||||
|
npm run typecheck
|
||||||
|
npm run typecheck:e2e
|
||||||
|
npm test
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Test E2E
|
||||||
run: npm run typecheck
|
run: |
|
||||||
|
npm run test:e2e
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: |
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: |
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
- name: Copy build to host pages mount
|
- name: Copy build to host pages mount
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
apt update && apt install -y rsync
|
apt update && apt install -y rsync
|
||||||
mkdir -p /pages
|
rsync -a --delete dist/ /pages/fleeting
|
||||||
rsync -a --delete dist/ /pages/fleeting-garden
|
|
||||||
|
|
|
||||||
44
.gitignore
vendored
44
.gitignore
vendored
|
|
@ -1,45 +1,5 @@
|
||||||
# Dependency directory
|
|
||||||
node_modules
|
node_modules
|
||||||
modules/
|
|
||||||
ts-node--*/
|
|
||||||
rss.xml
|
|
||||||
|
|
||||||
dist
|
dist
|
||||||
|
test-results
|
||||||
# Logs
|
.DS_Store
|
||||||
logs
|
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.ssh
|
|
||||||
*.ppk
|
|
||||||
v8-compile-cache-0/
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
bin
|
|
||||||
ts-node
|
|
||||||
|
|
||||||
# Personal Scripts
|
|
||||||
*.bat
|
|
||||||
*.ssh
|
|
||||||
*.sh
|
|
||||||
!system.min.js
|
|
||||||
|
|
||||||
# Editors
|
|
||||||
.vscode
|
|
||||||
.markdownlint.json
|
|
||||||
|
|
||||||
# Build Files
|
|
||||||
temp
|
|
||||||
*.js
|
|
||||||
*.map
|
|
||||||
!webpack.*
|
|
||||||
|
|
|
||||||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
||||||
22
|
22.13.0
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,5 @@
|
||||||
"endOfLine": "lf",
|
"endOfLine": "lf",
|
||||||
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
|
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
|
||||||
"importOrder": ["<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>", "", "^[./]"],
|
"importOrder": ["<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>", "", "^[./]"],
|
||||||
"importOrderTypeScriptVersion": "5.0.0"
|
"importOrderTypeScriptVersion": "6.0.3"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
definitions.d.ts
vendored
4
definitions.d.ts
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
declare module '*.wgsl?raw' {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
265
e2e/app.spec.ts
Normal file
265
e2e/app.spec.ts
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
import { test as base, expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const canvasName = 'Interactive generative garden canvas';
|
||||||
|
|
||||||
|
interface BrowserDiagnostics {
|
||||||
|
browserFailures: Array<string>;
|
||||||
|
consoleErrors: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLocalUrl = (url: string) => {
|
||||||
|
const { hostname } = new URL(url);
|
||||||
|
return hostname === '127.0.0.1' || hostname === 'localhost';
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectLocalBrowserFailures = (page: Page) => {
|
||||||
|
const failures: Array<string> = [];
|
||||||
|
|
||||||
|
page.on('requestfailed', (request) => {
|
||||||
|
if (!isLocalUrl(request.url())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const failure = request.failure();
|
||||||
|
failures.push(`${request.method()} ${request.url()} ${failure?.errorText}`);
|
||||||
|
});
|
||||||
|
page.on('response', (response) => {
|
||||||
|
if (response.status() < 400 || !isLocalUrl(response.url())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
failures.push(`${response.status()} ${response.url()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return failures;
|
||||||
|
};
|
||||||
|
|
||||||
|
const test = base.extend<{ browserDiagnostics: BrowserDiagnostics }>({
|
||||||
|
browserDiagnostics: [
|
||||||
|
async ({ page }, use) => {
|
||||||
|
const browserFailures = collectLocalBrowserFailures(page);
|
||||||
|
const consoleErrors: Array<string> = [];
|
||||||
|
|
||||||
|
page.on('console', (message) => {
|
||||||
|
if (message.type() === 'error') {
|
||||||
|
consoleErrors.push(message.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await use({ browserFailures, consoleErrors });
|
||||||
|
|
||||||
|
expect(consoleErrors).toEqual([]);
|
||||||
|
expect(browserFailures).toEqual([]);
|
||||||
|
},
|
||||||
|
{ auto: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const disableWebGpu = async (page: Page) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, 'gpu', {
|
||||||
|
configurable: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test('starts the WebGPU garden and accepts drawing input', async ({ page }) => {
|
||||||
|
await page.addInitScript((expectedCanvasName) => {
|
||||||
|
const captureState = { count: 0 };
|
||||||
|
Object.defineProperty(window, '__fleetingGardenPointerCaptures', {
|
||||||
|
configurable: true,
|
||||||
|
value: captureState,
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalSetPointerCapture = Element.prototype.setPointerCapture;
|
||||||
|
Element.prototype.setPointerCapture = function setPointerCapture(pointerId) {
|
||||||
|
if (
|
||||||
|
this instanceof HTMLCanvasElement &&
|
||||||
|
this.getAttribute('aria-label') === expectedCanvasName
|
||||||
|
) {
|
||||||
|
captureState.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalSetPointerCapture.call(this, pointerId);
|
||||||
|
};
|
||||||
|
}, canvasName);
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
const startButton = page.getByRole('button', { exact: true, name: 'Start' });
|
||||||
|
await expect(startButton).toBeVisible();
|
||||||
|
await expect(startButton).toBeEnabled({ timeout: 30_000 });
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await expect(page.locator('body')).not.toHaveClass(/is-loading/, {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||||
|
await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible();
|
||||||
|
|
||||||
|
const canvas = page.getByRole('img', { name: canvasName });
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
const canvasSize = await canvas.evaluate((element) => {
|
||||||
|
const canvasElement = element as HTMLCanvasElement;
|
||||||
|
return {
|
||||||
|
height: canvasElement.height,
|
||||||
|
width: canvasElement.width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(canvasSize.width).toBeGreaterThan(0);
|
||||||
|
expect(canvasSize.height).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const box = await canvas.boundingBox();
|
||||||
|
expect(box).not.toBeNull();
|
||||||
|
if (!box) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.mouse.move(box.x + box.width * 0.2, box.y + box.height * 0.5);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(box.x + box.width * 0.8, box.y + box.height * 0.5, {
|
||||||
|
steps: 16,
|
||||||
|
});
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
page.evaluate(
|
||||||
|
() =>
|
||||||
|
(
|
||||||
|
window as unknown as {
|
||||||
|
__fleetingGardenPointerCaptures?: { count: number };
|
||||||
|
}
|
||||||
|
).__fleetingGardenPointerCaptures?.count ?? 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
page.evaluate(
|
||||||
|
() =>
|
||||||
|
(
|
||||||
|
window as unknown as {
|
||||||
|
__fleetingGardenBrushPasses?: number;
|
||||||
|
}
|
||||||
|
).__fleetingGardenBrushPasses ?? 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows a clear fallback when WebGPU is unavailable', async ({ page }) => {
|
||||||
|
await disableWebGpu(page);
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle('Fleeting Garden');
|
||||||
|
await expect(page.getByRole('img', { name: canvasName })).toBeVisible();
|
||||||
|
await expect(page.getByRole('toolbar', { name: 'Garden toolbar' })).toBeVisible();
|
||||||
|
await expect(page.locator('body')).not.toHaveClass(/is-loading/);
|
||||||
|
|
||||||
|
const fallback = page.getByRole('alert');
|
||||||
|
await expect(fallback).toContainText('Fleeting Garden needs WebGPU');
|
||||||
|
await expect(fallback).toContainText('webgpu-unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('syncs the selected vibe with the URI', async ({ page }) => {
|
||||||
|
await disableWebGpu(page);
|
||||||
|
await page.goto('/?vibe=Aurora%20Mycelium');
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/vibe=aurora-mycelium/);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Next vibe' }).click();
|
||||||
|
await expect(page).toHaveURL(/vibe=velvet-observatory/);
|
||||||
|
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(/vibe=aurora-mycelium/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps audio focus outlines scoped to the active control', async ({ page }) => {
|
||||||
|
await disableWebGpu(page);
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('body')).not.toHaveClass(/is-loading/);
|
||||||
|
|
||||||
|
const audioControl = page.locator('.audio-control');
|
||||||
|
const soundButton = page.getByRole('button', { name: /audio/i });
|
||||||
|
const volumeSlider = page.getByRole('slider', { name: 'Master volume' });
|
||||||
|
|
||||||
|
await soundButton.click();
|
||||||
|
await expect(audioControl).toHaveCSS('outline-style', 'none');
|
||||||
|
await expect(soundButton).toHaveCSS('outline-style', 'none');
|
||||||
|
|
||||||
|
await page.mouse.click(10, 10);
|
||||||
|
for (let tabIndex = 0; tabIndex < 12; tabIndex += 1) {
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
const activeClass = await page.evaluate(() =>
|
||||||
|
String(document.activeElement?.className ?? '')
|
||||||
|
);
|
||||||
|
if (activeClass.includes('sound')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(soundButton).toBeFocused();
|
||||||
|
await expect(soundButton).toHaveCSS('outline-style', 'solid');
|
||||||
|
await expect(soundButton).toHaveCSS('outline-offset', '-4px');
|
||||||
|
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await expect(volumeSlider).toBeFocused();
|
||||||
|
await expect(volumeSlider).toHaveCSS('outline-style', 'solid');
|
||||||
|
await expect(volumeSlider).toHaveCSS('outline-offset', '-4px');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the config overlay scrollable and dismissible on mobile', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setViewportSize({ width: 390, height: 640 });
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const startButton = page.getByRole('button', { exact: true, name: 'Start' });
|
||||||
|
await expect(startButton).toBeEnabled({ timeout: 30_000 });
|
||||||
|
await startButton.click();
|
||||||
|
await expect(page.locator('body')).not.toHaveClass(/is-loading/, {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsButton = page.getByRole('button', { name: 'Show config overlay' });
|
||||||
|
await settingsButton.click();
|
||||||
|
|
||||||
|
const pane = page.locator('.config-pane');
|
||||||
|
const closeButton = page.locator('.config-pane-close');
|
||||||
|
await expect(pane).toBeVisible();
|
||||||
|
await expect(closeButton).toBeVisible();
|
||||||
|
|
||||||
|
const paneMetrics = await pane.evaluate((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
bottom: rect.bottom,
|
||||||
|
clientHeight: element.clientHeight,
|
||||||
|
overflowY: style.overflowY,
|
||||||
|
scrollHeight: element.scrollHeight,
|
||||||
|
top: rect.top,
|
||||||
|
viewportHeight: window.innerHeight,
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
width: rect.width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(paneMetrics.top).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(paneMetrics.bottom).toBeLessThanOrEqual(paneMetrics.viewportHeight);
|
||||||
|
expect(Math.round(paneMetrics.width)).toBe(Math.round(paneMetrics.viewportWidth * 0.8));
|
||||||
|
expect(paneMetrics.scrollHeight).toBeGreaterThan(paneMetrics.clientHeight);
|
||||||
|
expect(['auto', 'scroll']).toContain(paneMetrics.overflowY);
|
||||||
|
|
||||||
|
await pane.evaluate((element) => {
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
});
|
||||||
|
await expect
|
||||||
|
.poll(() => pane.evaluate((element) => element.scrollTop))
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await closeButton.click();
|
||||||
|
await expect(pane).toBeHidden();
|
||||||
|
await expect(settingsButton).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
30
eslint.config.js
Normal file
30
eslint.config.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['node_modules/**', 'dist/**', 'public/**'],
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
prettierConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
1174
package-lock.json
generated
1174
package-lock.json
generated
File diff suppressed because it is too large
Load diff
37
package.json
37
package.json
|
|
@ -1,22 +1,28 @@
|
||||||
{
|
{
|
||||||
"name": "webgpu-seed",
|
"name": "fleeting-garden",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "A WebGPU-powered slime-mold-meets-territory-control simulation.",
|
"description": "Tend it while you can. The garden returns to weather either way. A WebGPU drawing toy in your browser.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.{ts,scss,json,html}\"",
|
"lint:check": "eslint . && npm run unused:check",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write \"index.html\" \"public/manifest.webmanifest\" \"src/**/*.{ts,scss,json,html}\" \"e2e/**/*.ts\" \".forgejo/workflows/*.yml\" \"*.{json,js,ts,md}\" \".prettierrc\"",
|
||||||
|
"format:check": "prettier --check \"index.html\" \"public/manifest.webmanifest\" \"src/**/*.{ts,scss,json,html}\" \"e2e/**/*.ts\" \".forgejo/workflows/*.yml\" \"*.{json,js,ts,md}\" \".prettierrc\"",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
"typecheck:e2e": "tsc --noEmit --project tsconfig.playwright.json",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"generate-icons": "pwa-assets-generator",
|
"unused:check": "knip --production --files --dependencies && knip --exports --include-entry-exports",
|
||||||
"update": "ncu"
|
"generate-icons": "pwa-assets-generator"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=22.13.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -33,23 +39,28 @@
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"supports webgpu and last 2 years"
|
"supports webgpu and last 2 years"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"knip": {
|
||||||
"gl-matrix": "^3.4.4"
|
"ignoreFiles": [
|
||||||
|
"pwa-assets.config.ts"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
|
"@tweakpane/core": "~2.0.5",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
|
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
||||||
"@webgpu/types": "^0.1.69",
|
"@webgpu/types": "^0.1.69",
|
||||||
"browserslist": "^4.28.2",
|
"browserslist": "^4.28.2",
|
||||||
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
"gl-matrix": "^3.4.4",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"knip": "^6.14.1",
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"npm-check-updates": "^22.1.0",
|
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
|
|
@ -57,5 +68,9 @@
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.0.10",
|
||||||
"vite-plugin-singlefile": "^2.3.3",
|
"vite-plugin-singlefile": "^2.3.3",
|
||||||
"vitest": "^4.1.5"
|
"vitest": "^4.1.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@plausible-analytics/tracker": "^0.4.5",
|
||||||
|
"tweakpane": "~4.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
playwright.config.ts
Normal file
37
playwright.config.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
const port = 4173;
|
||||||
|
const baseURL = `https://127.0.0.1:${port}`;
|
||||||
|
const isCi = Boolean(process.env.CI);
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: isCi,
|
||||||
|
retries: isCi ? 2 : 0,
|
||||||
|
workers: 1,
|
||||||
|
reporter: isCi ? [['list'], ['html', { open: 'never' }]] : 'list',
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: `npm run build && npm run preview -- --host 127.0.0.1 --port ${port}`,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
reuseExistingServer: false,
|
||||||
|
timeout: 120_000,
|
||||||
|
url: baseURL,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
launchOptions: {
|
||||||
|
args: ['--enable-unsafe-webgpu'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -9,14 +9,13 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": false
|
"noUnusedParameters": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "definitions.d.ts", "vite.config.ts"]
|
"include": ["src/**/*", "pwa-assets.config.ts", "vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
tsconfig.playwright.json
Normal file
7
tsconfig.playwright.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["playwright.config.ts", "e2e/**/*.ts"]
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
|
import basicSsl from '@vitejs/plugin-basic-ssl';
|
||||||
import browserslist from 'browserslist';
|
import browserslist from 'browserslist';
|
||||||
|
import browserslistToEsbuild from 'browserslist-to-esbuild';
|
||||||
import { browserslistToTargets } from 'lightningcss';
|
import { browserslistToTargets } from 'lightningcss';
|
||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
const cssTargets = browserslistToTargets(browserslist());
|
const cssTargets = browserslistToTargets(browserslist());
|
||||||
|
const esbuildTargets = browserslistToEsbuild();
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => ({
|
||||||
plugins: [viteSingleFile()],
|
plugins: [viteSingleFile(), ...(command === 'serve' ? [basicSsl()] : [])],
|
||||||
css: {
|
css: {
|
||||||
transformer: 'lightningcss',
|
transformer: 'lightningcss',
|
||||||
lightningcss: {
|
lightningcss: {
|
||||||
|
|
@ -14,16 +17,14 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
target: 'es2022',
|
target: esbuildTargets,
|
||||||
cssCodeSplit: false,
|
|
||||||
cssMinify: 'lightningcss',
|
cssMinify: 'lightningcss',
|
||||||
assetsInlineLimit: Number.MAX_SAFE_INTEGER,
|
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
open: true,
|
host: true,
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
include: ['src/**/*.test.ts'],
|
include: ['src/**/*.test.ts'],
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue