/** * 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 './build/plugins/application-theme-plugin/theme-handle.js'; import { rewriteCssUrls } from './build/plugins/theme-loader/theme-loader-utils.js'; import { addFunctionComponentSourceLocationBabel } from './build/plugins/react-function-location-plugin/react-function-location-plugin.js'; import settings from './build/vaadin-dev-server-settings.json'; import { AssetInfo, ChunkInfo, defineConfig, mergeConfig, OutputOptions, PluginOption, UserConfigFn } from 'vite'; import * as rollup from 'rollup'; import brotli from 'rollup-plugin-brotli'; import checker from 'vite-plugin-checker'; import postcssLit from './build/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js'; import vaadinI18n from './build/plugins/rollup-plugin-vaadin-i18n/rollup-plugin-vaadin-i18n.js'; import serviceWorkerPlugin from './build/plugins/vite-plugin-service-worker'; import vaadinBundlesPlugin from './build/plugins/vite-plugin-vaadin-bundles'; import { visualizer } from 'rollup-plugin-visualizer'; import reactPlugin from '@vitejs/plugin-react'; import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js'; const frontendFolder = path.resolve(__dirname, settings.frontendFolder); const themeFolder = path.resolve(frontendFolder, settings.themeFolder); const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput); const devBundleFolder = path.resolve(__dirname, settings.devBundleOutput); const devBundle = !!process.env.devBundle; const jarResourcesFolder = path.resolve(__dirname, settings.jarResourcesFolder); const themeResourceFolder = path.resolve(__dirname, settings.themeResourceFolder); const projectPackageJsonFile = path.resolve(__dirname, 'package.json'); const buildOutputFolder = devBundle ? devBundleFolder : frontendBundleFolder; const statsFolder = path.resolve(__dirname, devBundle ? settings.devBundleStatsOutput : settings.statsOutput); const statsFile = path.resolve(statsFolder, 'stats.json'); const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html'); const i18nFolder = path.resolve(__dirname, settings.i18nOutput); const nodeModulesFolder = path.resolve(__dirname, 'node_modules'); const webComponentTags = ''; const projectIndexHtml = path.resolve(frontendFolder, 'index.html'); const projectStaticAssetsFolders = [ path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'), path.resolve(__dirname, 'src', 'main', 'resources', 'static'), frontendFolder ]; // Folders in the project which can contain application themes const themeProjectFolders = projectStaticAssetsFolders.map((folder) => path.resolve(folder, settings.themeFolder)); const themeOptions = { devMode: false, useDevBundle: devBundle, // The following matches folder 'frontend/generated/themes/' // (not 'frontend/themes') for theme in JAR that is copied there themeResourceFolder: path.resolve(themeResourceFolder, settings.themeFolder), themeProjectFolders: themeProjectFolders, projectStaticAssetsOutputFolder: devBundle ? path.resolve(devBundleFolder, '../assets') : path.resolve(__dirname, settings.staticOutput), frontendGeneratedFolder: path.resolve(frontendFolder, settings.generatedFolder), projectStaticOutput: path.resolve(__dirname, settings.staticOutput), javaResourceFolder: settings.javaResourceFolder ? path.resolve(__dirname, settings.javaResourceFolder) : '' }; const hasExportedWebComponents = existsSync(path.resolve(frontendFolder, 'web-component.html')); const commercialBannerComponent = path.resolve(frontendFolder, settings.generatedFolder, 'commercial-banner.js'); const hasCommercialBanner = existsSync(commercialBannerComponent); const target = ['es2023']; // Block debug and trace logs. console.trace = () => {}; console.debug = () => {}; function statsExtracterPlugin(): PluginOption { function collectThemeJsonsInFrontend(themeJsonContents: Record, themeName: string) { const themeJson = path.resolve(frontendFolder, settings.themeFolder, themeName, 'theme.json'); if (existsSync(themeJson)) { const themeJsonContent = readFileSync(themeJson, { encoding: 'utf-8' }).replace(/\r\n/g, '\n'); themeJsonContents[themeName] = themeJsonContent; const themeJsonObject = JSON.parse(themeJsonContent); if (themeJsonObject.parent) { collectThemeJsonsInFrontend(themeJsonContents, themeJsonObject.parent); } } } return { name: 'vaadin:stats', enforce: 'post', async writeBundle(options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }) { const modules = Object.values(bundle).flatMap((b) => (b.modules ? Object.keys(b.modules) : [])); const nodeModulesFolders = modules .map((id) => id.replace(/\\/g, '/')) .filter((id) => id.startsWith(nodeModulesFolder.replace(/\\/g, '/'))) .map((id) => id.substring(nodeModulesFolder.length + 1)); const npmModules = nodeModulesFolders .map((id) => id.replace(/\\/g, '/')) .map((id) => { const parts = id.split('/'); if (id.startsWith('@')) { return parts[0] + '/' + parts[1]; } else { return parts[0]; } }) .sort() .filter((value, index, self) => self.indexOf(value) === index); const npmModuleAndVersion = Object.fromEntries(npmModules.map((module) => [module, getVersion(module)])); const cvdls = Object.fromEntries( npmModules .filter((module) => getCvdlName(module) != null) .map((module) => [module, { name: getCvdlName(module), version: getVersion(module) }]) ); mkdirSync(path.dirname(statsFile), { recursive: true }); const projectPackageJson = JSON.parse(readFileSync(projectPackageJsonFile, { encoding: 'utf-8' })); const entryScripts = Object.values(bundle) .filter((bundle) => bundle.isEntry) .map((bundle) => bundle.fileName); const generatedIndexHtml = path.resolve(buildOutputFolder, 'index.html'); const customIndexData: string = readFileSync(projectIndexHtml, { encoding: 'utf-8' }); const generatedIndexData: string = readFileSync(generatedIndexHtml, { encoding: 'utf-8' }); const customIndexRows = new Set(customIndexData.split(/[\r\n]/).filter((row) => row.trim() !== '')); const generatedIndexRows = generatedIndexData.split(/[\r\n]/).filter((row) => row.trim() !== ''); const rowsGenerated: string[] = []; generatedIndexRows.forEach((row) => { if (!customIndexRows.has(row)) { rowsGenerated.push(row); } }); //After dev-bundle build add used Flow frontend imports JsModule/JavaScript/CssImport const parseImports = (filename: string, result: Set): void => { const content: string = readFileSync(filename, { encoding: 'utf-8' }); const lines = content.split('\n'); const staticImports = lines .filter((line) => line.startsWith('import ')) .map((line) => line.substring(line.indexOf("'") + 1, line.lastIndexOf("'"))) .map((line) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line)); const dynamicImports = lines .filter((line) => line.includes('import(')) .map((line) => line.replace(/.*import\(/, '')) .map((line) => line.split(/'/)[1]) .map((line) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line)); staticImports.forEach((staticImport) => result.add(staticImport)); dynamicImports.map((dynamicImport) => { const importedFile = path.resolve(path.dirname(filename), dynamicImport); parseImports(importedFile, result); }); }; const generatedImportsSet = new Set(); parseImports( path.resolve(themeOptions.frontendGeneratedFolder, 'flow', 'generated-flow-imports.js'), generatedImportsSet ); parseImports( path.resolve(themeOptions.frontendGeneratedFolder, 'app-shell-imports.js'), generatedImportsSet ); const generatedImports = Array.from(generatedImportsSet).sort(); const frontendFiles: Record = {}; frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map']; const isThemeComponentsResource = (id: string) => id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/')) && id.match(/.*\/jar-resources\/themes\/[^\/]+\/components\//); const isGeneratedWebComponentResource = (id: string) => id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/')) && id.match(/.*\/flow\/web-components\//); const isFrontendResourceCollected = (id: string) => !id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/')) || isThemeComponentsResource(id) || isGeneratedWebComponentResource(id); // collects project's frontend resources in frontend folder, excluding // 'generated' sub-folder, except for legacy shadow DOM stylesheets // packaged in `theme/components/` folder // and generated web component resources in `flow/web-components` folder. modules .map((id) => id.replace(/\\/g, '/')) .filter((id) => id.startsWith(frontendFolder.replace(/\\/g, '/'))) .filter(isFrontendResourceCollected) .map((id) => id.substring(frontendFolder.length + 1)) .map((line: string) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line)) .forEach((line: string) => { // \r\n from windows made files may be used so change to \n const filePath = path.resolve(frontendFolder, line); if (projectFileExtensions.includes(path.extname(filePath))) { const fileBuffer = readFileSync(filePath, { encoding: 'utf-8' }).replace(/\r\n/g, '\n'); frontendFiles[line] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex'); } }); // collects frontend resources from the JARs generatedImports .filter((line: string) => line.includes('generated/jar-resources')) .forEach((line: string) => { let filename = line.substring(line.indexOf('generated')); // \r\n from windows made files may be used ro remove to be only \n const fileBuffer = readFileSync(path.resolve(frontendFolder, filename), { encoding: 'utf-8' }).replace( /\r\n/g, '\n' ); const hash = createHash('sha256').update(fileBuffer, 'utf8').digest('hex'); const fileKey = line.substring(line.indexOf('jar-resources/') + 14); frontendFiles[fileKey] = hash; }); // collects and hash rest of the Frontend resources excluding files in /generated/ and /themes/ // and files already in frontendFiles. let frontendFolderAlias = "Frontend"; generatedImports .filter((line: string) => line.startsWith(frontendFolderAlias + '/')) .filter((line: string) => !line.startsWith(frontendFolderAlias + '/generated/')) .filter((line: string) => !line.startsWith(frontendFolderAlias + '/themes/')) .map((line) => line.substring(frontendFolderAlias.length + 1)) .filter((line: string) => !frontendFiles[line]) .forEach((line: string) => { const filePath = path.resolve(frontendFolder, line); if (projectFileExtensions.includes(path.extname(filePath)) && existsSync(filePath)) { const fileBuffer = readFileSync(filePath, { encoding: 'utf-8' }).replace(/\r\n/g, '\n'); frontendFiles[line] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex'); } }); // If a index.ts exists hash it to be able to see if it changes. if (existsSync(path.resolve(frontendFolder, 'index.ts'))) { const fileBuffer = readFileSync(path.resolve(frontendFolder, 'index.ts'), { encoding: 'utf-8' }).replace( /\r\n/g, '\n' ); frontendFiles[`index.ts`] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex'); } if (hasCommercialBanner) { const fileBuffer = readFileSync(commercialBannerComponent, { encoding: 'utf-8' }).replace(/\r\n/g, '\n'); frontendFiles[settings.generatedFolder + '/commercial-banner.js'] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex'); } const themeJsonContents: Record = {}; const themesFolder = path.resolve(jarResourcesFolder, 'themes'); if (existsSync(themesFolder)) { readdirSync(themesFolder).forEach((themeFolder) => { const themeJson = path.resolve(themesFolder, themeFolder, 'theme.json'); if (existsSync(themeJson)) { themeJsonContents[path.basename(themeFolder)] = readFileSync(themeJson, { encoding: 'utf-8' }).replace( /\r\n/g, '\n' ); } }); } collectThemeJsonsInFrontend(themeJsonContents, settings.themeName); let webComponents: string[] = []; if (webComponentTags) { webComponents = webComponentTags.split(';'); } const stats = { packageJsonDependencies: projectPackageJson.dependencies, npmModules: npmModuleAndVersion, bundleImports: generatedImports, frontendHashes: frontendFiles, themeJsonContents: themeJsonContents, entryScripts, webComponents, cvdlModules: cvdls, packageJsonHash: projectPackageJson?.vaadin?.hash, indexHtmlGenerated: rowsGenerated }; writeFileSync(statsFile, JSON.stringify(stats, null, 1)); } }; } function themePlugin(opts: { devMode: boolean }): PluginOption { const fullThemeOptions = { ...themeOptions, devMode: opts.devMode }; return { name: 'vaadin:theme', config() { processThemeResources(fullThemeOptions, console); }, configureServer(server) { function handleThemeFileCreateDelete(themeFile: string, stats?: Stats) { if (themeFile.startsWith(themeFolder)) { const changed = path.relative(themeFolder, themeFile); console.debug('Theme file ' + (!!stats ? 'created' : 'deleted'), changed); processThemeResources(fullThemeOptions, console); } } server.watcher.on('add', handleThemeFileCreateDelete); server.watcher.on('unlink', handleThemeFileCreateDelete); }, 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 expectedComment = '/*! vaadin-dev-mode:start'; const newSrc = src.replace(DEV_MODE_START_REGEXP, expectedComment); if (newSrc === src) { if (!src.includes(expectedComment)) { console.error('vaadin-dev-mode:start tag not found'); } } else if (!newSrc.match(DEV_MODE_CODE_REGEXP)) { console.error('New comment fails to match original regexp'); } else { return { code: newSrc }; } } } return { code: src }; } }; } export const vaadinConfig: UserConfigFn = (env) => { const devMode = env.mode === 'development'; const productionMode = !devMode && !devBundle const commercialBanner = productionMode && hasCommercialBanner; if (devMode && process.env.watchDogPort) { // Open a connection with the Java dev-mode handler in order to finish // vite when it exits or crashes. runWatchDog(parseInt(process.env.watchDogPort), process.env.watchDogHost); } return { root: frontendFolder, base: '', publicDir: false, resolve: { alias: { '@vaadin/flow-frontend': jarResourcesFolder, Frontend: frontendFolder }, preserveSymlinks: true }, define: { OFFLINE_PATH: settings.offlinePath, VITE_ENABLED: 'true' }, server: { host: '127.0.0.1', strictPort: true, fs: { allow: allowedFrontendFolders } }, esbuild: { legalComments: 'inline', }, build: { minify: productionMode, outDir: buildOutputFolder, emptyOutDir: devBundle, assetsDir: 'VAADIN/build', target, rollupOptions: { input: { indexhtml: projectIndexHtml, ...(hasExportedWebComponents ? { webcomponenthtml: path.resolve(frontendFolder, 'web-component.html') } : {}) }, output: { // Workaround to enable dynamic imports with top-level await for // commonjs modules, such as "atmosphere.js" in Hilla. Extracting // Rollup's commonjs helpers into separate manual chunk avoids // circular dependencies in this case. Caused // - https://github.com/vitejs/vite/issues/10995 // - https://github.com/rollup/rollup/issues/5884 // - https://github.com/vitejs/vite/issues/19695 // - https://github.com/vitejs/vite/issues/12209 manualChunks: (id: string) => id.startsWith('\0commonjsHelpers.js') ? 'commonjsHelpers' : null }, onwarn: (warning: 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: { esbuildOptions: { target, }, entries: [ // Pre-scan entrypoints in Vite to avoid reloading on first open 'generated/vaadin.ts' ], exclude: [ '@vaadin/router', '@vaadin/vaadin-license-checker', '@vaadin/vaadin-usage-statistics', 'workbox-core', 'workbox-precaching', 'workbox-routing', 'workbox-strategies' ] }, plugins: [ productionMode && brotli(), devMode && vaadinBundlesPlugin({ nodeModulesFolder }), devMode && showRecompileReason(), settings.offlineEnabled && serviceWorkerPlugin({ srcPath: settings.clientServiceWorkerSource, }), !devMode && statsExtracterPlugin(), !productionMode && preserveUsageStats(), themePlugin({ devMode }), postcssLit({ include: ['**/*.css', /.*\/.*\.css\?.*/], exclude: [ `${themeFolder}/**/*.css`, new RegExp(`${themeFolder}/.*/.*\\.css\\?.*`), `${themeResourceFolder}/**/*.css`, new RegExp(`${themeResourceFolder}/.*/.*\\.css\\?.*`), new RegExp('.*/.*\\?html-proxy.*') ] }), // The React plugin provides fast refresh and debug source info reactPlugin({ include: '**/*.tsx', babel: { // We need to use babel to provide the source information for it to be correct // (otherwise Babel will slightly rewrite the source file and esbuild generate source info for the modified file) presets: [ [ '@babel/preset-react', { runtime: 'automatic', importSource: productionMode ? 'react' : 'Frontend/generated/jsx-dev-transform', development: !productionMode } ] ], // React writes the source location for where components are used, this writes for where they are defined plugins: [ !productionMode && addFunctionComponentSourceLocationBabel(), [ 'module:@preact/signals-react-transform', { mode: 'all' // Needed to include translations which do not use something.value } ] ].filter(Boolean) } }), productionMode && vaadinI18n({ cwd: __dirname, meta: { output: { dir: i18nFolder, }, }, }), { name: 'vaadin:force-remove-html-middleware', configureServer(server) { return () => { server.middlewares.stack = server.middlewares.stack.filter((mw) => { const handleName = `${mw.handle}`; return !handleName.includes('viteHtmlFallbackMiddleware'); }); }; }, }, hasExportedWebComponents && { name: 'vaadin:inject-entrypoints-to-web-component-html', transformIndexHtml: { order: 'pre', handler(_html, { path, server }) { if (path !== '/web-component.html') { return; } const scripts = [ { tag: 'script', attrs: { type: 'module', src: `/generated/vaadin-web-component.ts` }, injectTo: 'head' } ]; if (commercialBanner) { scripts.push({ tag: 'script', attrs: { type: 'module', src: '/generated/commercial-banner.js' }, injectTo: 'head' }); } return scripts; } } }, { name: 'vaadin:inject-entrypoints-to-index-html', transformIndexHtml: { order: 'pre', handler(_html, { path, server }) { if (path !== '/index.html') { return; } const scripts = []; if (devMode) { scripts.push({ tag: 'script', attrs: { type: 'module', src: `/generated/vite-devmode.ts`, onerror: "document.location.reload()" }, injectTo: 'head' }); } scripts.push({ tag: 'script', attrs: { type: 'module', src: '/generated/vaadin.ts' }, injectTo: 'head' }); if (commercialBanner) { scripts.push({ tag: 'script', attrs: { type: 'module', src: '/generated/commercial-banner.js' }, injectTo: 'head' }); } return scripts; } } }, vitePluginFileSystemRouter({isDevMode: devMode}), 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; }