diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java index 1e06f6aa3fc..d2482c12e98 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java @@ -118,6 +118,8 @@ abstract class AbstractUpdateImports implements Runnable { final File generatedFlowImports; final File generatedFlowWebComponentImports; private final File generatedFlowDefinitions; + final File appShellImports; + final File appShellDefinitions; private File chunkFolder; private final GeneratedFilesSupport generatedFilesSupport; @@ -141,6 +143,12 @@ abstract class AbstractUpdateImports implements Runnable { generatedFlowDefinitions = new File( generatedFlowImports.getParentFile(), FrontendUtils.IMPORTS_D_TS_NAME); + var generatedFolder = FrontendUtils + .getFrontendGeneratedFolder(options.getFrontendDirectory()); + appShellImports = new File(generatedFolder, + FrontendUtils.APP_SHELL_IMPORTS_NAME); + appShellDefinitions = new File(generatedFolder, + FrontendUtils.APP_SHELL_IMPORTS_D_TS_NAME); generatedFlowWebComponentImports = FrontendUtils .getFlowGeneratedWebComponentsImports( @@ -160,8 +168,8 @@ public void run() { Map> output = process(css, javascript); writeOutput(output); - writeWebComponentImports( - filterWebComponentImports(output.get(generatedFlowImports))); + writeWebComponentImports(filterWebComponentImports( + mergeWebComponentOutputLines(output))); getLogger().debug("Imports and chunks update took {} ms.", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); @@ -250,6 +258,17 @@ private void adaptCssInjectForWebComponent(ListIterator iterator, } } + private List mergeWebComponentOutputLines( + Map> outputFiles) { + return Stream.concat( + outputFiles + .getOrDefault(appShellImports, Collections.emptyList()) + .stream(), + outputFiles.getOrDefault(generatedFlowImports, + Collections.emptyList()).stream()) + .distinct().toList(); + } + private void writeWebComponentImports(List lines) { if (lines != null) { try { @@ -276,6 +295,7 @@ private void writeWebComponentImports(List lines) { private Map> process(Map> css, Map> javascript) { getLogger().debug("Start sorting imports to lazy and eager."); + int cssLineOffset = 0; long start = System.nanoTime(); Map> files = new HashMap<>(); @@ -284,6 +304,7 @@ private Map> process(Map> css, List eagerJavascript = new ArrayList<>(); Map> lazyCss = new LinkedHashMap<>(); List eagerCssData = new ArrayList<>(); + List appShellCssData = new ArrayList<>(); for (Entry> entry : javascript.entrySet()) { if (isLazyRoute(entry.getKey())) { lazyJavascript.put(entry.getKey(), entry.getValue()); @@ -296,12 +317,18 @@ private Map> process(Map> css, boolean hasThemeFor = entry.getValue().stream() .anyMatch(cssData -> cssData.getThemefor() != null); if (isLazyRoute(entry.getKey()) && !hasThemeFor) { - List cssLines = getCssLines(entry.getValue()); + List cssLines = getCssLines(entry.getValue(), + cssLineOffset); + cssLineOffset += cssLines.size(); if (!cssLines.isEmpty()) { lazyCss.put(entry.getKey(), cssLines); } } else { - eagerCssData.addAll(entry.getValue()); + if (entry.getKey().equals(ChunkInfo.APP_SHELL)) { + appShellCssData.addAll(entry.getValue()); + } else { + eagerCssData.addAll(entry.getValue()); + } } } @@ -372,10 +399,22 @@ private Map> process(Map> css, "const loadOnDemand = (key) => { return Promise.resolve(0); }"); } + List appShellLines = new ArrayList<>(); + List appShellCssLines = getCssLines(appShellCssData, + cssLineOffset); + cssLineOffset += appShellCssLines.size(); + if (!appShellCssLines.isEmpty()) { + appShellLines.add(IMPORT_INJECT); + appShellLines.addAll(appShellCssLines); + } + files.put(appShellImports, appShellLines); + files.put(appShellDefinitions, Collections.singletonList("export {}")); + List mainLines = new ArrayList<>(); // Convert eager CSS data to JS and deduplicate it - List mainCssLines = getCssLines(eagerCssData); + List mainCssLines = getCssLines(eagerCssData, cssLineOffset); + cssLineOffset += mainCssLines.size(); if (!mainCssLines.isEmpty()) { mainLines.add(IMPORT_INJECT); mainLines.add(THEMABLE_MIXIN_IMPORT); @@ -470,12 +509,12 @@ String resolveGeneratedModule(String module) { * the CSS import data * @return the JS statements needed to import and apply the CSS data */ - protected List getCssLines(List css) { + private List getCssLines(List css, int startOffset) { List lines = new ArrayList<>(); Set cssNotFound = new HashSet<>(); LinkedHashSet allCss = new LinkedHashSet<>(css); - int i = 0; + int i = startOffset; for (CssData cssData : allCss) { if (!addCssLines(lines, cssData, i)) { cssNotFound.add(cssData.getValue()); @@ -561,9 +600,9 @@ private Map> mergeJavascript( } - protected List merge(Map> css) { + protected List merge(Map> outputFiles) { List result = new ArrayList<>(); - css.forEach((key, value) -> result.addAll(value)); + outputFiles.forEach((key, value) -> result.addAll(value)); return result; } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java index 5c63019fac4..c54ec095b1e 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java @@ -189,6 +189,18 @@ public class FrontendUtils { public static final String THEME_IMPORTS_D_TS_NAME = "theme.d.ts"; public static final String THEME_IMPORTS_NAME = "theme.js"; + /** + * The name of the file that contains application shell imports, such as + * style imports for the theme. + */ + public static final String APP_SHELL_IMPORTS_NAME = "app-shell-imports.js"; + + /** + * The TypeScript definitions for the + * {@link FrontendUtils#APP_SHELL_IMPORTS_NAME} + */ + public static final String APP_SHELL_IMPORTS_D_TS_NAME = "app-shell-imports.d.ts"; + /** * File name of the bootstrap file that is generated in frontend * {@link #GENERATED} folder. The bootstrap file is always executed in a diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateBootstrap.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateBootstrap.java index ba01d6955db..bcb543ff741 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateBootstrap.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateBootstrap.java @@ -116,6 +116,7 @@ private String getIndexTsEntryPath() { private Collection getThemeLines() { Collection lines = new ArrayList<>(); + lines.add("import './app-shell-imports.js';"); ThemeDefinition themeDef = frontDeps.getThemeDefinition(); if (themeDef != null && !"".equals(themeDef.getName())) { lines.add("import './theme-" + themeDef.getName() diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/ChunkInfo.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/ChunkInfo.java index e3b8e6f6787..6cff85a812e 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/ChunkInfo.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/ChunkInfo.java @@ -26,13 +26,20 @@ * loaded immediately when the JS bundle is loaded while chunks marked as not * eager (i.e. lazy) are loaded on demand later. *

- * There is one special, global chunk, defined as {@link #GLOBAL} in this class, + * There is a special application shell chunk, defined as {@link #APP_SHELL} in + * this class, which is used for gathering all data that relates to the + * application shell. + *

+ * There is a special global chunk, defined as {@link #GLOBAL} in this class, * which is used for gathering all data that relates to internal entry points. *

* For internal use only. May be renamed or removed in a future release. **/ public class ChunkInfo { + public static final ChunkInfo APP_SHELL = new ChunkInfo( + EntryPointType.INTERNAL, null, null, true); + public static final ChunkInfo GLOBAL = new ChunkInfo( EntryPointType.INTERNAL, null, null, false); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/EntryPointType.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/EntryPointType.java index e0d35f5389e..1d6d5b59f64 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/EntryPointType.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/EntryPointType.java @@ -24,5 +24,5 @@ * For internal use only. May be renamed or removed in a future release. */ public enum EntryPointType { - ROUTE, WEB_COMPONENT, INTERNAL; + ROUTE, WEB_COMPONENT, INTERNAL, APP_SHELL; } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FrontendDependencies.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FrontendDependencies.java index 7b691516bc1..840beec48e6 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FrontendDependencies.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FrontendDependencies.java @@ -125,11 +125,8 @@ public FrontendDependencies(ClassFinder finder, if (themeDefinition != null && themeDefinition.getTheme() != null) { Class themeClass = themeDefinition .getTheme(); - if (!visitedClasses.containsKey(themeClass.getName())) { - addInternalEntryPoint(themeClass); - visitEntryPoint(entryPoints.get(themeClass.getName())); - visitedClasses.get(themeClass.getName()).loadCss = true; - } + addAppShellEntryPoint(themeClass); + visitEntryPoint(entryPoints.get(themeClass.getName())); } if (reactEnabled) { computeReactClasses(finder); @@ -186,6 +183,8 @@ private void computeReactClasses(ClassFinder finder) throws IOException { } private void aggregateEntryPointInformation() { + var activeTheme = themeDefinition != null + && themeDefinition.getTheme() != null; for (Entry entry : entryPoints.entrySet()) { EntryPointData entryPoint = entry.getValue(); for (String className : entryPoint.reachableClasses) { @@ -193,7 +192,8 @@ private void aggregateEntryPointInformation() { entryPoint.getModules().addAll(classInfo.modules); entryPoint.getModulesDevelopmentOnly() .addAll(classInfo.modulesDevelopmentOnly); - if (classInfo.loadCss) { + if (classInfo.loadCss || activeTheme + && entryPoint.getType() == EntryPointType.APP_SHELL) { entryPoint.getCss().addAll(classInfo.css); } entryPoint.getScripts().addAll(classInfo.scripts); @@ -322,6 +322,9 @@ public Map> getModulesDevelopment() { } private ChunkInfo getChunkInfo(EntryPointData data) { + if (data.getType() == EntryPointType.APP_SHELL) { + return ChunkInfo.APP_SHELL; + } if (data.getType() == EntryPointType.INTERNAL) { return ChunkInfo.GLOBAL; } @@ -458,7 +461,7 @@ private void collectEntryPoints(boolean generateEmbeddableWebComponents) for (Class appShell : getFinder().getSubTypesOf( getFinder().loadClass(AppShellConfigurator.class.getName()))) { - addInternalEntryPoint(appShell); + addAppShellEntryPoint(appShell); } try { @@ -547,6 +550,10 @@ private List getDependencyTriggers(Class route, return null; } + private void addAppShellEntryPoint(Class entryPointClass) { + addEntryPoint(entryPointClass, EntryPointType.APP_SHELL, null, true); + } + private void addInternalEntryPoint(Class entryPointClass) { addEntryPoint(entryPointClass, EntryPointType.INTERNAL, null, true); } @@ -631,9 +638,6 @@ private void computeApplicationTheme() throws ClassNotFoundException, themeDefinition = new ThemeDefinition(theme, variant, themeName); themeInstance = new ThemeWrapper(theme); - classesWithTheme.get(themeData).children.stream() - .map(visitedClasses::get).filter(Objects::nonNull) - .forEach(classInfo -> classInfo.loadCss = true); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FullDependenciesScanner.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FullDependenciesScanner.java index 9aaae80475f..07cdb920074 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FullDependenciesScanner.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/scanner/FullDependenciesScanner.java @@ -73,6 +73,7 @@ class FullDependenciesScanner extends AbstractDependenciesScanner { private Map devPackages; private HashMap> assets = new HashMap<>(); private HashMap> devAssets = new HashMap<>(); + private List themeCssData; private List cssData; private List scripts; private List scriptsDevelopment; @@ -150,7 +151,10 @@ class FullDependenciesScanner extends AbstractDependenciesScanner { collectScripts(modulesSet, modulesSetDevelopment, JsModule.class); collectScripts(scriptsSet, scriptsSetDevelopment, JavaScript.class); - cssData = discoverCss(); + + themeCssData = new ArrayList<>(); + cssData = new ArrayList<>(); + discoverCss(); if (!reactEnabled) { modulesSet.removeIf( @@ -214,8 +218,9 @@ public Map> getScriptsDevelopment() { @Override public Map> getCss() { - return Collections.singletonMap(ChunkInfo.GLOBAL, - new ArrayList<>(cssData)); + // Map theme CSS to the APP_SHELL chunk + return Map.ofEntries(Map.entry(ChunkInfo.APP_SHELL, themeCssData), + Map.entry(ChunkInfo.GLOBAL, cssData)); } @Override @@ -307,27 +312,34 @@ private void discoverPackages(final Map packages, } } - private List discoverCss() { + private void discoverCss() { try { Class loadedAnnotation = getFinder() .loadClass(CssImport.class.getName()); Set> annotatedClasses = getFinder() .getAnnotatedClasses(loadedAnnotation); - LinkedHashSet result = new LinkedHashSet<>(); + var themeCss = new LinkedHashSet(); + var globalCss = new LinkedHashSet(); for (Class clazz : annotatedClasses) { classes.add(clazz.getName()); - if (AbstractTheme.class.isAssignableFrom(clazz) - && (themeDefinition == null - || !clazz.equals(themeDefinition.getTheme()))) { + var isAppShellClass = AppShellConfigurator.class + .isAssignableFrom(clazz); + var isThemeClass = AbstractTheme.class.isAssignableFrom(clazz); + if (isThemeClass && (themeDefinition == null + || !clazz.equals(themeDefinition.getTheme()))) { // Do not add css from all found theme classes, // only defined theme. continue; } List imports = annotationFinder .apply(clazz, loadedAnnotation); - imports.stream().forEach(imp -> result.add(createCssData(imp))); + imports.stream() + .forEach(imp -> ((isAppShellClass || isThemeClass) + ? themeCss + : globalCss).add(createCssData(imp))); } - return new ArrayList<>(result); + themeCssData.addAll(themeCss); + cssData.addAll(globalCss); } catch (ClassNotFoundException exception) { throw new IllegalStateException( COULD_NOT_LOAD_ERROR_MSG + CssData.class.getName(), diff --git a/flow-server/src/main/resources/vite.generated.ts b/flow-server/src/main/resources/vite.generated.ts index 28cd194e5d1..fbd93a435ce 100644 --- a/flow-server/src/main/resources/vite.generated.ts +++ b/flow-server/src/main/resources/vite.generated.ts @@ -181,6 +181,10 @@ function statsExtracterPlugin(): PluginOption { 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 = {}; diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java index 472f4c2f11f..437381bbdde 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java @@ -63,6 +63,7 @@ import com.vaadin.flow.server.frontend.scanner.DepsTests; import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner; import com.vaadin.flow.theme.AbstractTheme; +import com.vaadin.flow.theme.Theme; import com.vaadin.tests.util.MockOptions; import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR; @@ -104,6 +105,11 @@ public static class FooCssImport2 extends Component { } + @Theme(themeClass = LumoTest.class) + @CssImport("./foo.css") + public static class ThemeCssImport implements AppShellConfigurator { + } + protected File tmpRoot; protected File frontendDirectory; protected File nodeModulesPath; @@ -350,12 +356,6 @@ public void generateLines_resultingLinesContainsThemeLinesAndExpectedImportsAndC "import \\$cssFromFile_\\d from 'Frontend/foo.css\\?inline';"); expectedLines.add( "import \\$cssFromFile_\\d from 'Frontend/foo.css\\?inline';"); - expectedLines.add( - "import \\$cssFromFile_\\d from 'Frontend/foo.css\\?inline';"); - expectedLines.add( - "import \\$cssFromFile_\\d from 'lumo-css-import.css\\?inline';"); - expectedLines.add( - "injectGlobalCss\\(\\$cssFromFile_\\d.toString\\(\\), 'CSSImport end', document\\);"); expectedLines.add( "injectGlobalCss\\(\\$cssFromFile_\\d.toString\\(\\), 'CSSImport end', document\\);"); expectedLines.add( @@ -374,8 +374,23 @@ public void generateLines_resultingLinesContainsThemeLinesAndExpectedImportsAndC expectedLines .add("import 'Frontend/generated/flow/generated-modules-bar';"); + // AppShell and @Theme CSS imports are expected to be generated in + // the dedicated file. + List expectedAppShellImports = List.of( + "import \\$cssFromFile_\\d from 'lumo-css-import.css\\?inline';", + "injectGlobalCss\\(\\$cssFromFile_\\d.toString\\(\\), 'CSSImport end', document\\);"); + updater.run(); + List appShellImports = updater.getOutput() + .get(updater.appShellImports); + String appShellOutput = String.join("\n", appShellImports); + for (String line : expectedAppShellImports) { + Assert.assertTrue( + "\n" + line + " IS NOT FOUND IN: \n" + appShellImports, + Pattern.compile(line).matcher(appShellOutput).find()); + } + List mergedOutput = updater.getMergedOutput(); String outputString = String.join("\n", mergedOutput); for (String line : expectedLines) { diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java index 436497a790c..edecade8c36 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java @@ -293,6 +293,39 @@ public void lazyRouteTriggeredByOtherComponents() throws Exception { mainImportContent.contains("key === '" + routeHash + "'")); } + @Test + public void cssImportFromAppShellAndThemeWork() throws Exception { + Class[] testClasses = { ThemeCssImport.class, UI.class }; + ClassFinder classFinder = getClassFinder(testClasses); + updater = new UpdateImports(getScanner(classFinder), options); + updater.run(); + + Map> output = updater.getOutput(); + + Assert.assertNotNull(output); + Assert.assertEquals(4, output.size()); + + Optional appShellFile = output.keySet().stream() + .filter(file -> file.getName().endsWith("app-shell-imports.js")) + .findAny(); + Assert.assertTrue(appShellFile.isPresent()); + List appShellLines = output.get(appShellFile.get()); + + assertOnce( + "import { injectGlobalCss } from 'Frontend/generated/jar-resources/theme-util.js';", + appShellLines); + assertOnce("from 'Frontend/foo.css?inline';", appShellLines); + assertOnce("from 'lumo-css-import.css?inline';", appShellLines); + + Optional appShellDTsFile = output.keySet().stream().filter( + file -> file.getName().endsWith("app-shell-imports.d.ts")) + .findAny(); + Assert.assertTrue(appShellDTsFile.isPresent()); + List appShellDTsLines = output.get(appShellDTsFile.get()); + Assert.assertEquals(1, appShellDTsLines.size()); + assertOnce("export {}", appShellDTsLines); + } + @Test public void cssInLazyChunkWorks() throws Exception { Class[] testClasses = { FooCssImport.class, UI.class }; @@ -303,7 +336,7 @@ public void cssInLazyChunkWorks() throws Exception { Map> output = updater.getOutput(); Assert.assertNotNull(output); - Assert.assertEquals(3, output.size()); + Assert.assertEquals(5, output.size()); Optional chunkFile = findOptionalChunkFile(output); Assert.assertTrue(chunkFile.isPresent()); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/scanner/FrontendDependenciesTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/scanner/FrontendDependenciesTest.java index 00fc7b2ac6f..bcc850edeb8 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/scanner/FrontendDependenciesTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/scanner/FrontendDependenciesTest.java @@ -103,7 +103,7 @@ public void appShellConfigurator_collectedAsEntryPoint() FrontendDependencies dependencies = new FrontendDependencies( classFinder, false, null, true); - Assert.assertEquals("UI, AppShell should be found", 2, + Assert.assertEquals("UI, AppShell should be found", 3, dependencies.getEntryPoints().size()); AbstractTheme theme = dependencies.getTheme();