Rechnungsmaske: ZIP-Auto-Download, löschbare Vorbelegungen, Icon- und Textausrichtungs-Korrekturen
- Valides ZIP startet den Download direkt; der sichtbare Download-Link entfällt - Vorbelegung laden: Löschen-Icon rechts an jedem Eintrag, Löschung aus MongoDB nach Sicherheitsabfrage - Template-Wechsel leert die Rechnungssteller-Felder nicht mehr, damit "Eingaben speichern" aktiv wird - Aktionsspalten der Grids mit fester Breite, damit die Icons nicht abgeschnitten werden - Texte im PDF-Renderer und Designer-Canvas oben statt zentriert ausgerichtet, damit mehrzeilige Variablenwerte auf Höhe ihrer Nachbar-Elemente beginnen - Vaadin-Frontend-Scaffolding (package.json, tsconfig, vite-Konfiguration) aufgenommen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+9537
File diff suppressed because it is too large
Load Diff
+104
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "no-name",
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@vaadin/bundles": "24.7.6",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/polymer-legacy-adapter": "24.7.6",
|
||||
"@vaadin/react-components": "24.7.6",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.7.6",
|
||||
"@vaadin/vaadin-material-styles": "24.7.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.6",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"date-fns": "2.29.3",
|
||||
"lit": "3.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router": "7.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@preact/signals-react-transform": "0.5.1",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@types/react": "18.3.20",
|
||||
"@types/react-dom": "18.3.6",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"async": "3.2.6",
|
||||
"glob": "11.0.2",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.3.4",
|
||||
"vite-plugin-checker": "0.9.1",
|
||||
"workbox-build": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
},
|
||||
"vaadin": {
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@vaadin/bundles": "24.7.6",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/polymer-legacy-adapter": "24.7.6",
|
||||
"@vaadin/react-components": "24.7.6",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.7.6",
|
||||
"@vaadin/vaadin-material-styles": "24.7.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.6",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"date-fns": "2.29.3",
|
||||
"lit": "3.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router": "7.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@preact/signals-react-transform": "0.5.1",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@types/react": "18.3.20",
|
||||
"@types/react-dom": "18.3.6",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"async": "3.2.6",
|
||||
"glob": "11.0.2",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.3.4",
|
||||
"vite-plugin-checker": "0.9.1",
|
||||
"workbox-build": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
},
|
||||
"hash": "79560e4ddc395b0ad64481d9a3f56513be65cec5fb700f13f4c25c4dd181fa94"
|
||||
},
|
||||
"overrides": {
|
||||
"@vaadin/bundles": "$@vaadin/bundles",
|
||||
"@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter",
|
||||
"@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
|
||||
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
||||
"@vaadin/react-components": "$@vaadin/react-components",
|
||||
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
||||
"react-dom": "$react-dom",
|
||||
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
||||
"lit": "$lit",
|
||||
"@polymer/polymer": "$@polymer/polymer",
|
||||
"react": "$react",
|
||||
"react-router": "$react-router",
|
||||
"date-fns": "$date-fns",
|
||||
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
||||
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
||||
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -345,10 +345,10 @@ window.initProfileInvoiceGenerator = function() {
|
||||
// Text elements
|
||||
var lines = (el.text || '').split('\n');
|
||||
var lineHeight = fontSize * 1.2;
|
||||
var totalTextHeight = lines.length * lineHeight;
|
||||
|
||||
// Vertically center the text in the element
|
||||
var ty = y + (h - totalTextHeight) / 2;
|
||||
// Oben ausrichten statt zentrieren — wie im gerenderten PDF, damit die
|
||||
// erste Zeile unabhängig von der Zeilenzahl an der Box-Oberkante beginnt
|
||||
var ty = y;
|
||||
|
||||
// Masterdata elements are never bold; other elements respect their fontStyle
|
||||
var fontWeight = (el.isStatic && !el.isCustomer) ? '' : (el.fontStyle || '');
|
||||
|
||||
@@ -36,6 +36,11 @@ public class InvoiceDraftService {
|
||||
return repository.existsByName(name);
|
||||
}
|
||||
|
||||
/** Löscht die unter dem Namen gespeicherten Eingaben; ohne Treffer passiert nichts. */
|
||||
public void delete(String name) {
|
||||
repository.findByName(name).ifPresent(existing -> repository.deleteById(existing.id()));
|
||||
}
|
||||
|
||||
/** @return die Namen aller gespeicherten Eingaben, alphabetisch sortiert. */
|
||||
public List<String> names() {
|
||||
return repository.findAll().stream()
|
||||
|
||||
@@ -165,7 +165,10 @@ public class TemplatePdfService {
|
||||
if ("services.list".equals(variable)) {
|
||||
htmlBuilder.append("display:block;overflow:visible;padding:0;");
|
||||
} else {
|
||||
htmlBuilder.append("display:flex;align-items:center;");
|
||||
// Oben ausrichten statt zentrieren: Die erste Zeile muss an der
|
||||
// Box-Oberkante beginnen, sonst verschiebt sich mehrzeiliger
|
||||
// Variablentext gegenüber einzeiligen Nachbar-Elementen.
|
||||
htmlBuilder.append("display:flex;align-items:flex-start;");
|
||||
switch (textAlign) {
|
||||
case "center":
|
||||
htmlBuilder.append("justify-content:center;text-align:center;");
|
||||
|
||||
@@ -85,6 +85,8 @@ public class AddressBookView extends VerticalLayout {
|
||||
.setAutoWidth(true);
|
||||
grid.addColumn(address -> address.getName()).setHeader("Firma / Name").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(address -> address.getStreet()).setHeader("Straße").setAutoWidth(true);
|
||||
// Feste Breite statt AutoWidth: Vaadin misst Komponentenspalten vor dem
|
||||
// Rendern der Buttons, dadurch würden die Icons rechts abgeschnitten.
|
||||
grid.addComponentColumn(address -> {
|
||||
Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openEditDialog(address));
|
||||
edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
@@ -93,8 +95,11 @@ public class AddressBookView extends VerticalLayout {
|
||||
delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR,
|
||||
ButtonVariant.LUMO_SMALL);
|
||||
delete.setTooltipText("Löschen");
|
||||
return new HorizontalLayout(edit, delete);
|
||||
}).setHeader("").setFlexGrow(0).setAutoWidth(true);
|
||||
HorizontalLayout actions = new HorizontalLayout(edit, delete);
|
||||
actions.setSpacing(false);
|
||||
actions.setPadding(false);
|
||||
return actions;
|
||||
}).setHeader("").setFlexGrow(0).setWidth("110px");
|
||||
grid.addItemDoubleClickListener(event -> openEditDialog(event.getItem()));
|
||||
grid.setSizeFull();
|
||||
add(grid);
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
@@ -24,6 +25,7 @@ import com.vaadin.flow.component.textfield.EmailField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.component.upload.Upload;
|
||||
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
@@ -143,6 +145,25 @@ public class MainView extends VerticalLayout {
|
||||
draftSelect.setPlaceholder("Vorbelegung laden");
|
||||
draftSelect.setClearButtonVisible(true);
|
||||
draftSelect.setWidth("300px");
|
||||
// Jeder Eintrag zeigt rechts ein Löschen-Icon, das die Vorbelegung
|
||||
// nach einer Sicherheitsabfrage aus MongoDB entfernt.
|
||||
draftSelect.setRenderer(new ComponentRenderer<>(name -> {
|
||||
Span label = new Span(name);
|
||||
Icon delete = VaadinIcon.TRASH.create();
|
||||
delete.setSize("var(--lumo-icon-size-s)");
|
||||
delete.getStyle().set("color", "var(--lumo-error-color)").set("cursor", "pointer");
|
||||
delete.getElement().setAttribute("title", "Vorbelegung löschen");
|
||||
// Der Klick auf das Icon darf den Eintrag nicht auswählen; der
|
||||
// Server-Listener auf dem Icon selbst wird davon nicht berührt.
|
||||
delete.getElement().executeJs("this.addEventListener('click', function(e) { e.stopPropagation(); })");
|
||||
delete.getElement().addEventListener("click", e -> confirmDeleteDraft(name));
|
||||
HorizontalLayout row = new HorizontalLayout(label, delete);
|
||||
row.setJustifyContentMode(JustifyContentMode.BETWEEN);
|
||||
row.setAlignItems(Alignment.CENTER);
|
||||
row.setWidthFull();
|
||||
row.setPadding(false);
|
||||
return row;
|
||||
}));
|
||||
draftSelect.addValueChangeListener(event -> {
|
||||
if (event.isFromClient() && event.getValue() != null) {
|
||||
loadDraft(event.getValue());
|
||||
@@ -181,9 +202,14 @@ public class MainView extends VerticalLayout {
|
||||
// Entweder PDF oder Template: Auswahl entfernt ein hochgeladenes PDF.
|
||||
upload.clearFileList();
|
||||
uploadedPdf = null;
|
||||
// Rechnungssteller-Felder leer beginnen lassen.
|
||||
senderAddressSelect.clear();
|
||||
sender.clearFields();
|
||||
// Rechnungssteller-Felder nur bei der ersten Template-Auswahl
|
||||
// leer beginnen lassen; beim Wechsel zwischen Templates bleiben
|
||||
// die Eingaben erhalten (sonst wäre u. a. der Speichern-Button
|
||||
// wegen leerer Pflichtfelder nie aktiv).
|
||||
if (event.getOldValue() == null) {
|
||||
senderAddressSelect.clear();
|
||||
sender.clearFields();
|
||||
}
|
||||
}
|
||||
updateControlsEnabled();
|
||||
});
|
||||
@@ -226,8 +252,11 @@ public class MainView extends VerticalLayout {
|
||||
recipientAddressSelect, recipient, itemsGrid, addItem, generate));
|
||||
updateControlsEnabled();
|
||||
|
||||
// Unsichtbarer Anchor: dient nur als Träger für den automatischen
|
||||
// Download nach dem Erzeugen (display:none statt setVisible, damit
|
||||
// callJsFunction("click") den Klick sofort ausführen kann).
|
||||
downloadLink.getElement().setAttribute("download", true);
|
||||
downloadLink.setVisible(false);
|
||||
downloadLink.getStyle().set("display", "none");
|
||||
|
||||
add(new HorizontalLayout(generate, saveDraftButton, downloadLink));
|
||||
|
||||
@@ -290,6 +319,39 @@ public class MainView extends VerticalLayout {
|
||||
confirm.open();
|
||||
}
|
||||
|
||||
/** Sicherheitsabfrage vor dem endgültigen Löschen einer Vorbelegung. */
|
||||
private void confirmDeleteDraft(String name) {
|
||||
ConfirmDialog confirm = new ConfirmDialog();
|
||||
confirm.setHeader("Vorbelegung löschen?");
|
||||
confirm.setText("Sollen die gespeicherten Eingaben \"" + name + "\" endgültig gelöscht werden?");
|
||||
confirm.setConfirmText("Löschen");
|
||||
confirm.setConfirmButtonTheme("error primary");
|
||||
confirm.setCancelable(true);
|
||||
confirm.setCancelText("Abbrechen");
|
||||
confirm.addConfirmListener(event -> deleteDraft(name));
|
||||
confirm.open();
|
||||
}
|
||||
|
||||
private void deleteDraft(String name) {
|
||||
try {
|
||||
invoiceDraftService.delete(name);
|
||||
} catch (Exception ex) {
|
||||
showErrors(List.of("Die Vorbelegung \"" + name + "\" konnte nicht gelöscht werden: " + ex.getMessage()));
|
||||
return;
|
||||
}
|
||||
boolean wasSelected = name.equals(draftSelect.getValue());
|
||||
draftSelect.setItems(invoiceDraftService.names());
|
||||
if (wasSelected) {
|
||||
// Die geladene Vorbelegung existiert nicht mehr; das Formular
|
||||
// behält seine Werte, gilt aber wieder als ungespeichert.
|
||||
draftSelect.clear();
|
||||
loadedDraftState = null;
|
||||
updateSaveDraftEnabled();
|
||||
}
|
||||
Notification.show("Vorbelegung \"" + name + "\" gelöscht.", 3000, Notification.Position.BOTTOM_START)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
}
|
||||
|
||||
private void saveDraft(String name) {
|
||||
try {
|
||||
invoiceDraftService.save(buildDraft(name));
|
||||
@@ -412,6 +474,8 @@ public class MainView extends VerticalLayout {
|
||||
.setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
|
||||
itemsGrid.addColumn(item -> formatDecimal(item.vatPercent())).setHeader("USt %")
|
||||
.setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
|
||||
// Feste Breite statt AutoWidth: Vaadin misst Komponentenspalten vor dem
|
||||
// Rendern der Buttons, dadurch würden die Icons rechts abgeschnitten.
|
||||
itemsGrid.addComponentColumn(item -> {
|
||||
Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openItemDialog(item));
|
||||
edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
|
||||
@@ -423,8 +487,11 @@ public class MainView extends VerticalLayout {
|
||||
delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR,
|
||||
ButtonVariant.LUMO_SMALL);
|
||||
delete.setTooltipText("Löschen");
|
||||
return new HorizontalLayout(edit, delete);
|
||||
}).setHeader("").setAutoWidth(true).setFlexGrow(0);
|
||||
HorizontalLayout actions = new HorizontalLayout(edit, delete);
|
||||
actions.setSpacing(false);
|
||||
actions.setPadding(false);
|
||||
return actions;
|
||||
}).setHeader("").setWidth("110px").setFlexGrow(0);
|
||||
itemsGrid.addItemDoubleClickListener(event -> openItemDialog(event.getItem()));
|
||||
itemsGrid.setAllRowsVisible(true);
|
||||
itemsGrid.setWidthFull();
|
||||
@@ -561,7 +628,6 @@ public class MainView extends VerticalLayout {
|
||||
}
|
||||
|
||||
private void generate() {
|
||||
downloadLink.setVisible(false);
|
||||
List<String> errors = validateForm();
|
||||
if (!errors.isEmpty()) {
|
||||
showErrors(errors);
|
||||
@@ -642,13 +708,12 @@ public class MainView extends VerticalLayout {
|
||||
() -> new ByteArrayInputStream(result.zip()));
|
||||
resource.setContentType("application/zip");
|
||||
downloadLink.setHref(resource);
|
||||
downloadLink.setText("ZIP herunterladen (" + result.zipFileName() + ")");
|
||||
downloadLink.setVisible(true);
|
||||
|
||||
if (result.valid()) {
|
||||
Notification.show("E-Rechnung erzeugt und erfolgreich validiert.", 4000,
|
||||
Notification.Position.BOTTOM_START)
|
||||
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||
downloadLink.getElement().callJsFunction("click");
|
||||
} else {
|
||||
Notification.show("E-Rechnung erzeugt, aber die Validierung meldet Probleme. "
|
||||
+ "Details stehen im Prüfbericht (validation-report.xml) im ZIP.", 8000,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// This TypeScript configuration file is generated by vaadin-maven-plugin.
|
||||
// This is needed for TypeScript compiler to compile your TypeScript code in the project.
|
||||
// It is recommended to commit this file to the VCS.
|
||||
// You might want to change the configurations to fit your preferences
|
||||
// For more information about the configurations, please refer to http://www.typescriptlang.org/docs/handbook/tsconfig-json.html
|
||||
{
|
||||
"_version": "9.1",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"inlineSources": true,
|
||||
"module": "esNext",
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"baseUrl": "src/main/frontend",
|
||||
"paths": {
|
||||
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
||||
"@vaadin/flow-frontend/*": ["generated/jar-resources/*"],
|
||||
"Frontend/*": ["*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/main/frontend/**/*",
|
||||
"types.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/main/frontend/generated/jar-resources/**"
|
||||
]
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
// This TypeScript modules definition file is generated by vaadin-maven-plugin.
|
||||
// You can not directly import your different static files into TypeScript,
|
||||
// This is needed for TypeScript compiler to declare and export as a TypeScript module.
|
||||
// It is recommended to commit this file to the VCS.
|
||||
// You might want to change the configurations to fit your preferences
|
||||
declare module '*.css?inline' {
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
const content: CSSResultGroup;
|
||||
export default content;
|
||||
}
|
||||
|
||||
// Allow any CSS Custom Properties
|
||||
declare module 'csstype' {
|
||||
interface Properties {
|
||||
[index: `--${string}`]: any;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,858 @@
|
||||
/**
|
||||
* 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,
|
||||
build,
|
||||
defineConfig,
|
||||
mergeConfig,
|
||||
OutputOptions,
|
||||
PluginOption,
|
||||
InlineConfig,
|
||||
UserConfigFn
|
||||
} from 'vite';
|
||||
import { getManifest, type ManifestTransform } from 'workbox-build';
|
||||
|
||||
import * as rollup from 'rollup';
|
||||
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 { createRequire } from 'module';
|
||||
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import reactPlugin from '@vitejs/plugin-react';
|
||||
|
||||
|
||||
|
||||
// Make `require` compatible with ES modules
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const appShellUrl = '.';
|
||||
|
||||
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 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)
|
||||
};
|
||||
|
||||
const hasExportedWebComponents = existsSync(path.resolve(frontendFolder, 'web-component.html'));
|
||||
|
||||
// Block debug and trace logs.
|
||||
console.trace = () => {};
|
||||
console.debug = () => {};
|
||||
|
||||
function injectManifestToSWPlugin(): rollup.Plugin {
|
||||
const rewriteManifestIndexHtmlUrl: ManifestTransform = (manifest) => {
|
||||
const indexEntry = manifest.find((entry) => entry.url === 'index.html');
|
||||
if (indexEntry) {
|
||||
indexEntry.url = appShellUrl;
|
||||
}
|
||||
|
||||
return { manifest, warnings: [] };
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'vaadin:inject-manifest-to-sw',
|
||||
async transform(code, id) {
|
||||
if (/sw\.(ts|js)$/.test(id)) {
|
||||
const { manifestEntries } = await getManifest({
|
||||
globDirectory: buildOutputFolder,
|
||||
globPatterns: ['**/*'],
|
||||
globIgnores: ['**/*.br', 'pwa-icons/**'],
|
||||
manifestTransforms: [rewriteManifestIndexHtmlUrl],
|
||||
maximumFileSizeToCacheInBytes: 100 * 1024 * 1024 // 100mb,
|
||||
});
|
||||
|
||||
return code.replace('self.__WB_MANIFEST', JSON.stringify(manifestEntries));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildSWPlugin(opts: { devMode: boolean }): PluginOption {
|
||||
let buildConfig: InlineConfig;
|
||||
let buildOutput: rollup.RollupOutput;
|
||||
const devMode = opts.devMode;
|
||||
|
||||
return {
|
||||
name: 'vaadin:build-sw',
|
||||
enforce: 'post',
|
||||
async configResolved(viteConfig) {
|
||||
buildConfig = {
|
||||
base: viteConfig.base,
|
||||
root: viteConfig.root,
|
||||
mode: viteConfig.mode,
|
||||
resolve: viteConfig.resolve,
|
||||
define: {
|
||||
...viteConfig.define,
|
||||
'process.env.NODE_ENV': JSON.stringify(viteConfig.mode),
|
||||
},
|
||||
build: {
|
||||
write: !devMode,
|
||||
minify: viteConfig.build.minify,
|
||||
outDir: viteConfig.build.outDir,
|
||||
sourcemap: viteConfig.command === 'serve' || viteConfig.build.sourcemap,
|
||||
emptyOutDir: false,
|
||||
modulePreload: false,
|
||||
target: ['safari15', 'es2022'],
|
||||
rollupOptions: {
|
||||
input: {
|
||||
sw: settings.clientServiceWorkerSource
|
||||
},
|
||||
output: {
|
||||
exports: 'none',
|
||||
entryFileNames: 'sw.js',
|
||||
inlineDynamicImports: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
async buildStart() {
|
||||
if (devMode) {
|
||||
buildOutput = await build(buildConfig) as rollup.RollupOutput;
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id.endsWith('sw.js')) {
|
||||
return buildOutput.output[0].code;
|
||||
}
|
||||
},
|
||||
async closeBundle() {
|
||||
if (!devMode) {
|
||||
await build({
|
||||
...buildConfig,
|
||||
plugins: [injectManifestToSWPlugin(), brotli()]
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function statsExtracterPlugin(): PluginOption {
|
||||
function collectThemeJsonsInFrontend(themeJsonContents: Record<string, string>, 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<string>): 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<string>();
|
||||
parseImports(
|
||||
path.resolve(themeOptions.frontendGeneratedFolder, 'flow', 'generated-flow-imports.js'),
|
||||
generatedImportsSet
|
||||
);
|
||||
const generatedImports = Array.from(generatedImportsSet).sort();
|
||||
|
||||
const frontendFiles: Record<string, string> = {};
|
||||
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');
|
||||
}
|
||||
|
||||
const themeJsonContents: Record<string, string> = {};
|
||||
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 vaadinBundlesPlugin(): PluginOption {
|
||||
type ExportInfo =
|
||||
| string
|
||||
| {
|
||||
namespace?: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
type ExposeInfo = {
|
||||
exports: ExportInfo[];
|
||||
};
|
||||
|
||||
type PackageInfo = {
|
||||
version: string;
|
||||
exposes: Record<string, ExposeInfo>;
|
||||
};
|
||||
|
||||
type BundleJson = {
|
||||
packages: Record<string, PackageInfo>;
|
||||
};
|
||||
|
||||
const disabledMessage = 'Vaadin component dependency bundles are disabled.';
|
||||
|
||||
const modulesDirectory = nodeModulesFolder.replace(/\\/g, '/');
|
||||
|
||||
let vaadinBundleJson: BundleJson;
|
||||
|
||||
function parseModuleId(id: string): { packageName: string; modulePath: string } {
|
||||
const [scope, scopedPackageName] = id.split('/', 3);
|
||||
const packageName = scope.startsWith('@') ? `${scope}/${scopedPackageName}` : scope;
|
||||
const modulePath = `.${id.substring(packageName.length)}`;
|
||||
return {
|
||||
packageName,
|
||||
modulePath
|
||||
};
|
||||
}
|
||||
|
||||
function getExports(id: string): string[] | undefined {
|
||||
const { packageName, modulePath } = parseModuleId(id);
|
||||
const packageInfo = vaadinBundleJson.packages[packageName];
|
||||
|
||||
if (!packageInfo) return;
|
||||
|
||||
const exposeInfo: ExposeInfo = packageInfo.exposes[modulePath];
|
||||
if (!exposeInfo) return;
|
||||
|
||||
const exportsSet = new Set<string>();
|
||||
for (const e of exposeInfo.exports) {
|
||||
if (typeof e === 'string') {
|
||||
exportsSet.add(e);
|
||||
} else {
|
||||
const { namespace, source } = e;
|
||||
if (namespace) {
|
||||
exportsSet.add(namespace);
|
||||
} else {
|
||||
const sourceExports = getExports(source);
|
||||
if (sourceExports) {
|
||||
sourceExports.forEach((e) => exportsSet.add(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(exportsSet);
|
||||
}
|
||||
|
||||
function getExportBinding(binding: string) {
|
||||
return binding === 'default' ? '_default as default' : binding;
|
||||
}
|
||||
|
||||
function getImportAssigment(binding: string) {
|
||||
return binding === 'default' ? 'default: _default' : binding;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vaadin:bundles',
|
||||
enforce: 'pre',
|
||||
apply(config, { command }) {
|
||||
if (command !== 'serve') return false;
|
||||
|
||||
try {
|
||||
const vaadinBundleJsonPath = require.resolve('@vaadin/bundles/vaadin-bundle.json');
|
||||
vaadinBundleJson = JSON.parse(readFileSync(vaadinBundleJsonPath, { encoding: 'utf8' }));
|
||||
} catch (e: unknown) {
|
||||
if (typeof e === 'object' && (e as { code: string }).code === 'MODULE_NOT_FOUND') {
|
||||
vaadinBundleJson = { packages: {} };
|
||||
console.info(`@vaadin/bundles npm package is not found, ${disabledMessage}`);
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const versionMismatches: Array<{ name: string; bundledVersion: string; installedVersion: string }> = [];
|
||||
for (const [name, packageInfo] of Object.entries(vaadinBundleJson.packages)) {
|
||||
let installedVersion: string | undefined = undefined;
|
||||
try {
|
||||
const { version: bundledVersion } = packageInfo;
|
||||
const installedPackageJsonFile = path.resolve(modulesDirectory, name, 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(installedPackageJsonFile, { encoding: 'utf8' }));
|
||||
installedVersion = packageJson.version;
|
||||
if (installedVersion && installedVersion !== bundledVersion) {
|
||||
versionMismatches.push({
|
||||
name,
|
||||
bundledVersion,
|
||||
installedVersion
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore package not found
|
||||
}
|
||||
}
|
||||
if (versionMismatches.length) {
|
||||
console.info(`@vaadin/bundles has version mismatches with installed packages, ${disabledMessage}`);
|
||||
console.info(`Packages with version mismatches: ${JSON.stringify(versionMismatches, undefined, 2)}`);
|
||||
vaadinBundleJson = { packages: {} };
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async config(config) {
|
||||
return mergeConfig(
|
||||
{
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
// Vaadin bundle
|
||||
'@vaadin/bundles',
|
||||
...Object.keys(vaadinBundleJson.packages),
|
||||
'@vaadin/vaadin-material-styles'
|
||||
]
|
||||
}
|
||||
},
|
||||
config
|
||||
);
|
||||
},
|
||||
load(rawId) {
|
||||
const [path, params] = rawId.split('?');
|
||||
if (!path.startsWith(modulesDirectory)) return;
|
||||
|
||||
const id = path.substring(modulesDirectory.length + 1);
|
||||
const bindings = getExports(id);
|
||||
if (bindings === undefined) return;
|
||||
|
||||
const cacheSuffix = params ? `?${params}` : '';
|
||||
const bundlePath = `@vaadin/bundles/vaadin.js${cacheSuffix}`;
|
||||
|
||||
return `import { init as VaadinBundleInit, get as VaadinBundleGet } from '${bundlePath}';
|
||||
await VaadinBundleInit('default');
|
||||
const { ${bindings.map(getImportAssigment).join(', ')} } = (await VaadinBundleGet('./node_modules/${id}'))();
|
||||
export { ${bindings.map(getExportBinding).join(', ')} };`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
handleHotUpdate(context) {
|
||||
const contextPath = path.resolve(context.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',
|
||||
handleHotUpdate(context) {
|
||||
console.log('Recompiling because', context.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 newSrc = src.replace(DEV_MODE_START_REGEXP, '/*! vaadin-dev-mode:start');
|
||||
if (newSrc === src) {
|
||||
console.error('Comment replacement failed to change anything');
|
||||
} 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
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
build: {
|
||||
minify: productionMode,
|
||||
outDir: buildOutputFolder,
|
||||
emptyOutDir: devBundle,
|
||||
assetsDir: 'VAADIN/build',
|
||||
target: ['safari15', 'es2022'],
|
||||
rollupOptions: {
|
||||
input: {
|
||||
indexhtml: projectIndexHtml,
|
||||
|
||||
...(hasExportedWebComponents ? { webcomponenthtml: path.resolve(frontendFolder, 'web-component.html') } : {})
|
||||
},
|
||||
onwarn: (warning: rollup.RollupLog, defaultHandler: rollup.LoggingFunction) => {
|
||||
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: {
|
||||
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 && vaadinBundlesPlugin(),
|
||||
devMode && showRecompileReason(),
|
||||
settings.offlineEnabled && buildSWPlugin({ devMode }),
|
||||
!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)
|
||||
}
|
||||
}),
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: { type: 'module', src: `/generated/vaadin-web-component.ts` },
|
||||
injectTo: 'head'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
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'
|
||||
});
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user