Compare commits

...
4 Commits
Author SHA1 Message Date
SvenandClaude Fable 5 fd71c272fa PDF-Upload aus der Rechnungsmaske entfernt; Rechnungen entstehen in der UI nur noch aus Templates
Die REST-API (/api/invoices/sign, /validate) nimmt unverändert PDFs entgegen
und bietet weiterhin keinen Template-Zugriff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:31:29 +02:00
SvenandClaude Fable 5 fa0d9da2b1 Beispiel-Hinweise unter den Eingabefeldern entfernt (USt-IdNr., Webseite, IBAN, BIC)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:21:32 +02:00
SvenandClaude Fable 5 28f62cee06 Telefon und Webseite des Rechnungsstellers: Adressbuch-Feld, Formularfelder, Belegung von masterdata.phone/website
- Adressbuch: neues Feld "Webseite" pro Unternehmen
- Rechnungsmaske: Telefon und Webseite beim Rechnungssteller manuell erfassbar, Vorbelegung aus dem Adressbuch
- masterdata.phone/masterdata.website werden aus den Formularfeldern belegt, mit Adressbuch-Fallback (bisher immer leer)
- Webseite wird in Vorbelegungen mitgespeichert, Telefon fließt über Party.phone in die ZUGFeRD-Daten

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:13:40 +02:00
SvenandClaude Fable 5 6838ce3b20 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>
2026-07-17 09:56:30 +02:00
14 changed files with 10734 additions and 77 deletions
+9537
View File
File diff suppressed because it is too large Load Diff
+104
View File
@@ -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 // Text elements
var lines = (el.text || '').split('\n'); var lines = (el.text || '').split('\n');
var lineHeight = fontSize * 1.2; var lineHeight = fontSize * 1.2;
var totalTextHeight = lines.length * lineHeight;
// Oben ausrichten statt zentrieren — wie im gerenderten PDF, damit die
// Vertically center the text in the element // erste Zeile unabhängig von der Zeilenzahl an der Box-Oberkante beginnt
var ty = y + (h - totalTextHeight) / 2; var ty = y;
// Masterdata elements are never bold; other elements respect their fontStyle // Masterdata elements are never bold; other elements respect their fontStyle
var fontWeight = (el.isStatic && !el.isCustomer) ? '' : (el.fontStyle || ''); var fontWeight = (el.isStatic && !el.isCustomer) ? '' : (el.fontStyle || '');
@@ -25,6 +25,7 @@ public class Address {
private String vatId; private String vatId;
private String email; private String email;
private String phone; private String phone;
private String website;
// Bankverbindung (v. a. für Rechnungssteller relevant) // Bankverbindung (v. a. für Rechnungssteller relevant)
private String bankName; private String bankName;
@@ -119,6 +120,14 @@ public class Address {
this.phone = phone; this.phone = phone;
} }
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
public String getBankName() { public String getBankName() {
return bankName; return bankName;
} }
@@ -27,6 +27,7 @@ public record InvoiceDraft(
String buyerReference, String buyerReference,
String iban, String iban,
String bic, String bic,
String website,
InvoiceMetadata.Party sender, InvoiceMetadata.Party sender,
InvoiceMetadata.Party recipient, InvoiceMetadata.Party recipient,
List<InvoiceMetadata.LineItem> items, List<InvoiceMetadata.LineItem> items,
@@ -36,6 +36,11 @@ public class InvoiceDraftService {
return repository.existsByName(name); 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. */ /** @return die Namen aller gespeicherten Eingaben, alphabetisch sortiert. */
public List<String> names() { public List<String> names() {
return repository.findAll().stream() return repository.findAll().stream()
@@ -165,7 +165,10 @@ public class TemplatePdfService {
if ("services.list".equals(variable)) { if ("services.list".equals(variable)) {
htmlBuilder.append("display:block;overflow:visible;padding:0;"); htmlBuilder.append("display:block;overflow:visible;padding:0;");
} else { } 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) { switch (textAlign) {
case "center": case "center":
htmlBuilder.append("justify-content:center;text-align:center;"); htmlBuilder.append("justify-content:center;text-align:center;");
@@ -85,6 +85,8 @@ public class AddressBookView extends VerticalLayout {
.setAutoWidth(true); .setAutoWidth(true);
grid.addColumn(address -> address.getName()).setHeader("Firma / Name").setSortable(true).setAutoWidth(true); grid.addColumn(address -> address.getName()).setHeader("Firma / Name").setSortable(true).setAutoWidth(true);
grid.addColumn(address -> address.getStreet()).setHeader("Straße").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 -> { grid.addComponentColumn(address -> {
Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openEditDialog(address)); Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openEditDialog(address));
edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL); 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, delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR,
ButtonVariant.LUMO_SMALL); ButtonVariant.LUMO_SMALL);
delete.setTooltipText("Löschen"); delete.setTooltipText("Löschen");
return new HorizontalLayout(edit, delete); HorizontalLayout actions = new HorizontalLayout(edit, delete);
}).setHeader("").setFlexGrow(0).setAutoWidth(true); actions.setSpacing(false);
actions.setPadding(false);
return actions;
}).setHeader("").setFlexGrow(0).setWidth("110px");
grid.addItemDoubleClickListener(event -> openEditDialog(event.getItem())); grid.addItemDoubleClickListener(event -> openEditDialog(event.getItem()));
grid.setSizeFull(); grid.setSizeFull();
add(grid); add(grid);
@@ -142,7 +147,6 @@ public class AddressBookView extends VerticalLayout {
countryCode.setPattern("[A-Za-z]{2}"); countryCode.setPattern("[A-Za-z]{2}");
TextField vatId = new TextField("USt-IdNr."); TextField vatId = new TextField("USt-IdNr.");
vatId.setValue(target.getVatId() != null ? target.getVatId() : ""); vatId.setValue(target.getVatId() != null ? target.getVatId() : "");
vatId.setHelperText("z. B. DE261094748");
EmailField email = new EmailField("E-Mail"); EmailField email = new EmailField("E-Mail");
email.setValue(target.getEmail() != null ? target.getEmail() : ""); email.setValue(target.getEmail() != null ? target.getEmail() : "");
// E-Mail direkt beim Verlassen des Feldes prüfen // E-Mail direkt beim Verlassen des Feldes prüfen
@@ -154,18 +158,19 @@ public class AddressBookView extends VerticalLayout {
}); });
TextField phone = new TextField("Telefon"); TextField phone = new TextField("Telefon");
phone.setValue(target.getPhone() != null ? target.getPhone() : ""); phone.setValue(target.getPhone() != null ? target.getPhone() : "");
TextField website = new TextField("Webseite");
website.setValue(target.getWebsite() != null ? target.getWebsite() : "");
// Bankverbindung (optional) // Bankverbindung (optional)
TextField bankName = new TextField("Bank"); TextField bankName = new TextField("Bank");
bankName.setValue(target.getBankName() != null ? target.getBankName() : ""); bankName.setValue(target.getBankName() != null ? target.getBankName() : "");
TextField iban = new TextField("IBAN"); TextField iban = new TextField("IBAN");
iban.setValue(target.getIban() != null ? target.getIban() : ""); iban.setValue(target.getIban() != null ? target.getIban() : "");
iban.setHelperText("z. B. DE02 1203 0000 0000 2020 51");
TextField bic = new TextField("BIC"); TextField bic = new TextField("BIC");
bic.setValue(target.getBic() != null ? target.getBic() : ""); bic.setValue(target.getBic() != null ? target.getBic() : "");
FormLayout form = new FormLayout(type, name, street, zip, city, countryCode, vatId, email, phone, FormLayout form = new FormLayout(type, name, street, zip, city, countryCode, vatId, email, phone,
bankName, iban, bic); website, bankName, iban, bic);
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("500px", 2)); form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("500px", 2));
form.setWidth("560px"); form.setWidth("560px");
dialog.add(form); dialog.add(form);
@@ -226,6 +231,7 @@ public class AddressBookView extends VerticalLayout {
target.setVatId(vatId.getValue().isBlank() ? "" : VatIdValidator.normalize(vatId.getValue())); target.setVatId(vatId.getValue().isBlank() ? "" : VatIdValidator.normalize(vatId.getValue()));
target.setEmail(email.getValue().trim()); target.setEmail(email.getValue().trim());
target.setPhone(phone.getValue().trim()); target.setPhone(phone.getValue().trim());
target.setWebsite(website.getValue().trim());
target.setBankName(bankName.getValue().trim()); target.setBankName(bankName.getValue().trim());
target.setIban(iban.getValue().isBlank() ? "" : IbanValidator.normalize(iban.getValue())); target.setIban(iban.getValue().isBlank() ? "" : IbanValidator.normalize(iban.getValue()));
target.setBic(bic.getValue().trim().toUpperCase()); target.setBic(bic.getValue().trim().toUpperCase());
@@ -13,6 +13,7 @@ import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Paragraph; 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.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
@@ -22,8 +23,7 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.BigDecimalField; import com.vaadin.flow.component.textfield.BigDecimalField;
import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.upload.Upload; import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.StreamResource;
@@ -45,8 +45,6 @@ import de.assecutor.pdftool.zugferd.ZugferdResult;
import de.assecutor.pdftool.zugferd.ZugferdService; import de.assecutor.pdftool.zugferd.ZugferdService;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.time.LocalDate; import java.time.LocalDate;
@@ -70,10 +68,7 @@ public class MainView extends VerticalLayout {
private final InvoiceTemplateService invoiceTemplateService; private final InvoiceTemplateService invoiceTemplateService;
private final TemplatePdfService templatePdfService; private final TemplatePdfService templatePdfService;
private final MemoryBuffer uploadBuffer = new MemoryBuffer(); /** Grundlage der Rechnung: ein gespeichertes Rechnungstemplate. */
private byte[] uploadedPdf;
/** Alternative zum PDF-Upload: ein gespeichertes Rechnungstemplate. */
private final ComboBox<String> templateSelect = new ComboBox<>("Rechnungstemplate"); private final ComboBox<String> templateSelect = new ComboBox<>("Rechnungstemplate");
/** Vorbelegung der Rechnungssteller-Felder aus dem Adressbuch. */ /** Vorbelegung der Rechnungssteller-Felder aus dem Adressbuch. */
@@ -117,8 +112,8 @@ public class MainView extends VerticalLayout {
private final Anchor downloadLink = new Anchor(); private final Anchor downloadLink = new Anchor();
/** /**
* Alle Controls unterhalb der Quellenwahl; sie sind erst aktiv, wenn ein PDF * Alle Controls unterhalb der Template-Auswahl; sie sind erst aktiv, wenn
* hochgeladen oder ein Template ausgewählt wurde. * ein Template ausgewählt wurde.
*/ */
private final List<com.vaadin.flow.component.HasEnabled> sourceDependentControls = new ArrayList<>(); private final List<com.vaadin.flow.component.HasEnabled> sourceDependentControls = new ArrayList<>();
@@ -134,15 +129,33 @@ public class MainView extends VerticalLayout {
setMaxWidth("960px"); setMaxWidth("960px");
add(new Paragraph("Laden Sie ein Rechnungs-PDF hoch oder wählen Sie ein gespeichertes " add(new Paragraph("Wählen Sie ein gespeichertes Rechnungstemplate und ergänzen Sie die "
+ "Rechnungstemplate und ergänzen Sie die Rechnungsdaten. Die Rechnung wird als " + "Rechnungsdaten. Die Rechnung wird als rechtskonforme ZUGFeRD-Rechnung "
+ "rechtskonforme ZUGFeRD-Rechnung (PDF/A-3 mit eingebettetem EN16931-XML) erzeugt " + "(PDF/A-3 mit eingebettetem EN16931-XML) erzeugt und als ZIP-Datei bereitgestellt."));
+ "und als ZIP-Datei bereitgestellt."));
// Eingaben unter einem Namen speichern bzw. gespeicherte Eingaben laden. // Eingaben unter einem Namen speichern bzw. gespeicherte Eingaben laden.
draftSelect.setPlaceholder("Vorbelegung laden"); draftSelect.setPlaceholder("Vorbelegung laden");
draftSelect.setClearButtonVisible(true); draftSelect.setClearButtonVisible(true);
draftSelect.setWidth("300px"); 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 -> { draftSelect.addValueChangeListener(event -> {
if (event.isFromClient() && event.getValue() != null) { if (event.isFromClient() && event.getValue() != null) {
loadDraft(event.getValue()); loadDraft(event.getValue());
@@ -153,46 +166,24 @@ public class MainView extends VerticalLayout {
}); });
add(draftSelect); add(draftSelect);
Upload upload = new Upload(uploadBuffer);
upload.setAcceptedFileTypes("application/pdf", ".pdf");
upload.setMaxFiles(1);
upload.setMaxFileSize(25 * 1024 * 1024);
upload.addSucceededListener(event -> {
try {
uploadedPdf = uploadBuffer.getInputStream().readAllBytes();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
// Entweder PDF oder Template: hochgeladenes PDF hebt die Template-Auswahl auf.
templateSelect.clear();
updateControlsEnabled();
});
upload.addFileRemovedListener(event -> {
uploadedPdf = null;
updateControlsEnabled();
});
templateSelect.setItems(invoiceTemplateService.templateNames()); templateSelect.setItems(invoiceTemplateService.templateNames());
templateSelect.setPlaceholder("Template auswählen"); templateSelect.setPlaceholder("Template auswählen");
templateSelect.setClearButtonVisible(true); templateSelect.setClearButtonVisible(true);
templateSelect.setWidth("300px"); templateSelect.setWidth("300px");
templateSelect.addValueChangeListener(event -> { templateSelect.addValueChangeListener(event -> {
if (event.isFromClient() && event.getValue() != null) { if (event.isFromClient() && event.getValue() != null) {
// Entweder PDF oder Template: Auswahl entfernt ein hochgeladenes PDF. // Rechnungssteller-Felder nur bei der ersten Template-Auswahl
upload.clearFileList(); // leer beginnen lassen; beim Wechsel zwischen Templates bleiben
uploadedPdf = null; // die Eingaben erhalten (sonst wäre u. a. der Speichern-Button
// Rechnungssteller-Felder leer beginnen lassen. // wegen leerer Pflichtfelder nie aktiv).
senderAddressSelect.clear(); if (event.getOldValue() == null) {
sender.clearFields(); senderAddressSelect.clear();
sender.clearFields();
}
} }
updateControlsEnabled(); updateControlsEnabled();
}); });
HorizontalLayout sourceLayout = new HorizontalLayout(upload, templateSelect); add(new H3("1. Template auswählen"), templateSelect);
sourceLayout.setAlignItems(Alignment.CENTER);
// Das Label der ComboBox ausgleichen, damit das Eingabefeld selbst
// mittig zur Upload-Fläche steht.
templateSelect.getStyle().set("margin-top", "-1.25em");
add(new H3("1. Rechnungs-PDF hochladen oder Template auswählen"), sourceLayout);
invoiceNumber.setRequired(true); invoiceNumber.setRequired(true);
invoiceNumber.setErrorMessage("Rechnungsnummer ist ein Pflichtfeld."); invoiceNumber.setErrorMessage("Rechnungsnummer ist ein Pflichtfeld.");
@@ -226,8 +217,11 @@ public class MainView extends VerticalLayout {
recipientAddressSelect, recipient, itemsGrid, addItem, generate)); recipientAddressSelect, recipient, itemsGrid, addItem, generate));
updateControlsEnabled(); 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.getElement().setAttribute("download", true);
downloadLink.setVisible(false); downloadLink.getStyle().set("display", "none");
add(new HorizontalLayout(generate, saveDraftButton, downloadLink)); add(new HorizontalLayout(generate, saveDraftButton, downloadLink));
@@ -290,6 +284,39 @@ public class MainView extends VerticalLayout {
confirm.open(); 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) { private void saveDraft(String name) {
try { try {
invoiceDraftService.save(buildDraft(name)); invoiceDraftService.save(buildDraft(name));
@@ -306,7 +333,7 @@ public class MainView extends VerticalLayout {
.addThemeVariants(NotificationVariant.LUMO_SUCCESS); .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} }
/** Sammelt den aktuellen Formularstand; ein hochgeladenes PDF wird nicht mitgespeichert. */ /** Sammelt den aktuellen Formularstand. */
private InvoiceDraft buildDraft(String name) { private InvoiceDraft buildDraft(String name) {
return new InvoiceDraft(null, name, return new InvoiceDraft(null, name,
templateSelect.getValue(), templateSelect.getValue(),
@@ -317,6 +344,7 @@ public class MainView extends VerticalLayout {
"", "",
sender.ibanNormalized(), sender.ibanNormalized(),
sender.bicNormalized(), sender.bicNormalized(),
sender.websiteValue(),
sender.toParty(), recipient.toParty(), sender.toParty(), recipient.toParty(),
List.copyOf(lineItems), List.copyOf(lineItems),
LocalDateTime.now()); LocalDateTime.now());
@@ -335,8 +363,7 @@ public class MainView extends VerticalLayout {
/** Überträgt gespeicherte Eingaben in das Formular. */ /** Überträgt gespeicherte Eingaben in das Formular. */
private void applyDraft(InvoiceDraft draft) { private void applyDraft(InvoiceDraft draft) {
// Die Template-Auswahl wiederherstellen, sofern das Template noch existiert; // Die Template-Auswahl wiederherstellen, sofern das Template noch existiert.
// ein hochgeladenes PDF ist nicht Teil der gespeicherten Eingaben.
if (draft.templateName() != null && !draft.templateName().isBlank() if (draft.templateName() != null && !draft.templateName().isBlank()
&& invoiceTemplateService.templateExists(draft.templateName())) { && invoiceTemplateService.templateExists(draft.templateName())) {
templateSelect.setValue(draft.templateName()); templateSelect.setValue(draft.templateName());
@@ -348,6 +375,8 @@ public class MainView extends VerticalLayout {
currency.setValue(nullToEmpty(draft.currency())); currency.setValue(nullToEmpty(draft.currency()));
paymentTerms.setValue(nullToEmpty(draft.paymentTerms())); paymentTerms.setValue(nullToEmpty(draft.paymentTerms()));
applyParty(sender, draft.sender()); applyParty(sender, draft.sender());
sender.prefillContact(draft.sender() != null ? nullToEmpty(draft.sender().phone()) : "",
nullToEmpty(draft.website()));
sender.prefillBank(nullToEmpty(draft.iban()), nullToEmpty(draft.bic())); sender.prefillBank(nullToEmpty(draft.iban()), nullToEmpty(draft.bic()));
applyParty(recipient, draft.recipient()); applyParty(recipient, draft.recipient());
lineItems.clear(); lineItems.clear();
@@ -385,6 +414,7 @@ public class MainView extends VerticalLayout {
nullToEmpty(address.getZip()), nullToEmpty(address.getCity()), nullToEmpty(address.getZip()), nullToEmpty(address.getCity()),
nullToEmpty(address.getCountryCode()), nullToEmpty(address.getVatId()), nullToEmpty(address.getCountryCode()), nullToEmpty(address.getVatId()),
nullToEmpty(address.getEmail())); nullToEmpty(address.getEmail()));
form.prefillContact(nullToEmpty(address.getPhone()), nullToEmpty(address.getWebsite()));
form.prefillBank(nullToEmpty(address.getIban()), nullToEmpty(address.getBic())); form.prefillBank(nullToEmpty(address.getIban()), nullToEmpty(address.getBic()));
} }
}); });
@@ -412,6 +442,8 @@ public class MainView extends VerticalLayout {
.setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0); .setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0);
itemsGrid.addColumn(item -> formatDecimal(item.vatPercent())).setHeader("USt %") itemsGrid.addColumn(item -> formatDecimal(item.vatPercent())).setHeader("USt %")
.setTextAlign(ColumnTextAlign.END).setAutoWidth(true).setFlexGrow(0); .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 -> { itemsGrid.addComponentColumn(item -> {
Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openItemDialog(item)); Button edit = new Button(new Icon(VaadinIcon.EDIT), event -> openItemDialog(item));
edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL); edit.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SMALL);
@@ -423,8 +455,11 @@ public class MainView extends VerticalLayout {
delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR, delete.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR,
ButtonVariant.LUMO_SMALL); ButtonVariant.LUMO_SMALL);
delete.setTooltipText("Löschen"); delete.setTooltipText("Löschen");
return new HorizontalLayout(edit, delete); HorizontalLayout actions = new HorizontalLayout(edit, delete);
}).setHeader("").setAutoWidth(true).setFlexGrow(0); actions.setSpacing(false);
actions.setPadding(false);
return actions;
}).setHeader("").setWidth("110px").setFlexGrow(0);
itemsGrid.addItemDoubleClickListener(event -> openItemDialog(event.getItem())); itemsGrid.addItemDoubleClickListener(event -> openItemDialog(event.getItem()));
itemsGrid.setAllRowsVisible(true); itemsGrid.setAllRowsVisible(true);
itemsGrid.setWidthFull(); itemsGrid.setWidthFull();
@@ -478,6 +513,7 @@ public class MainView extends VerticalLayout {
"", "",
sender.ibanNormalized(), sender.ibanNormalized(),
sender.bicNormalized(), sender.bicNormalized(),
sender.websiteValue(),
sender.toParty(), recipient.toParty(), sender.toParty(), recipient.toParty(),
List.copyOf(lineItems), List.copyOf(lineItems),
null); null);
@@ -553,24 +589,19 @@ public class MainView extends VerticalLayout {
/** /**
* Das Rechnungsformular und der Erzeugen-Button sind nur aktiv, wenn ein * Das Rechnungsformular und der Erzeugen-Button sind nur aktiv, wenn ein
* PDF hochgeladen oder ein Template ausgewählt wurde. * Template ausgewählt wurde.
*/ */
private void updateControlsEnabled() { private void updateControlsEnabled() {
boolean sourceSelected = uploadedPdf != null || templateSelect.getValue() != null; boolean sourceSelected = templateSelect.getValue() != null;
sourceDependentControls.forEach(control -> control.setEnabled(sourceSelected)); sourceDependentControls.forEach(control -> control.setEnabled(sourceSelected));
} }
private void generate() { private void generate() {
downloadLink.setVisible(false);
List<String> errors = validateForm(); List<String> errors = validateForm();
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
showErrors(errors); showErrors(errors);
return; return;
} }
if (uploadedPdf != null) {
convertAndOfferDownload(uploadedPdf);
return;
}
String templateName = templateSelect.getValue(); String templateName = templateSelect.getValue();
Optional<String> templateData = invoiceTemplateService.loadTemplate(templateName); Optional<String> templateData = invoiceTemplateService.loadTemplate(templateName);
if (templateData.isEmpty()) { if (templateData.isEmpty()) {
@@ -595,12 +626,20 @@ public class MainView extends VerticalLayout {
private Map<String, String> buildTemplateVariables(InvoiceMetadata metadata) { private Map<String, String> buildTemplateVariables(InvoiceMetadata metadata) {
Map<String, String> variables = new LinkedHashMap<>(); Map<String, String> variables = new LinkedHashMap<>();
InvoiceMetadata.Party senderParty = metadata.sender(); InvoiceMetadata.Party senderParty = metadata.sender();
// Telefon und Webseite kommen aus den Formularfeldern des
// Rechnungsstellers; sind sie leer (z. B. alte Vorbelegung), springt
// der Adressbuch-Eintrag des Rechnungsstellers ein.
Optional<Address> senderAddress = findSenderAddress(senderParty.name());
String phone = !senderParty.phone().isBlank() ? senderParty.phone()
: senderAddress.map(address -> nullToEmpty(address.getPhone())).orElse("");
String website = !sender.websiteValue().isBlank() ? sender.websiteValue()
: senderAddress.map(address -> nullToEmpty(address.getWebsite())).orElse("");
variables.put("masterdata.company_name", senderParty.name()); variables.put("masterdata.company_name", senderParty.name());
variables.put("masterdata.street", senderParty.street()); variables.put("masterdata.street", senderParty.street());
variables.put("masterdata.city", senderParty.zip() + " " + senderParty.city()); variables.put("masterdata.city", senderParty.zip() + " " + senderParty.city());
variables.put("masterdata.phone", senderParty.phone()); variables.put("masterdata.phone", phone);
variables.put("masterdata.email", senderParty.email()); variables.put("masterdata.email", senderParty.email());
variables.put("masterdata.website", ""); variables.put("masterdata.website", website);
variables.put("masterdata.sender_line", variables.put("masterdata.sender_line",
senderParty.name() + " · " + senderParty.street() + " · " + senderParty.zip() + " " + senderParty.city()); senderParty.name() + " · " + senderParty.street() + " · " + senderParty.zip() + " " + senderParty.city());
variables.put("masterdata.payment_terms", metadata.paymentTerms()); variables.put("masterdata.payment_terms", metadata.paymentTerms());
@@ -631,6 +670,22 @@ public class MainView extends VerticalLayout {
return variables; return variables;
} }
/**
* @return der Adressbuch-Eintrag des Rechnungsstellers: die Auswahl in
* "Aus dem Adressbuch übernehmen", sonst der Namenstreffer unter
* den Rechnungssteller-Adressen (z. B. nach Laden einer Vorbelegung).
*/
private Optional<Address> findSenderAddress(String senderName) {
Address selected = senderAddressSelect.getValue();
if (selected != null) {
return Optional.of(selected);
}
String normalized = senderName == null ? "" : senderName.trim();
return addressRepository.findByType(AddressType.RECHNUNGSSTELLER).stream()
.filter(address -> normalized.equalsIgnoreCase(nullToEmpty(address.getName()).trim()))
.findFirst();
}
/** Wandelt das Quell-PDF in die ZUGFeRD-E-Rechnung um und bietet das ZIP zum Download an. */ /** Wandelt das Quell-PDF in die ZUGFeRD-E-Rechnung um und bietet das ZIP zum Download an. */
private void convertAndOfferDownload(byte[] sourcePdf) { private void convertAndOfferDownload(byte[] sourcePdf) {
try { try {
@@ -642,13 +697,12 @@ public class MainView extends VerticalLayout {
() -> new ByteArrayInputStream(result.zip())); () -> new ByteArrayInputStream(result.zip()));
resource.setContentType("application/zip"); resource.setContentType("application/zip");
downloadLink.setHref(resource); downloadLink.setHref(resource);
downloadLink.setText("ZIP herunterladen (" + result.zipFileName() + ")");
downloadLink.setVisible(true);
if (result.valid()) { if (result.valid()) {
Notification.show("E-Rechnung erzeugt und erfolgreich validiert.", 4000, Notification.show("E-Rechnung erzeugt und erfolgreich validiert.", 4000,
Notification.Position.BOTTOM_START) Notification.Position.BOTTOM_START)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS); .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
downloadLink.getElement().callJsFunction("click");
} else { } else {
Notification.show("E-Rechnung erzeugt, aber die Validierung meldet Probleme. " Notification.show("E-Rechnung erzeugt, aber die Validierung meldet Probleme. "
+ "Details stehen im Prüfbericht (validation-report.xml) im ZIP.", 8000, + "Details stehen im Prüfbericht (validation-report.xml) im ZIP.", 8000,
@@ -758,6 +812,10 @@ public class MainView extends VerticalLayout {
private final TextField vatId = new TextField("USt-IdNr."); private final TextField vatId = new TextField("USt-IdNr.");
private final EmailField email = new EmailField("E-Mail"); private final EmailField email = new EmailField("E-Mail");
// Kontaktangaben; nur beim Rechnungssteller sichtbar und optional
private final TextField phone = new TextField("Telefon");
private final TextField website = new TextField("Webseite");
// Bankverbindung; nur beim Rechnungssteller sichtbar und Pflicht // Bankverbindung; nur beim Rechnungssteller sichtbar und Pflicht
private final TextField iban = new TextField("IBAN"); private final TextField iban = new TextField("IBAN");
private final TextField bic = new TextField("BIC"); private final TextField bic = new TextField("BIC");
@@ -774,7 +832,6 @@ public class MainView extends VerticalLayout {
countryCode.setPattern("[A-Za-z]{2}"); countryCode.setPattern("[A-Za-z]{2}");
countryCode.setErrorMessage("Zweistelliger ISO-Code, z. B. DE."); countryCode.setErrorMessage("Zweistelliger ISO-Code, z. B. DE.");
vatId.setRequired(vatRequired); vatId.setRequired(vatRequired);
vatId.setHelperText("z. B. DE261094748");
// USt-IdNr. direkt beim Verlassen des Feldes prüfen // USt-IdNr. direkt beim Verlassen des Feldes prüfen
vatId.addValueChangeListener(event -> checkVatId()); vatId.addValueChangeListener(event -> checkVatId());
// Elektronische Adresse (BT-34/BT-49) ist nach PEPPOL-EN16931 Pflicht // Elektronische Adresse (BT-34/BT-49) ist nach PEPPOL-EN16931 Pflicht
@@ -785,10 +842,8 @@ public class MainView extends VerticalLayout {
add(name, street, zip, city, countryCode, vatId, email); add(name, street, zip, city, countryCode, vatId, email);
if (vatRequired) { if (vatRequired) {
iban.setRequired(true); iban.setRequired(true);
iban.setHelperText("Für die Zahlungsangaben (BG-16)");
bic.setRequired(true); bic.setRequired(true);
bic.setHelperText("8 oder 11 Stellen, z. B. NOLADE21RZB"); add(phone, website, iban, bic);
add(iban, bic);
} }
setResponsiveSteps(new ResponsiveStep("0", 1), new ResponsiveStep("600px", 3)); setResponsiveSteps(new ResponsiveStep("0", 1), new ResponsiveStep("600px", 3));
} }
@@ -799,6 +854,16 @@ public class MainView extends VerticalLayout {
bic.setValue(bicValue); bic.setValue(bicValue);
} }
/** Belegt Telefon und Webseite vor, z. B. aus dem Adressbuch. */
void prefillContact(String phoneValue, String websiteValue) {
phone.setValue(phoneValue);
website.setValue(websiteValue);
}
String websiteValue() {
return website.getValue().trim();
}
/** IBAN ohne Leerzeichen in Großschreibung. */ /** IBAN ohne Leerzeichen in Großschreibung. */
String ibanNormalized() { String ibanNormalized() {
return iban.getValue().replaceAll("\\s", "").toUpperCase(); return iban.getValue().replaceAll("\\s", "").toUpperCase();
@@ -841,6 +906,8 @@ public class MainView extends VerticalLayout {
countryCode.addValueChangeListener(event -> listener.run()); countryCode.addValueChangeListener(event -> listener.run());
vatId.addValueChangeListener(event -> listener.run()); vatId.addValueChangeListener(event -> listener.run());
email.addValueChangeListener(event -> listener.run()); email.addValueChangeListener(event -> listener.run());
phone.addValueChangeListener(event -> listener.run());
website.addValueChangeListener(event -> listener.run());
iban.addValueChangeListener(event -> listener.run()); iban.addValueChangeListener(event -> listener.run());
bic.addValueChangeListener(event -> listener.run()); bic.addValueChangeListener(event -> listener.run());
} }
@@ -848,6 +915,7 @@ public class MainView extends VerticalLayout {
/** Leert alle Felder; das Land behält den Standardwert "DE". */ /** Leert alle Felder; das Land behält den Standardwert "DE". */
void clearFields() { void clearFields() {
prefill("", "", "", "", "DE", "", ""); prefill("", "", "", "", "DE", "", "");
prefillContact("", "");
prefillBank("", ""); prefillBank("", "");
name.setInvalid(false); name.setInvalid(false);
street.setInvalid(false); street.setInvalid(false);
@@ -930,7 +998,7 @@ public class MainView extends VerticalLayout {
countryCode.getValue().trim().toUpperCase(), countryCode.getValue().trim().toUpperCase(),
vatId.isEmpty() ? "" : VatIdValidator.normalize(vatId.getValue()), vatId.isEmpty() ? "" : VatIdValidator.normalize(vatId.getValue()),
email.getValue().trim(), email.getValue().trim(),
"" phone.getValue().trim()
); );
} }
} }
+40
View File
@@ -0,0 +1,40 @@
// 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,
"ignoreDeprecations": "6.0",
"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
View File
@@ -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;
}
}
+9
View File
@@ -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);
+858
View File
@@ -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;
}