diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseInputStreamFactory.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseInputStreamFactory.java index 95aafb6..c0caa32 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseInputStreamFactory.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/BaseInputStreamFactory.java @@ -23,6 +23,7 @@ import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.Grid.Column; +import com.vaadin.flow.component.grid.HeaderRow; import com.vaadin.flow.component.grid.dataview.GridLazyDataView; import com.vaadin.flow.data.provider.AbstractBackEndDataProvider; import com.vaadin.flow.data.provider.DataCommunicator; @@ -89,14 +90,54 @@ protected Stream getDataStream(Query newQuery) { return stream; } - protected List>> getGridHeaders(Grid grid) { - return exporter.getColumnsOrdered().stream() - .map( - column -> - ImmutablePair.of( - renderCellTextContent(grid, column, GridExporter.COLUMN_HEADER), column)) - .collect(Collectors.toList()); - } + protected List, Column>> getGridHeaders(Grid grid) { + return exporter.getColumnsOrdered().stream() + .map(column -> ImmutablePair.of(getHeaderTexts(grid, column), column)) + .collect(Collectors.toList()); + } + + private List getHeaderTexts(Grid grid, Column column) { + List headerTexts = new ArrayList<>(); + + List headerRows = grid.getHeaderRows(); + for (HeaderRow headerRow : headerRows) { + String headerText = renderCellTextContent(grid, column, GridExporter.COLUMN_HEADER, headerRow); + headerTexts.add(headerText); + } + + return headerTexts; + } + + private String renderCellTextContent(Grid grid, Column column, String columnType, HeaderRow headerRow) { + String headerOrFooter = (String) ComponentUtil.getData(column, columnType); + + if (Strings.isBlank(headerOrFooter)) { + Function, Component> getHeaderOrFooterComponent; + if (GridExporter.COLUMN_HEADER.equals(columnType)) { + getHeaderOrFooterComponent = col -> col.getHeaderComponent(); + headerOrFooter = column.getHeaderText(); + } else if (GridExporter.COLUMN_FOOTER.equals(columnType)) { + getHeaderOrFooterComponent = col -> col.getFooterComponent(); + headerOrFooter = column.getFooterText(); + } else { + throw new IllegalArgumentException(); + } + + if (Strings.isBlank(headerOrFooter)) { + try { + Component component = getHeaderOrFooterComponent.apply(column); + if (component != null) { + headerOrFooter = component.getElement().getTextRecursively(); + } + } catch (RuntimeException e) { + throw new IllegalStateException( + "Problem when trying to render header or footer cell text content", e); + } + } + } + + return headerOrFooter; + } protected List>> getGridFooters(Grid grid) { return exporter.getColumnsOrdered().stream() diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxInputStreamFactory.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxInputStreamFactory.java index 069ea19..4ed9d19 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxInputStreamFactory.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/DocxInputStreamFactory.java @@ -147,7 +147,10 @@ protected XWPFDocument createDoc() throws IOException { cctblgridcol, "" + Math.round(9638 / exporter.getColumns().size())); }); - List>> headers = getGridHeaders(exporter.grid); + List>> headers = getGridHeaders(exporter.grid).stream() + .map(pair -> + Pair.of(pair.getLeft().get(0), pair.getRight()) + ).toList(); XWPFTableCell cell = findCellWithPlaceHolder(table, exporter.headersPlaceHolder); if (cell != null) { fillHeaderOrFooter(table, cell, headers, true, exporter.headersPlaceHolder); diff --git a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelInputStreamFactory.java b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelInputStreamFactory.java index ea83952..7c7615d 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelInputStreamFactory.java +++ b/src/main/java/com/flowingcode/vaadin/addons/gridexporter/ExcelInputStreamFactory.java @@ -37,6 +37,7 @@ import java.util.stream.Stream; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; @@ -94,16 +95,20 @@ public InputStream createInputStream() { } Cell cell = findCellWithPlaceHolder(sheet, exporter.headersPlaceHolder); - List>> headers = getGridHeaders(exporter.grid); + List, Column>> headersTest = getGridHeaders(exporter.grid); + List>> headers = headersTest.stream() + .map(pair -> + Pair.of(pair.getLeft().get(0), pair.getRight()) + ).toList(); - fillHeaderOrFooter(sheet, cell, headers, true); + fillHeaderOrFooter(sheet, cell, headersTest, true); if (exporter.autoMergeTitle && titleCell != null) { sheet.addMergedRegion( new CellRangeAddress( titleCell.getRowIndex(), titleCell.getRowIndex(), titleCell.getColumnIndex(), - titleCell.getColumnIndex() + headers.size() - 1)); + titleCell.getColumnIndex() + headersTest.size() - 1)); } cell = findCellWithPlaceHolder(sheet, exporter.dataPlaceHolder); @@ -126,7 +131,7 @@ public InputStream createInputStream() { cell = findCellWithPlaceHolder(sheet, exporter.footersPlaceHolder); List>> footers = getGridFooters(exporter.grid); if (cell != null) { - fillHeaderOrFooter(sheet, cell, footers, false); + fillFooter(sheet, cell, footers, false); } if (exporter.isAutoSizeColumns()) { @@ -440,39 +445,54 @@ private Cell findCellWithPlaceHolder(Sheet sheet, String placeholder) { return null; } - private void fillHeaderOrFooter( - Sheet sheet, - Cell headersOrFootersCell, - List>> headersOrFooters, - boolean isHeader) { + private void fillFooter(Sheet sheet, Cell headersOrFootersCell, + List>> headersOrFooters, boolean isHeader) { + + List, Column>> headersOrFootersCellSingleRow = headersOrFooters.stream() + .map(pair -> Pair.of(List.of(pair.getLeft()), pair.getRight())).toList(); + fillHeaderOrFooter(sheet, headersOrFootersCell, headersOrFootersCellSingleRow, isHeader); + } + + private void fillHeaderOrFooter(Sheet sheet, Cell headersOrFootersCell, + List, Column>> headersOrFooters, boolean isHeader) { + CellStyle style = headersOrFootersCell.getCellStyle(); - sheet.setActiveCell(headersOrFootersCell.getAddress()); - headersOrFooters.forEach( - headerOrFooter -> { - if (!isHeader) { - // clear the styles before processing the column in the footer - ComponentUtil.setData(headerOrFooter.getRight(), COLUMN_CELLSTYLE_MAP, null); - } - Cell cell = - sheet - .getRow(sheet.getActiveCell().getRow()) - .getCell(sheet.getActiveCell().getColumn()); - if (cell == null) { - cell = - sheet - .getRow(sheet.getActiveCell().getRow()) - .createCell(sheet.getActiveCell().getColumn()); - } - cell.setCellStyle(style); - Object value = - (isHeader - ? headerOrFooter.getLeft() - : transformToType(headerOrFooter.getLeft(), headerOrFooter.getRight())); - buildCell(value, cell, headerOrFooter.getRight(), null); - configureAlignment(headerOrFooter.getRight(), cell, isHeader?ExcelCellType.HEADER:ExcelCellType.FOOTER); - sheet.setActiveCell( - new CellAddress( - sheet.getActiveCell().getRow(), sheet.getActiveCell().getColumn() + 1)); - }); + + int startRow = headersOrFootersCell.getRowIndex(); + int currentColumn = headersOrFootersCell.getColumnIndex(); + + for (Pair, Column> headerOrFooter : headersOrFooters) { + List headerTexts = headerOrFooter.getLeft(); + Column column = headerOrFooter.getRight(); + + if (!isHeader) { + ComponentUtil.setData(column, COLUMN_CELLSTYLE_MAP, null); + } + + sheet.shiftRows(startRow, sheet.getLastRowNum(), headerTexts.size()); + + for (int i = 0; i < headerTexts.size(); i++) { + Row row = sheet.getRow(startRow + i); + if (row == null) { + row = sheet.createRow(startRow + i); + } + + Cell cell = row.getCell(currentColumn); + if (cell == null) { + cell = row.createCell(currentColumn); + } + + cell.setCellStyle(style); + + Object value = + (isHeader ? headerTexts.get(i) : transformToType(headerTexts.get(i), column)); + buildCell(value, cell, column, null); + + configureAlignment(column, cell, isHeader ? ExcelCellType.HEADER : ExcelCellType.FOOTER); + } + + currentColumn++; + } } + } diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java index 338000c..812606f 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterDemoView.java @@ -41,6 +41,7 @@ public GridExporterDemoView() { addDemo(GridExporterCustomColumnsDemo.class); addDemo(GridExporterHierarchicalDataDemo.class); addDemo(GridExporterBigDatasetDemo.class); + addDemo(GridExporterMultipleHeaderRowsDemo.class); setSizeFull(); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterMultipleHeaderRowsDemo.java b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterMultipleHeaderRowsDemo.java new file mode 100644 index 0000000..dc398e6 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/gridexporter/GridExporterMultipleHeaderRowsDemo.java @@ -0,0 +1,113 @@ +/*- + * #%L + * Grid Exporter Add-on + * %% + * Copyright (C) 2022 - 2023 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.gridexporter; + +import java.io.IOException; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.HashMap; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.apache.poi.EncryptedDocumentException; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.github.javafaker.Faker; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.Grid.Column; +import com.vaadin.flow.component.grid.HeaderRow; +import com.vaadin.flow.component.grid.HeaderRow.HeaderCell; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.data.renderer.LitRenderer; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@DemoSource +@PageTitle("Grid Exporter Addon Multiple Header Rows Demo") +@Route(value = "gridexporter/headers", layout = GridExporterDemoView.class) +@SuppressWarnings("serial") +public class GridExporterMultipleHeaderRowsDemo extends Div { + + public GridExporterMultipleHeaderRowsDemo() throws EncryptedDocumentException, IOException { + Grid grid = new Grid<>(Person.class); + DecimalFormat decimalFormat = new DecimalFormat("$#,###.##"); + grid.removeAllColumns(); + grid.addColumn( + LitRenderer.of("${item.name}").withProperty("name", Person::getName)) + .setHeader("Name"); + grid.addColumn("lastName").setHeader("Last Name"); + grid.addColumn(item -> Faker.instance().lorem().characters(30, 50)).setHeader("Big column"); + Column budgetColumn = + grid.addColumn(item -> decimalFormat.format(item.getBudget())) + .setHeader("Budget") + .setTextAlign(ColumnTextAlign.END); + BigDecimal[] total = new BigDecimal[1]; + total[0] = BigDecimal.ZERO; + Stream stream = + IntStream.range(0, 100) + .asLongStream() + .mapToObj( + number -> { + Faker faker = new Faker(); + Double budget = faker.number().randomDouble(2, 10000, 100000); + total[0] = total[0].add(BigDecimal.valueOf(budget)); + budgetColumn.setFooter(new DecimalFormat("$#,###.##").format(total[0])); + return new Person( + faker.name().firstName(), + (Math.random() > 0.3 ? faker.name().lastName() : null), + faker.number().numberBetween(15, 50), + budget); + }); + + grid.setItems(DataProvider.fromStream(stream)); + grid.setWidthFull(); + this.setSizeFull(); + + HeaderRow firstExtraHeaderRow = grid.appendHeaderRow(); + HeaderRow secondExtraHeaderRow = grid.appendHeaderRow(); + for (Column column : grid.getColumns()) { + String columnHeader = grid.getHeaderRows().get(0).getCell(column).getText(); + + HeaderCell firstHeaderCell = firstExtraHeaderRow.getCell(column); + firstHeaderCell.setComponent(new Span(columnHeader + " 1")); + HeaderCell secondHeaderCell = secondExtraHeaderRow.getCell(column); + secondHeaderCell.setComponent(new Span(columnHeader + " 2")); + } + + GridExporter exporter = + GridExporter.createFor(grid, "/custom-template.xlsx", "/custom-template.docx"); + HashMap placeholders = new HashMap<>(); + placeholders.put("${date}", new SimpleDateFormat().format(Calendar.getInstance().getTime())); + exporter.setAdditionalPlaceHolders(placeholders); + exporter.setSheetNumber(1); + exporter.setCsvExportEnabled(false); + exporter.setNumberColumnFormat(budgetColumn, decimalFormat, "$#,###.##"); + exporter.setTitle("People information"); + exporter.setNullValueHandler(() -> "(No lastname)"); + exporter.setFileName( + "GridExport" + new SimpleDateFormat("yyyyddMM").format(Calendar.getInstance().getTime())); + add(grid); + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4d6a022 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,9 @@ +import { UserConfigFn } from 'vite'; +import { overrideVaadinConfig } from './vite.generated'; + +const customConfig: UserConfigFn = (env) => ({ + // Here you can add custom Vite parameters + // https://vitejs.dev/config/ +}); + +export default overrideVaadinConfig(customConfig); diff --git a/vite.generated.ts b/vite.generated.ts new file mode 100644 index 0000000..54f4c94 --- /dev/null +++ b/vite.generated.ts @@ -0,0 +1,640 @@ +/** + * 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 { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'; +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 settings from './target/vaadin-dev-server-settings.json'; +import { defineConfig, mergeConfig, PluginOption, ResolvedConfig, UserConfigFn, OutputOptions, AssetInfo, ChunkInfo } from 'vite'; +import { getManifest } from 'workbox-build'; + +import * as rollup from 'rollup'; +import brotli from 'rollup-plugin-brotli'; +import replace from '@rollup/plugin-replace'; +import checker from 'vite-plugin-checker'; +import postcssLit from './target/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js'; + +const appShellUrl = '.'; + +const frontendFolder = path.resolve(__dirname, settings.frontendFolder); +const themeFolder = path.resolve(frontendFolder, settings.themeFolder); +const statsFolder = path.resolve(__dirname, settings.statsOutput); +const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput); +const jarResourcesFolder = path.resolve(__dirname, settings.jarResourcesFolder); +const generatedFlowImportsFolder = path.resolve(__dirname, settings.generatedFlowImportsFolder); +const themeResourceFolder = path.resolve(__dirname, settings.themeResourceFolder); + +const statsFile = path.resolve(statsFolder, 'stats.json'); + +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, + // 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: 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 = (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: frontendBundleFolder, + globPatterns: ['**/*'], + globIgnores: ['**/*.br'], + manifestTransforms: [rewriteManifestIndexHtmlUrl], + maximumFileSizeToCacheInBytes: 100 * 1024 * 1024, // 100mb, + }); + + return code.replace('self.__WB_MANIFEST', JSON.stringify(manifestEntries)); + } + } + } +} + +function buildSWPlugin(opts): PluginOption { + let config: ResolvedConfig; + const devMode = opts.devMode; + + const swObj = {} + + async function build(action: 'generate' | 'write', additionalPlugins: rollup.Plugin[] = []) { + const includedPluginNames = [ + 'alias', + 'vite:resolve', + 'vite:esbuild', + 'rollup-plugin-dynamic-import-variables', + 'vite:esbuild-transpile', + 'vite:terser', + ] + const plugins: rollup.Plugin[] = config.plugins.filter((p) => { + return includedPluginNames.includes(p.name) + }); + plugins.push( + replace({ + values: { + 'process.env.NODE_ENV': JSON.stringify(config.mode), + ...config.define, + }, + preventAssignment: true + }) + ); + if (additionalPlugins) { + plugins.push(...additionalPlugins); + } + const bundle = await rollup.rollup({ + input: path.resolve(settings.clientServiceWorkerSource), + plugins + }); + + try { + return await bundle[action]({ + file: path.resolve(frontendBundleFolder, 'sw.js'), + format: 'es', + exports: 'none', + sourcemap: config.command === 'serve' || config.build.sourcemap, + inlineDynamicImports: true, + }); + } finally { + await bundle.close(); + } + } + + return { + name: 'vaadin:build-sw', + enforce: 'post', + async configResolved(resolvedConfig) { + config = resolvedConfig; + }, + async buildStart() { + if (devMode) { + const { output } = await build('generate'); + swObj.code = output[0].code; + swObj.map = output[0].map; + } + }, + async load(id) { + if (id.endsWith('sw.js')) { + return ''; + } + }, + async transform(_code, id) { + if (id.endsWith('sw.js')) { + return swObj; + } + }, + async closeBundle() { + await build('write', [ + injectManifestToSWPlugin(), + brotli(), + ]); + } + } +} + +function statsExtracterPlugin(): PluginOption { + 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.filter((id) => id.includes('node_modules')); + const npmModules = nodeModulesFolders + .map((id) => id.replace(/.*node_modules./, '')) + .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); + + mkdirSync(path.dirname(statsFile), { recursive: true }); + writeFileSync(statsFile, JSON.stringify({ npmModules }, null, 1)); + } + }; +} +function vaadinBundlesPlugin(): PluginOption { + type ExportInfo = + | string + | { + namespace?: string; + source: string; + }; + + type ExposeInfo = { + exports: ExportInfo[]; + }; + + type PackageInfo = { + version: string; + exposes: Record; + }; + + type BundleJson = { + packages: Record; + }; + + const disabledMessage = 'Vaadin component dependency bundles are disabled.'; + + const modulesDirectory = path.resolve(__dirname, 'node_modules').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(); + 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): PluginOption { + const fullThemeOptions = {...themeOptions, devMode: opts.devMode }; + return { + name: 'vaadin:theme', + config() { + processThemeResources(fullThemeOptions, console); + }, + configureServer(server) { + function handleThemeFileCreateDelete(themeFile, 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?.endsWith('.css')) { + return; + } + const [themeName] = bareId.substring(themeFolder.length + 1).split('/'); + return rewriteCssUrls(raw, path.dirname(bareId), path.resolve(themeFolder, themeName), console, opts); + } + }; +} +function lenientLitImportPlugin(): PluginOption { + return { + name: 'vaadin:lenient-lit-import', + async transform(code, id) { + const decoratorImports = [ + /import (.*?) from (['"])(lit\/decorators)(['"])/, + /import (.*?) from (['"])(lit-element\/decorators)(['"])/ + ]; + const directiveImports = [ + /import (.*?) from (['"])(lit\/directives\/)([^\\.]*?)(['"])/, + /import (.*?) from (['"])(lit-html\/directives\/)([^\\.]*?)(['"])/ + ]; + + decoratorImports.forEach((decoratorImport) => { + let decoratorMatch; + while ((decoratorMatch = code.match(decoratorImport))) { + console.warn( + `Warning: the file ${id} imports from '${decoratorMatch[3]}' when it should import from '${decoratorMatch[3]}.js'` + ); + code = code.replace(decoratorImport, 'import $1 from $2$3.js$4'); + } + }); + + directiveImports.forEach((directiveImport) => { + let directiveMatch; + while ((directiveMatch = code.match(directiveImport))) { + console.warn( + `Warning: the file ${id} imports from '${directiveMatch[3]}${directiveMatch[4]}' when it should import from '${directiveMatch[3]}${directiveMatch[4]}.js'` + ); + code = code.replace(directiveImport, 'import $1 from $2$3$4.js$5'); + } + }); + + return code; + } + }; +} + +function runWatchDog(watchDogPort, watchDogHost) { + const client = 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'); +} + +let spaMiddlewareForceRemoved = false; + +const allowedFrontendFolders = [ + frontendFolder, + path.resolve(generatedFlowImportsFolder), // Contains only generated-flow-imports + path.resolve(__dirname, 'node_modules') +]; + +function setHmrPortToServerPort(): PluginOption { + return { + name: 'set-hmr-port-to-server-port', + configResolved(config) { + if (config.server.strictPort && config.server.hmr !== false) { + if (config.server.hmr === true) config.server.hmr = {}; + config.server.hmr = config.server.hmr || {}; + config.server.hmr.clientPort = config.server.port; + } + } + }; +} +function showRecompileReason(): PluginOption { + return { + name: 'vaadin:why-you-compile', + handleHotUpdate(context) { + console.log('Recompiling because', context.file, 'changed'); + } + }; +} + +export const vaadinConfig: UserConfigFn = (env) => { + const devMode = env.mode === 'development'; + + 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(process.env.watchDogPort, process.env.watchDogHost); + } + + return { + root: frontendFolder, + base: '', + 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: { + outDir: frontendBundleFolder, + assetsDir: 'VAADIN/build', + rollupOptions: { + input: { + indexhtml: path.resolve(frontendFolder, 'index.html'), + + ...hasExportedWebComponents + ? { webcomponenthtml: path.resolve(frontendFolder, 'web-component.html') } + : {} + } + } + }, + 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: [ + !devMode && brotli(), + devMode && vaadinBundlesPlugin(), + devMode && setHmrPortToServerPort(), + devMode && showRecompileReason(), + settings.offlineEnabled && buildSWPlugin({ devMode }), + !devMode && statsExtracterPlugin(), + themePlugin({devMode}), + lenientLitImportPlugin(), + postcssLit({ + include: ['**/*.css', '**/*.css\?*'], + exclude: [ + `${themeFolder}/**/*.css`, + `${themeFolder}/**/*.css\?*`, + `${themeResourceFolder}/**/*.css`, + `${themeResourceFolder}/**/*.css\?*`, + '**/*\?html-proxy*' + ] + }), + { + name: 'vaadin:force-remove-html-middleware', + transformIndexHtml: { + enforce: 'pre', + transform(_html, { server }) { + if (server && !spaMiddlewareForceRemoved) { + server.middlewares.stack = server.middlewares.stack.filter((mw) => { + const handleName = '' + mw.handle; + return !handleName.includes('viteHtmlFallbackMiddleware'); + }); + spaMiddlewareForceRemoved = true; + } + } + } + }, + hasExportedWebComponents && { + name: 'vaadin:inject-entrypoints-to-web-component-html', + transformIndexHtml: { + enforce: 'pre', + transform(_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: { + enforce: 'pre', + transform(_html, { path, server }) { + if (path !== '/index.html') { + return; + } + + const scripts = []; + + if (devMode) { + scripts.push({ + tag: 'script', + attrs: { type: 'module', src: `/generated/vite-devmode.ts` }, + injectTo: 'head' + }); + } + scripts.push({ + tag: 'script', + attrs: { type: 'module', src: '/generated/vaadin.ts' }, + injectTo: 'head' + }); + return scripts; + } + } + }, + checker({ + typescript: true + }) + ] + }; +}; + +export const overrideVaadinConfig = (customConfig: UserConfigFn) => { + return defineConfig((env) => mergeConfig(vaadinConfig(env), customConfig(env))); +};