diff --git a/.github/workflows/tessl-publish.yml b/.github/workflows/tessl-publish.yml
index 5221375..9a6be93 100644
--- a/.github/workflows/tessl-publish.yml
+++ b/.github/workflows/tessl-publish.yml
@@ -11,4 +11,4 @@ jobs:
- uses: tesslio/setup-tessl@v2
with:
token: ${{ secrets.TESSL_TOKEN }}
- - run: tessl tile publish
\ No newline at end of file
+ - run: tessl tile publish ./skills/vaadin-playwright-test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2ff5f03..056426a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,6 @@ package.json
.vscode/.copilot-plugin
.vscode/launch.json
+
+dist
+.env
diff --git a/skills/vaadin-playwright-test/SKILL.md b/skills/vaadin-playwright-test/SKILL.md
index 9e6ff08..8cf5b74 100644
--- a/skills/vaadin-playwright-test/SKILL.md
+++ b/skills/vaadin-playwright-test/SKILL.md
@@ -7,72 +7,113 @@ description: Generate Playwright integration tests for Vaadin 25 views using the
## Best practices
-Always follow the guidelines in [@TESTING.md](TESTING.md) when generating tests. Key rules:
-
-- **One test, one assert** — each test method covers a single piece of functionality
-- **User-facing locators** — prefer label, `aria-label`, `aria-role`, or `data-testid` over CSS classes or generated IDs
-- **DramaFinder elements for all interactions** — never interact with raw locators when a wrapper exists
-- **No `Thread.sleep()`** — use Playwright auto-waiting or `waitFor` methods instead
-- **Assert on user-visible state** — check visibility, text, or enabled/disabled, not internal CSS or component state
+Always follow [@TESTING.md](TESTING.md) when generating tests. Key rules:
+
+- **One test, one assert** — each test method covers a single piece of
+ functionality
+- **User-facing locators** — prefer label, `aria-label`, `aria-role`, or
+ `data-testid` over CSS classes or generated IDs
+- **DramaFinder elements for all interactions** — never interact with raw
+ locators when a wrapper exists
+- **No `Thread.sleep()`** — use Playwright auto-waiting or `waitFor` methods
+ instead
+- **Assert on user-visible state** — check visibility, text, or
+ enabled/disabled, not internal CSS or component state
## Step 1 — Assess project state
Run these checks in parallel before doing anything else:
-1. **DramaFinder on classpath?** — grep `pom.xml` for `dramafinder` or `org.vaadin.addons`
-2. **Spring Boot app?** — grep `pom.xml` for `spring-boot-starter` or `vaadin-spring-boot-starter`
-3. **Existing IT tests?** — look for `*IT.java` files under `src/test/java`
+1. **DramaFinder on classpath?** — grep `pom.xml` for
+ `dramafinder`.
+2. **Spring Boot app?** — grep `pom.xml` for `spring-boot-starter`.
+3. **Existing IT tests?** — look for `*IT.java` files under `src/test/java`.
+4. **`SpringPlaywrightIT` already in project?** —
+ `find src/test/java -name SpringPlaywrightIT.java`.
+
+### DramaFinder not found — propose setup, then run it
-### DramaFinder not found
+Resolve the latest version (Step 1 of [setup.md](setup.md)) and propose the
+following in a single confirmation:
-Show the user this message and stop:
+- Add `org.vaadin.addons:dramafinder:` and
+ `com.microsoft.playwright:playwright` (test scope) to `pom.xml` with
+ `` in ``.
+- **Spring Boot only:** also create
+ `src/test/java//it/support/SpringPlaywrightIT.java`.
-> DramaFinder is not on the classpath. Follow the [setup guide](setup.md) to add the required dependencies, then come back to generate tests.
+On confirmation, execute [setup.md](setup.md) end-to-end, then continue with
+Step 2.
### DramaFinder found — follow existing patterns
-If existing `*IT.java` files are found, read one or two of them to understand the project's conventions (base class, package structure, assertion style, helper methods). Use those as the template for generated tests.
+If existing `*IT.java` files are found, read one or two to understand the
+project's conventions (base class, package structure, assertion style, helper
+methods) and use them as the template.
If no existing IT tests exist, use the default structure in Step 3.
+### `SpringPlaywrightIT` location
+
+- 1 hit → use that fully-qualified class name.
+- 0 hits + Spring Boot detected → run setup (it will create the file).
+- More than 1 hit → ask the user which one to use.
+
## Step 2 — Map view components to DramaFinder elements
Read the target view source provided by the user. Extract:
-- `@Route("value")` → URL path (default: class name lowercased, stripped of "View" suffix)
+- `@Route("value")` → URL path (default: class name lowercased, stripped of
+ `View` suffix, e.g. `PersonView` → `/person`).
- `@PageTitle("...")` → expected page title
-- All Vaadin component field declarations and `add(...)` calls → map to DramaFinder elements
+- Every interactive component → its DramaFinder wrapper (see table below).
+- Form fields → label text used as locator.
+- Grids → column headers and row content to assert against.
+- Navigation triggers → button labels or menu items that cause route changes.
-See [element-mapping.md](element-mapping.md) for the full component → element class table. Each element also has detailed documentation with examples in the [specifications folder](https://github.com/parttio/dramafinder/tree/master/docs/specifications).
+See [element-mapping.md](element-mapping.md) for the full component → element
+class table. Each element also has detailed documentation with examples in
+the [specifications folder](https://github.com/parttio/dramafinder/tree/master/docs/specifications).
-For components with **no DramaFinder wrapper**, use a plain Playwright locator. For more complex needs, you can create your own element class extending `VaadinElement`, or [open an issue](https://github.com/vaadin/dramafinder/issues) in the DramaFinder repository to request one.
+For components with **no DramaFinder wrapper**, use a plain Playwright locator.
+For more complex needs, you can create your own element class extending
+`VaadinElement`,
+or [open an issue](https://github.com/vaadin/dramafinder/issues) in the
+DramaFinder repository to request one.
## Step 3 — Generate the test class
### Default structure (no existing tests to mirror)
```java
-package ; // mirror src/test/java structure
+package
+
+; // mirror src/test/java structure
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.vaadin.addons.dramafinder.element.TextFieldElement; // import only used elements
-import org.vaadin.addons.dramafinder.tests.it.SpringPlaywrightIT; // or AbstractBasePlaywrightIT
+
+import .it.support.SpringPlaywrightIT; // Spring projects: actual location from Step 1
+// import org.vaadin.addons.dramafinder.AbstractBasePlaywrightIT; // non-Spring projects
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
-@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) // omit if not Spring Boot
-public class IT extends SpringPlaywrightIT { // or AbstractBasePlaywrightIT
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+// omit if not Spring Boot
+public class IT extends
+
+SpringPlaywrightIT { // or AbstractBasePlaywrightIT
@Override
- public String getView() {
+ public String getView () {
return "/";
}
@Test
- public void testTitle() {
+ public void testTitle () {
assertThat(page).hasTitle("");
}
@@ -80,27 +121,39 @@ public class IT extends SpringPlaywrightIT { // or AbstractBasePlaywri
}
```
-Use `SpringPlaywrightIT` if Spring Boot is detected, `AbstractBasePlaywrightIT` otherwise.
+Use `SpringPlaywrightIT` if Spring Boot is detected, `AbstractBasePlaywrightIT`
+otherwise.
### Component test patterns
**Smoke test (one per component):**
```java
-@Test
-public void test() {
- TextFieldElement field = TextFieldElement.getByLabel(page, "My Label");
- field.assertVisible();
- field.assertLabel("My Label");
- field.assertValue("");
- field.setValue("test value");
- field.assertValue("test value");
+ @Test
+public void test(){
+TextFieldElement field = TextFieldElement.getByLabel(page, "My Label");
+ field.
+
+assertVisible();
+ field.
+
+assertLabel("My Label");
+ field.
+
+assertValue("");
+ field.
+
+setValue("test value");
+ field.
+
+assertValue("test value");
}
```
**Form with validation:**
```java
+
@Test
public void testFormSubmitWithInvalidInput() {
TextFieldElement nameField = TextFieldElement.getByLabel(page, "Name");
@@ -124,6 +177,7 @@ public void testFormSubmitWithValidInput() {
**Grid data loading:**
```java
+
@Test
public void testGridLoadsData() {
GridElement grid = GridElement.get(page);
@@ -138,7 +192,8 @@ Display the full generated test class in a code block. Then ask:
> Shall I write this to `src/test/java//IT.java`?
-Only write the file after explicit confirmation. Place it in `src/test/java` mirroring the view's package under `src/main/java`.
+Only write the file after explicit confirmation. Place it in `src/test/java`
+mirroring the view's package under `src/main/java`.
## Step 5 — Offer to run the test
@@ -146,4 +201,5 @@ After writing, ask:
> Do you want me to run this test now with `mvn verify -Dit.test=IT`?
-**Warn the user**: the first Vaadin frontend build takes 3–5 minutes. Subsequent runs are ~25 seconds.
+**Warn the user**: the first Vaadin frontend build takes 3–5 minutes. Subsequent
+runs are ~25 seconds.
diff --git a/skills/vaadin-playwright-test/evals/.env.example b/skills/vaadin-playwright-test/evals/.env.example
new file mode 100644
index 0000000..6fb75b1
--- /dev/null
+++ b/skills/vaadin-playwright-test/evals/.env.example
@@ -0,0 +1,16 @@
+# Provider selection: "local" (LM Studio) or "gemini"
+EVAL_PROVIDER=local
+
+# Local model (LM Studio, OpenAI-compatible)
+LOCAL_BASE_URL=http://127.0.0.1:1234/v1
+LOCAL_MODEL=openai/gpt-oss-20b
+
+# Gemini (uses Google's OpenAI-compatible endpoint)
+GEMINI_API_KEY=
+GEMINI_MODEL=gemini-2.0-flash
+GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
+
+# Langfuse (cloud or self-hosted)
+LANGFUSE_PUBLIC_KEY=
+LANGFUSE_SECRET_KEY=
+LANGFUSE_HOST=https://cloud.langfuse.com
diff --git a/skills/vaadin-playwright-test/evals/README.md b/skills/vaadin-playwright-test/evals/README.md
new file mode 100644
index 0000000..cda75af
--- /dev/null
+++ b/skills/vaadin-playwright-test/evals/README.md
@@ -0,0 +1,45 @@
+# vaadin-playwright-test — eval harness
+
+Phase 0 bootstrap. See `../../../PLAN.md` for the full plan.
+
+The harness uses the OpenAI SDK against any OpenAI-compatible endpoint.
+Two providers are supported out of the box:
+
+- `local` — LM Studio (default), e.g. `openai/gpt-oss-20b` at `http://127.0.0.1:1234/v1`
+- `gemini` — Google's OpenAI-compatible endpoint
+
+## Setup
+
+```bash
+cd skills/vaadin-playwright-test/evals
+npm install
+cp .env.example .env
+# fill in keys / pick a provider
+```
+
+For self-hosted Langfuse:
+
+```bash
+docker compose up -d
+# then set LANGFUSE_HOST=http://localhost:3000 in .env
+```
+
+## Run
+
+```bash
+# default: local LM Studio
+npm run smoke
+
+# Gemini
+EVAL_PROVIDER=gemini npm run smoke
+```
+
+Runs one hardcoded prompt twice (with/without the skill in the system prompt)
+and logs both as Langfuse traces tagged `phase=bootstrap`.
+
+## Note on prompt caching
+
+The PLAN.md calls for prompt caching on the system prompt. Neither LM Studio
+nor Gemini's OpenAI-compatible endpoint supports the Anthropic-style
+`cache_control` parameter, so caching is a no-op here. Re-introduce it when
+the harness is pointed at a provider that supports it.
diff --git a/skills/vaadin-playwright-test/evals/docker-compose.yaml b/skills/vaadin-playwright-test/evals/docker-compose.yaml
new file mode 100644
index 0000000..d90f003
--- /dev/null
+++ b/skills/vaadin-playwright-test/evals/docker-compose.yaml
@@ -0,0 +1,29 @@
+services:
+ langfuse-db:
+ image: postgres:16
+ restart: always
+ environment:
+ POSTGRES_USER: langfuse
+ POSTGRES_PASSWORD: langfuse
+ POSTGRES_DB: langfuse
+ volumes:
+ - langfuse_db_data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+
+ langfuse:
+ image: langfuse/langfuse:2
+ restart: always
+ depends_on:
+ - langfuse-db
+ ports:
+ - "3000:3000"
+ environment:
+ DATABASE_URL: postgresql://langfuse:langfuse@langfuse-db:5432/langfuse
+ NEXTAUTH_SECRET: change-me
+ SALT: change-me
+ NEXTAUTH_URL: http://localhost:3000
+ TELEMETRY_ENABLED: "false"
+
+volumes:
+ langfuse_db_data:
diff --git a/skills/vaadin-playwright-test/evals/src/harness.ts b/skills/vaadin-playwright-test/evals/src/harness.ts
new file mode 100644
index 0000000..cbad346
--- /dev/null
+++ b/skills/vaadin-playwright-test/evals/src/harness.ts
@@ -0,0 +1,97 @@
+import OpenAI from "openai";
+import { readFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const SKILL_PATH = resolve(__dirname, "../../SKILL.md");
+
+const BASE_SYSTEM =
+ "You are a senior engineer helping to write Playwright tests for a Vaadin application called Drama Finder. Reply with the requested code only, no commentary.";
+
+type Provider = "local" | "gemini";
+
+interface ProviderConfig {
+ client: OpenAI;
+ model: string;
+ provider: Provider;
+}
+
+function getProvider(): ProviderConfig {
+ const provider = (process.env.EVAL_PROVIDER ?? "local") as Provider;
+
+ if (provider === "gemini") {
+ const apiKey = process.env.GEMINI_API_KEY;
+ if (!apiKey) throw new Error("GEMINI_API_KEY is not set");
+ return {
+ provider,
+ model: process.env.GEMINI_MODEL ?? "gemini-2.0-flash",
+ client: new OpenAI({
+ apiKey,
+ baseURL:
+ process.env.GEMINI_BASE_URL ??
+ "https://generativelanguage.googleapis.com/v1beta/openai/",
+ }),
+ };
+ }
+
+ if (provider === "local") {
+ return {
+ provider,
+ model: process.env.LOCAL_MODEL ?? "openai/gpt-oss-20b",
+ client: new OpenAI({
+ apiKey: "lm-studio",
+ baseURL: process.env.LOCAL_BASE_URL ?? "http://127.0.0.1:1234/v1",
+ }),
+ };
+ }
+
+ throw new Error(`Unknown EVAL_PROVIDER: ${provider}`);
+}
+
+function buildSystem(withSkill: boolean): string {
+ if (!withSkill) return BASE_SYSTEM;
+ const skill = readFileSync(SKILL_PATH, "utf8");
+ return `${BASE_SYSTEM}\n\nFollow the guidance in the skill below.\n\n\n${skill}\n`;
+}
+
+export interface RunResult {
+ text: string;
+ model: string;
+ provider: Provider;
+ usage: {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ };
+}
+
+export async function runWithSkill(
+ prompt: string,
+ withSkill: boolean,
+): Promise {
+ const { client, model, provider } = getProvider();
+
+ const completion = await client.chat.completions.create({
+ model,
+ max_tokens: 4096,
+ messages: [
+ { role: "system", content: buildSystem(withSkill) },
+ { role: "user", content: prompt },
+ ],
+ });
+
+ const text = completion.choices[0]?.message?.content ?? "";
+ const usage = completion.usage;
+
+ return {
+ text,
+ model,
+ provider,
+ usage: {
+ prompt_tokens: usage?.prompt_tokens ?? 0,
+ completion_tokens: usage?.completion_tokens ?? 0,
+ total_tokens: usage?.total_tokens ?? 0,
+ },
+ };
+}
diff --git a/skills/vaadin-playwright-test/evals/src/runSmoke.ts b/skills/vaadin-playwright-test/evals/src/runSmoke.ts
new file mode 100644
index 0000000..cfc4922
--- /dev/null
+++ b/skills/vaadin-playwright-test/evals/src/runSmoke.ts
@@ -0,0 +1,59 @@
+import { Langfuse } from "langfuse";
+import { runWithSkill } from "./harness.js";
+
+const SMOKE_PROMPT =
+ "Write a Playwright Java test for the Drama Finder app that fills the TextField labeled 'Title' with 'Hamlet' and clicks the Button labeled 'Search'. Show only the test method body.";
+
+async function main() {
+ const langfuse = new Langfuse();
+
+ for (const withSkill of [false, true]) {
+ const label = withSkill ? "with-skill" : "without-skill";
+ console.log(`\n=== ${label} ===`);
+
+ const start = new Date();
+ const result = await runWithSkill(SMOKE_PROMPT, withSkill);
+ const end = new Date();
+
+ const trace = langfuse.trace({
+ name: `smoke-${label}`,
+ tags: ["phase=bootstrap", label],
+ input: SMOKE_PROMPT,
+ metadata: {
+ withSkill,
+ provider: result.provider,
+ model: result.model,
+ },
+ });
+
+ trace.generation({
+ name: "chat.completions.create",
+ model: result.model,
+ modelParameters: { provider: result.provider },
+ startTime: start,
+ endTime: end,
+ input: [
+ { role: "system", content: "" },
+ { role: "user", content: SMOKE_PROMPT },
+ ],
+ output: result.text,
+ usage: {
+ input: result.usage.prompt_tokens,
+ output: result.usage.completion_tokens,
+ total: result.usage.total_tokens,
+ },
+ metadata: { withSkill, provider: result.provider, model: result.model },
+ });
+
+ trace.update({ output: result.text });
+
+ console.log(result.text);
+ }
+
+ await langfuse.shutdownAsync();
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/skills/vaadin-playwright-test/evals/tsconfig.json b/skills/vaadin-playwright-test/evals/tsconfig.json
new file mode 100644
index 0000000..d1387cd
--- /dev/null
+++ b/skills/vaadin-playwright-test/evals/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "forceConsistentCasingInFileNames": true,
+ "noUncheckedIndexedAccess": true,
+ "outDir": "dist"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/skills/vaadin-playwright-test/setup.md b/skills/vaadin-playwright-test/setup.md
index 15fc974..6fe002f 100644
--- a/skills/vaadin-playwright-test/setup.md
+++ b/skills/vaadin-playwright-test/setup.md
@@ -1,82 +1,86 @@
---
name: DramaFinder Setup
-description: How to add DramaFinder and Playwright to a Vaadin 25 Spring Boot project.
+description: Runbook for adding DramaFinder + Playwright to a Vaadin 25 project. Executed by Claude after the user confirms the setup plan.
---
-# Setting Up DramaFinder in Your Vaadin Project
+# DramaFinder Setup Runbook
-## 1. Add dependencies to `pom.xml`
+This file is a runbook for Claude to execute after the user confirms the setup plan in SKILL.md Step 1. Do not relay it to the user as documentation — perform the steps.
+
+## Constants
+
+- `KNOWN_LATEST = 1.1.1` — fallback version if Maven Central lookup fails. Bump when the library releases.
+
+## Step 1 — Resolve the latest version
+
+Run:
+
+```bash
+curl -s "https://repo1.maven.org/maven2/org/vaadin/addons/dramafinder/maven-metadata.xml" \
+ | grep -o '[^<]*' | sed 's/<[^>]*>//g'
+```
+
+If the command returns a non-empty version string, use it. Otherwise, use `KNOWN_LATEST`.
+
+## Step 2 — Edit `pom.xml`
+
+Two edits, both in `pom.xml`:
+
+### 2a. Add the version property
+
+Inside ``, add:
+
+```xml
+RESOLVED_VERSION
+```
+
+Replace `RESOLVED_VERSION` with the value from Step 1. If a `` property already exists, leave it alone.
+
+### 2b. Add the dependencies
+
+Inside ``, add (skip whichever is already present):
```xml
-
com.microsoft.playwright
playwright
test
-
- org.vaadin.addons.dramafinder
+ org.vaadin.addons
dramafinder
- LATEST
+ ${dramafinder.version}
test
```
-Check the latest version at [GitHub releases](https://github.com/vaadin/dramafinder/releases).
+Note the groupId is `org.vaadin.addons` (not `org.vaadin.addons.dramafinder`).
-## 2. Base test class
+## Step 3 — Copy `SpringPlaywrightIT` (Spring Boot projects only)
-All IT tests extend `SpringPlaywrightIT` (Spring Boot) or `AbstractBasePlaywrightIT` (plain):
+Skip this step entirely if the project is not a Spring Boot project.
-```java
-@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
-public class MyViewIT extends SpringPlaywrightIT {
+If `find src/test/java -name SpringPlaywrightIT.java` already returns a result, skip — the file exists and will be used as-is.
- @Override
- public String getView() {
- return "/my-route";
- }
+Otherwise:
- @Test
- public void testTitle() {
- assertThat(page).hasTitle("Expected Title");
- }
-}
-```
+1. **Find the base package** by locating the class annotated with `@SpringBootApplication` under `src/main/java`. Take its package (e.g., `com.example.app`).
+2. **Target package** is `.it.support` (e.g., `com.example.app.it.support`).
+3. **Read** `templates/SpringPlaywrightIT.java.tmpl` from this skill directory.
+4. **Substitute** `{{PACKAGE}}` with the target package.
+5. **Write** the result to `src/test/java//it/support/SpringPlaywrightIT.java`.
-## 3. Run tests
+## Step 4 — Confirm dependencies resolve
-```bash
-# Run all IT tests
-mvn verify
+Optional but recommended: run `mvn -q dependency:resolve` to surface any pom syntax errors before generating tests. Skip if the user wants to proceed without compile-time verification.
-# Run a specific IT test
-mvn verify -Dit.test=MyViewIT
-```
+## Debugging with a visible browser (informational)
-> **Note:** The first Vaadin frontend build takes 3–5 minutes. Subsequent runs take ~25 seconds.
-
-## 4. Debugging with a visible browser
-
-To disable headless mode and watch the browser during a test run, use the `headless` property or add a `debug-ui` profile:
+To run tests with a visible browser, pass `-Dheadless=false`:
```bash
-# Via system property
mvn -Dit.test=MyViewIT -Dheadless=false verify
-
-# Via profile (if added to pom.xml)
-mvn -Pdebug-ui -Dit.test=MyViewIT verify
```
-Add this profile to your `pom.xml` to enable the `-Pdebug-ui` shorthand:
-
-```xml
-
- debug-ui
-
- false
-
-
-```
+A `debug-ui` profile can be added to `pom.xml` for shorthand `-Pdebug-ui`. Mention this only if the user asks about debugging.
diff --git a/skills/vaadin-playwright-test/templates/SpringPlaywrightIT.java.tmpl b/skills/vaadin-playwright-test/templates/SpringPlaywrightIT.java.tmpl
new file mode 100644
index 0000000..ecb40b7
--- /dev/null
+++ b/skills/vaadin-playwright-test/templates/SpringPlaywrightIT.java.tmpl
@@ -0,0 +1,15 @@
+package {{PACKAGE}};
+
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.vaadin.addons.dramafinder.AbstractBasePlaywrightIT;
+
+public abstract class SpringPlaywrightIT extends AbstractBasePlaywrightIT {
+
+ @LocalServerPort
+ private int port;
+
+ @Override
+ public String getUrl() {
+ return String.format("http://localhost:%d/", port);
+ }
+}
diff --git a/tile.json b/skills/vaadin-playwright-test/tile.json
similarity index 82%
rename from tile.json
rename to skills/vaadin-playwright-test/tile.json
index bc0a8a1..19c8d94 100644
--- a/tile.json
+++ b/skills/vaadin-playwright-test/tile.json
@@ -1,11 +1,11 @@
{
"name": "dramafinder/vaadin-playwright-test",
- "version": "0.1.0",
+ "version": "0.1.1",
"private": false,
"summary": "Generate Playwright integration tests for Vaadin 25 views using the DramaFinder library. Use when the user wants to write IT tests for a Vaadin view, mentions DramaFinder, or asks about Playwright testing in a Vaadin project.",
"skills": {
"vaadin-playwright-test": {
- "path": "skills/vaadin-playwright-test/SKILL.md"
+ "path": "SKILL.md"
}
}
}
diff --git a/tessl.json b/tessl.json
new file mode 100644
index 0000000..6e64474
--- /dev/null
+++ b/tessl.json
@@ -0,0 +1,5 @@
+{
+ "name": "dramafinder",
+ "mode": "vendored",
+ "dependencies": {}
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..4d6a022
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,9 @@
+import { UserConfigFn } from 'vite';
+import { overrideVaadinConfig } from './vite.generated';
+
+const customConfig: UserConfigFn = (env) => ({
+ // Here you can add custom Vite parameters
+ // https://vitejs.dev/config/
+});
+
+export default overrideVaadinConfig(customConfig);
diff --git a/vite.generated.ts b/vite.generated.ts
new file mode 100644
index 0000000..5bf4fdd
--- /dev/null
+++ b/vite.generated.ts
@@ -0,0 +1,668 @@
+/**
+ * NOTICE: this is an auto-generated file
+ *
+ * This file has been generated by the `flow:prepare-frontend` maven goal.
+ * This file will be overwritten on every run. Any custom changes should be made to vite.config.ts
+ */
+import path from 'path';
+import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, Stats } from 'fs';
+import { createHash } from 'crypto';
+import * as net from 'net';
+
+import { processThemeResources } from './target/plugins/application-theme-plugin/theme-handle.js';
+import { rewriteCssUrls } from './target/plugins/theme-loader/theme-loader-utils.js';
+import { addFunctionComponentSourceLocationBabel } from './target/plugins/react-function-location-plugin/react-function-location-plugin.js';
+import settings from './target/vaadin-dev-server-settings.json';
+import {
+ AssetInfo,
+ ChunkInfo,
+ defineConfig,
+ mergeConfig,
+ OutputOptions,
+ PluginOption,
+ UserConfigFn
+} from 'vite';
+
+import brotli from 'rollup-plugin-brotli';
+import checker from 'vite-plugin-checker';
+import postcssLit from './target/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js';
+import vaadinI18n from './target/plugins/rollup-plugin-vaadin-i18n/rollup-plugin-vaadin-i18n.js';
+
+export { default as useLocalWebComponents } from './target/plugins/vite-plugin-local-web-components';
+
+import { visualizer } from 'rollup-plugin-visualizer';
+import reactPlugin from '@vitejs/plugin-react';
+
+
+
+
+const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
+const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
+const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput);
+const devBundleFolder = path.resolve(__dirname, settings.devBundleOutput);
+const devBundle = !!process.env.devBundle;
+const jarResourcesFolder = path.resolve(__dirname, settings.jarResourcesFolder);
+const themeResourceFolder = path.resolve(__dirname, settings.themeResourceFolder);
+const projectPackageJsonFile = path.resolve(__dirname, 'package.json');
+
+const buildOutputFolder = devBundle ? devBundleFolder : frontendBundleFolder;
+const statsFolder = path.resolve(__dirname, devBundle ? settings.devBundleStatsOutput : settings.statsOutput);
+const statsFile = path.resolve(statsFolder, 'stats.json');
+const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html');
+const i18nFolder = path.resolve(__dirname, settings.i18nOutput);
+const nodeModulesFolder = path.resolve(__dirname, 'node_modules');
+const webComponentTags = '';
+
+const projectIndexHtml = path.resolve(frontendFolder, 'index.html');
+
+const projectStaticAssetsFolders = [
+ path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'),
+ path.resolve(__dirname, 'src', 'main', 'resources', 'static'),
+ frontendFolder
+];
+
+// Folders in the project which can contain application themes
+const themeProjectFolders = projectStaticAssetsFolders.map((folder) => path.resolve(folder, settings.themeFolder));
+
+const themeOptions = {
+ devMode: false,
+ useDevBundle: devBundle,
+ // The following matches folder 'frontend/generated/themes/'
+ // (not 'frontend/themes') for theme in JAR that is copied there
+ themeResourceFolder: path.resolve(themeResourceFolder, settings.themeFolder),
+ themeProjectFolders: themeProjectFolders,
+ projectStaticAssetsOutputFolder: devBundle
+ ? path.resolve(devBundleFolder, '../assets')
+ : path.resolve(__dirname, settings.staticOutput),
+ frontendGeneratedFolder: path.resolve(frontendFolder, settings.generatedFolder),
+ projectStaticOutput: path.resolve(__dirname, settings.staticOutput),
+ javaResourceFolder: settings.javaResourceFolder ? path.resolve(__dirname, settings.javaResourceFolder) : ''
+};
+
+const hasExportedWebComponents = existsSync(path.resolve(frontendFolder, 'web-component.html'));
+const commercialBannerComponent = path.resolve(frontendFolder, settings.generatedFolder, 'commercial-banner.js');
+const hasCommercialBanner = existsSync(commercialBannerComponent);
+
+const target = ['es2023'];
+
+// Block debug and trace logs.
+console.trace = () => {};
+console.debug = () => {};
+
+function statsExtracterPlugin(): PluginOption {
+ function collectThemeJsonsInFrontend(themeJsonContents: Record, themeName: string) {
+ const themeJson = path.resolve(frontendFolder, settings.themeFolder, themeName, 'theme.json');
+ if (existsSync(themeJson)) {
+ const themeJsonContent = readFileSync(themeJson, { encoding: 'utf-8' }).replace(/\r\n/g, '\n');
+ themeJsonContents[themeName] = themeJsonContent;
+ const themeJsonObject = JSON.parse(themeJsonContent);
+ if (themeJsonObject.parent) {
+ collectThemeJsonsInFrontend(themeJsonContents, themeJsonObject.parent);
+ }
+ }
+ }
+
+ return {
+ name: 'vaadin:stats',
+ enforce: 'post',
+ async writeBundle(options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }) {
+ const modules = Object.values(bundle).flatMap((b) => (b.modules ? Object.keys(b.modules) : []));
+ const nodeModulesFolders = modules
+ .map((id) => id.replace(/\\/g, '/'))
+ .filter((id) => id.startsWith(nodeModulesFolder.replace(/\\/g, '/')))
+ .map((id) => id.substring(nodeModulesFolder.length + 1));
+ const npmModules = nodeModulesFolders
+ .map((id) => id.replace(/\\/g, '/'))
+ .map((id) => {
+ const parts = id.split('/');
+ if (id.startsWith('@')) {
+ return parts[0] + '/' + parts[1];
+ } else {
+ return parts[0];
+ }
+ })
+ .sort()
+ .filter((value, index, self) => self.indexOf(value) === index);
+ const npmModuleAndVersion = Object.fromEntries(npmModules.map((module) => [module, getVersion(module)]));
+ const cvdls = Object.fromEntries(
+ npmModules
+ .filter((module) => getCvdlName(module) != null)
+ .map((module) => [module, { name: getCvdlName(module), version: getVersion(module) }])
+ );
+
+ mkdirSync(path.dirname(statsFile), { recursive: true });
+ const projectPackageJson = JSON.parse(readFileSync(projectPackageJsonFile, { encoding: 'utf-8' }));
+
+ const entryScripts = Object.values(bundle)
+ .filter((bundle) => bundle.isEntry)
+ .map((bundle) => bundle.fileName);
+
+ const generatedIndexHtml = path.resolve(buildOutputFolder, 'index.html');
+ const customIndexData: string = readFileSync(projectIndexHtml, { encoding: 'utf-8' });
+ const generatedIndexData: string = readFileSync(generatedIndexHtml, {
+ encoding: 'utf-8'
+ });
+
+ const customIndexRows = new Set(customIndexData.split(/[\r\n]/).filter((row) => row.trim() !== ''));
+ const generatedIndexRows = generatedIndexData.split(/[\r\n]/).filter((row) => row.trim() !== '');
+
+ const rowsGenerated: string[] = [];
+ generatedIndexRows.forEach((row) => {
+ if (!customIndexRows.has(row)) {
+ rowsGenerated.push(row);
+ }
+ });
+
+ //After dev-bundle build add used Flow frontend imports JsModule/JavaScript/CssImport
+
+ const parseImports = (filename: string, result: Set): void => {
+ const content: string = readFileSync(filename, { encoding: 'utf-8' });
+ const lines = content.split('\n');
+ const staticImports = lines
+ .filter((line) => line.startsWith('import '))
+ .map((line) => line.substring(line.indexOf("'") + 1, line.lastIndexOf("'")))
+ .map((line) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line));
+ const dynamicImports = lines
+ .filter((line) => line.includes('import('))
+ .map((line) => line.replace(/.*import\(/, ''))
+ .map((line) => line.split(/'/)[1])
+ .map((line) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line));
+
+ staticImports.forEach((staticImport) => result.add(staticImport));
+
+ dynamicImports.map((dynamicImport) => {
+ const importedFile = path.resolve(path.dirname(filename), dynamicImport);
+ parseImports(importedFile, result);
+ });
+ };
+
+ const generatedImportsSet = new Set();
+ parseImports(
+ path.resolve(themeOptions.frontendGeneratedFolder, 'flow', 'generated-flow-imports.js'),
+ generatedImportsSet
+ );
+ parseImports(
+ path.resolve(themeOptions.frontendGeneratedFolder, 'app-shell-imports.js'),
+ generatedImportsSet
+ );
+ const generatedImports = Array.from(generatedImportsSet).sort();
+
+ const frontendFiles: Record = {};
+ frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
+
+ const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map'];
+
+ const isThemeComponentsResource = (id: string) =>
+ id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
+ && id.match(/.*\/jar-resources\/themes\/[^\/]+\/components\//);
+
+ const isGeneratedWebComponentResource = (id: string) =>
+ id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
+ && id.match(/.*\/flow\/web-components\//);
+
+ const isFrontendResourceCollected = (id: string) =>
+ !id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
+ || isThemeComponentsResource(id)
+ || isGeneratedWebComponentResource(id);
+
+ // collects project's frontend resources in frontend folder, excluding
+ // 'generated' sub-folder, except for legacy shadow DOM stylesheets
+ // packaged in `theme/components/` folder
+ // and generated web component resources in `flow/web-components` folder.
+ modules
+ .map((id) => id.replace(/\\/g, '/'))
+ .filter((id) => id.startsWith(frontendFolder.replace(/\\/g, '/')))
+ .filter(isFrontendResourceCollected)
+ .map((id) => id.substring(frontendFolder.length + 1))
+ .map((line: string) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line))
+ .forEach((line: string) => {
+ // \r\n from windows made files may be used so change to \n
+ const filePath = path.resolve(frontendFolder, line);
+ if (projectFileExtensions.includes(path.extname(filePath))) {
+ const fileBuffer = readFileSync(filePath, { encoding: 'utf-8' }).replace(/\r\n/g, '\n');
+ frontendFiles[line] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
+ }
+ });
+
+ // collects frontend resources from the JARs
+ generatedImports
+ .filter((line: string) => line.includes('generated/jar-resources'))
+ .forEach((line: string) => {
+ let filename = line.substring(line.indexOf('generated'));
+ // \r\n from windows made files may be used ro remove to be only \n
+ const fileBuffer = readFileSync(path.resolve(frontendFolder, filename), { encoding: 'utf-8' }).replace(
+ /\r\n/g,
+ '\n'
+ );
+ const hash = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
+
+ const fileKey = line.substring(line.indexOf('jar-resources/') + 14);
+ frontendFiles[fileKey] = hash;
+ });
+ // collects and hash rest of the Frontend resources excluding files in /generated/ and /themes/
+ // and files already in frontendFiles.
+ let frontendFolderAlias = "Frontend";
+ generatedImports
+ .filter((line: string) => line.startsWith(frontendFolderAlias + '/'))
+ .filter((line: string) => !line.startsWith(frontendFolderAlias + '/generated/'))
+ .filter((line: string) => !line.startsWith(frontendFolderAlias + '/themes/'))
+ .map((line) => line.substring(frontendFolderAlias.length + 1))
+ .filter((line: string) => !frontendFiles[line])
+ .forEach((line: string) => {
+ const filePath = path.resolve(frontendFolder, line);
+ if (projectFileExtensions.includes(path.extname(filePath)) && existsSync(filePath)) {
+ const fileBuffer = readFileSync(filePath, { encoding: 'utf-8' }).replace(/\r\n/g, '\n');
+ frontendFiles[line] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
+ }
+ });
+ // If a index.ts exists hash it to be able to see if it changes.
+ if (existsSync(path.resolve(frontendFolder, 'index.ts'))) {
+ const fileBuffer = readFileSync(path.resolve(frontendFolder, 'index.ts'), { encoding: 'utf-8' }).replace(
+ /\r\n/g,
+ '\n'
+ );
+ frontendFiles[`index.ts`] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
+ }
+ if (hasCommercialBanner) {
+ const fileBuffer = readFileSync(commercialBannerComponent, { encoding: 'utf-8' }).replace(/\r\n/g, '\n');
+ frontendFiles[settings.generatedFolder + '/commercial-banner.js'] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
+ }
+
+ const themeJsonContents: Record = {};
+ const themesFolder = path.resolve(jarResourcesFolder, 'themes');
+ if (existsSync(themesFolder)) {
+ readdirSync(themesFolder).forEach((themeFolder) => {
+ const themeJson = path.resolve(themesFolder, themeFolder, 'theme.json');
+ if (existsSync(themeJson)) {
+ themeJsonContents[path.basename(themeFolder)] = readFileSync(themeJson, { encoding: 'utf-8' }).replace(
+ /\r\n/g,
+ '\n'
+ );
+ }
+ });
+ }
+
+ collectThemeJsonsInFrontend(themeJsonContents, settings.themeName);
+
+ let webComponents: string[] = [];
+ if (webComponentTags) {
+ webComponents = webComponentTags.split(';');
+ }
+
+ const stats = {
+ packageJsonDependencies: projectPackageJson.dependencies,
+ npmModules: npmModuleAndVersion,
+ bundleImports: generatedImports,
+ frontendHashes: frontendFiles,
+ themeJsonContents: themeJsonContents,
+ entryScripts,
+ webComponents,
+ cvdlModules: cvdls,
+ packageJsonHash: projectPackageJson?.vaadin?.hash,
+ indexHtmlGenerated: rowsGenerated
+ };
+ writeFileSync(statsFile, JSON.stringify(stats, null, 1));
+ }
+ };
+}
+
+function themePlugin(opts: { devMode: boolean }): PluginOption {
+ const fullThemeOptions = { ...themeOptions, devMode: opts.devMode };
+ return {
+ name: 'vaadin:theme',
+ config() {
+ processThemeResources(fullThemeOptions, console);
+ },
+ configureServer(server) {
+ function handleThemeFileCreateDelete(themeFile: string, stats?: Stats) {
+ if (themeFile.startsWith(themeFolder)) {
+ const changed = path.relative(themeFolder, themeFile);
+ console.debug('Theme file ' + (!!stats ? 'created' : 'deleted'), changed);
+ processThemeResources(fullThemeOptions, console);
+ }
+ }
+ server.watcher.on('add', handleThemeFileCreateDelete);
+ server.watcher.on('unlink', handleThemeFileCreateDelete);
+ },
+ hotUpdate({ file }) {
+ const contextPath = path.resolve(file);
+ const themePath = path.resolve(themeFolder);
+ if (contextPath.startsWith(themePath)) {
+ const changed = path.relative(themePath, contextPath);
+
+ console.debug('Theme file changed', changed);
+
+ if (changed.startsWith(settings.themeName)) {
+ processThemeResources(fullThemeOptions, console);
+ }
+ }
+ },
+ async resolveId(id, importer) {
+ // force theme generation if generated theme sources does not yet exist
+ // this may happen for example during Java hot reload when updating
+ // @Theme annotation value
+ if (
+ path.resolve(themeOptions.frontendGeneratedFolder, 'theme.js') === importer &&
+ !existsSync(path.resolve(themeOptions.frontendGeneratedFolder, id))
+ ) {
+ console.debug('Generate theme file ' + id + ' not existing. Processing theme resource');
+ processThemeResources(fullThemeOptions, console);
+ return;
+ }
+ if (!id.startsWith(settings.themeFolder)) {
+ return;
+ }
+ for (const location of [themeResourceFolder, frontendFolder]) {
+ const result = await this.resolve(path.resolve(location, id));
+ if (result) {
+ return result;
+ }
+ }
+ },
+ async transform(raw, id, options) {
+ // rewrite urls for the application theme css files
+ const [bareId, query] = id.split('?');
+ if (
+ (!bareId?.startsWith(themeFolder) && !bareId?.startsWith(themeOptions.themeResourceFolder)) ||
+ !bareId?.endsWith('.css')
+ ) {
+ return;
+ }
+ const resourceThemeFolder = bareId.startsWith(themeFolder) ? themeFolder : themeOptions.themeResourceFolder;
+ const [themeName] = bareId.substring(resourceThemeFolder.length + 1).split('/');
+ return rewriteCssUrls(raw, path.dirname(bareId), path.resolve(resourceThemeFolder, themeName), console, opts);
+ }
+ };
+}
+
+function runWatchDog(watchDogPort: number, watchDogHost: string | undefined) {
+ const client = new net.Socket();
+ client.setEncoding('utf8');
+ client.on('error', function (err) {
+ console.log('Watchdog connection error. Terminating vite process...', err);
+ client.destroy();
+ process.exit(0);
+ });
+ client.on('close', function () {
+ client.destroy();
+ runWatchDog(watchDogPort, watchDogHost);
+ });
+
+ client.connect(watchDogPort, watchDogHost || 'localhost');
+}
+
+const allowedFrontendFolders = [frontendFolder, nodeModulesFolder];
+
+function showRecompileReason(): PluginOption {
+ return {
+ name: 'vaadin:why-you-compile',
+ hotUpdate({ file }) {
+ console.log('Recompiling because', file, 'changed');
+ }
+ };
+}
+
+const DEV_MODE_START_REGEXP = /\/\*[\*!]\s+vaadin-dev-mode:start/;
+const DEV_MODE_CODE_REGEXP = /\/\*[\*!]\s+vaadin-dev-mode:start([\s\S]*)vaadin-dev-mode:end\s+\*\*\//i;
+
+function preserveUsageStats() {
+ return {
+ name: 'vaadin:preserve-usage-stats',
+
+ transform(src: string, id: string) {
+ if (id.includes('vaadin-usage-statistics')) {
+ if (src.includes('vaadin-dev-mode:start')) {
+ const expectedComment = '/*! vaadin-dev-mode:start';
+ const newSrc = src.replace(DEV_MODE_START_REGEXP, expectedComment);
+ if (newSrc === src) {
+ if (!src.includes(expectedComment)) {
+ console.error('vaadin-dev-mode:start tag not found');
+ }
+ } else if (!newSrc.match(DEV_MODE_CODE_REGEXP)) {
+ console.error('New comment fails to match original regexp');
+ } else {
+ return { code: newSrc };
+ }
+ }
+ }
+
+ return { code: src };
+ }
+ };
+}
+
+export const vaadinConfig: UserConfigFn = (env) => {
+ const devMode = env.mode === 'development';
+ const productionMode = !devMode && !devBundle
+ const commercialBanner = productionMode && hasCommercialBanner;
+
+ if (devMode && process.env.watchDogPort) {
+ // Open a connection with the Java dev-mode handler in order to finish
+ // vite when it exits or crashes.
+ runWatchDog(parseInt(process.env.watchDogPort), process.env.watchDogHost);
+ }
+
+ return {
+ root: frontendFolder,
+ base: '',
+ publicDir: false,
+ resolve: {
+ alias: {
+ '@vaadin/flow-frontend': jarResourcesFolder,
+ Frontend: frontendFolder
+ },
+ preserveSymlinks: true
+ },
+ define: {
+ OFFLINE_PATH: settings.offlinePath,
+ VITE_ENABLED: 'true'
+ },
+ server: {
+ host: '127.0.0.1',
+ strictPort: true,
+ fs: {
+ allow: allowedFrontendFolders
+ }
+ },
+ esbuild: {
+ legalComments: 'inline',
+ },
+ build: {
+ minify: productionMode,
+ outDir: buildOutputFolder,
+ emptyOutDir: devBundle,
+ assetsDir: 'VAADIN/build',
+ target,
+ rollupOptions: {
+ input: {
+ indexhtml: projectIndexHtml,
+
+ ...(hasExportedWebComponents ? { webcomponenthtml: path.resolve(frontendFolder, 'web-component.html') } : {})
+ },
+ output: {
+ // Workaround to enable dynamic imports with top-level await for
+ // commonjs modules, such as "atmosphere.js" in Hilla. Extracting
+ // Rollup's commonjs helpers into separate manual chunk avoids
+ // circular dependencies in this case. Caused
+ // - https://github.com/vitejs/vite/issues/10995
+ // - https://github.com/rollup/rollup/issues/5884
+ // - https://github.com/vitejs/vite/issues/19695
+ // - https://github.com/vitejs/vite/issues/12209
+ manualChunks: (id: string) => id.startsWith('\0commonjsHelpers.js') ? 'commonjsHelpers' : null
+ },
+ onwarn: (warning: any, defaultHandler: (warning: any) => void) => {
+ const ignoreEvalWarning = [
+ 'generated/jar-resources/FlowClient.js',
+ 'generated/jar-resources/vaadin-spreadsheet/spreadsheet-export.js',
+ '@vaadin/charts/src/helpers.js'
+ ];
+ if (warning.code === 'EVAL' && warning.id && !!ignoreEvalWarning.find((id) => warning.id?.endsWith(id))) {
+ return;
+ }
+ defaultHandler(warning);
+ }
+ }
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ target,
+ },
+ entries: [
+ // Pre-scan entrypoints in Vite to avoid reloading on first open
+ 'generated/vaadin.ts'
+ ],
+ exclude: [
+ '@vaadin/router',
+ '@vaadin/vaadin-license-checker',
+ '@vaadin/vaadin-usage-statistics',
+ 'workbox-core',
+ 'workbox-precaching',
+ 'workbox-routing',
+ 'workbox-strategies'
+ ]
+ },
+ plugins: [
+ productionMode && brotli(),
+ devMode && showRecompileReason(),
+
+ !devMode && statsExtracterPlugin(),
+ !productionMode && preserveUsageStats(),
+ themePlugin({ devMode }),
+ postcssLit({
+ include: ['**/*.css', /.*\/.*\.css\?.*/],
+ exclude: [
+ `${themeFolder}/**/*.css`,
+ new RegExp(`${themeFolder}/.*/.*\\.css\\?.*`),
+ `${themeResourceFolder}/**/*.css`,
+ new RegExp(`${themeResourceFolder}/.*/.*\\.css\\?.*`),
+ new RegExp('.*/.*\\?html-proxy.*')
+ ]
+ }),
+ // The React plugin provides fast refresh and debug source info
+ reactPlugin({
+ include: '**/*.tsx',
+ babel: {
+ // We need to use babel to provide the source information for it to be correct
+ // (otherwise Babel will slightly rewrite the source file and esbuild generate source info for the modified file)
+ presets: [
+ [
+ '@babel/preset-react',
+ {
+ runtime: 'automatic',
+ importSource: productionMode ? 'react' : 'Frontend/generated/jsx-dev-transform',
+ development: !productionMode
+ }
+ ]
+ ],
+ // React writes the source location for where components are used, this writes for where they are defined
+ plugins: [
+ !productionMode && addFunctionComponentSourceLocationBabel(),
+ [
+ 'module:@preact/signals-react-transform',
+ {
+ mode: 'all' // Needed to include translations which do not use something.value
+ }
+ ]
+ ].filter(Boolean)
+ }
+ }),
+
+ productionMode && vaadinI18n({
+ cwd: __dirname,
+ meta: {
+ output: {
+ dir: i18nFolder,
+ },
+ },
+ }),
+ {
+ name: 'vaadin:force-remove-html-middleware',
+ configureServer(server) {
+ return () => {
+ server.middlewares.stack = server.middlewares.stack.filter((mw) => {
+ const handleName = `${mw.handle}`;
+ return !handleName.includes('viteHtmlFallbackMiddleware');
+ });
+ };
+ },
+ },
+ hasExportedWebComponents && {
+ name: 'vaadin:inject-entrypoints-to-web-component-html',
+ transformIndexHtml: {
+ order: 'pre',
+ handler(_html, { path, server }) {
+ if (path !== '/web-component.html') {
+ return;
+ }
+ const scripts = [
+ {
+ tag: 'script',
+ attrs: { type: 'module', src: `/generated/vaadin-web-component.ts` },
+ injectTo: 'head'
+ }
+ ];
+ if (commercialBanner) {
+ scripts.push({
+ tag: 'script',
+ attrs: { type: 'module', src: '/generated/commercial-banner.js' },
+ injectTo: 'head'
+ });
+ }
+ return scripts;
+ }
+ }
+ },
+ {
+ name: 'vaadin:inject-entrypoints-to-index-html',
+ transformIndexHtml: {
+ order: 'pre',
+ handler(_html, { path, server }) {
+ if (path !== '/index.html') {
+ return;
+ }
+
+ const scripts = [];
+
+ if (devMode) {
+ scripts.push({
+ tag: 'script',
+ attrs: { type: 'module', src: `/generated/vite-devmode.ts`, onerror: "document.location.reload()" },
+ injectTo: 'head'
+ });
+ }
+ scripts.push({
+ tag: 'script',
+ attrs: { type: 'module', src: '/generated/vaadin.ts' },
+ injectTo: 'head'
+ });
+ if (commercialBanner) {
+ scripts.push({
+ tag: 'script',
+ attrs: { type: 'module', src: '/generated/commercial-banner.js' },
+ injectTo: 'head'
+ });
+ }
+ return scripts;
+ }
+ }
+ },
+
+ checker({
+ typescript: true
+ }),
+ productionMode && visualizer({ brotliSize: true, filename: bundleSizeFile })
+ ]
+ };
+};
+
+export const overrideVaadinConfig = (customConfig: UserConfigFn) => {
+ return defineConfig((env) => mergeConfig(vaadinConfig(env), customConfig(env)));
+};
+function getVersion(module: string): string {
+ const packageJson = path.resolve(nodeModulesFolder, module, 'package.json');
+ return JSON.parse(readFileSync(packageJson, { encoding: 'utf-8' })).version;
+}
+function getCvdlName(module: string): string {
+ const packageJson = path.resolve(nodeModulesFolder, module, 'package.json');
+ return JSON.parse(readFileSync(packageJson, { encoding: 'utf-8' })).cvdlName;
+}