Skip to content

Commit 9aff4b7

Browse files
committed
feat: Optimize StyleSheet css for production build
Minify and inline imports for css imported using StyleSheet annotation. Fixes #22472
1 parent 15bca07 commit 9aff4b7

File tree

12 files changed

+883
-8
lines changed

12 files changed

+883
-8
lines changed

flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ internal class GradlePluginAdapter private constructor(
239239
override fun frontendOutputDirectory(): File =
240240
config.frontendOutputDirectory.get()
241241

242+
override fun resourcesOutputDirectory(): File =
243+
config.resourcesOutputDirectory.get()
244+
242245
override fun frontendResourcesDirectory(): File =
243246
config.frontendResourcesDirectory.get()
244247

flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ internal class PrepareFrontendInputProperties(
4949
.filterExists()
5050
.absolutePath
5151

52+
@Input
53+
@Optional
54+
fun getResourcesOutputDirectory(): Provider<String> =
55+
config.resourcesOutputDirectory
56+
.filterExists()
57+
.absolutePath
58+
5259
@Input
5360
fun getNpmFolder(): Provider<String> = config.npmFolder.absolutePath
5461

flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val
6262
*/
6363
public abstract val frontendOutputDirectory: Property<File>
6464

65+
/**
66+
* The folder where the META-INF/resources files are copied. Used for
67+
* finding the StyleSheet referenced css files.
68+
* Defaults to `null` which will use the auto-detected value of
69+
* resoucesDir of the main SourceSet, usually `build/resources/main/META-INF/resources/`.
70+
*/
71+
public abstract val resourcesOutputDirectory: Property<File>
72+
6573
/**
6674
* The folder where `package.json` file is located. Default is project root
6775
* dir.
@@ -399,6 +407,21 @@ public class PluginEffectiveConfiguration(
399407
)
400408
)
401409

410+
411+
412+
public val resourcesOutputDirectory: Provider<File> =
413+
extension.resourcesOutputDirectory.convention(
414+
extension.webpackOutputDirectory
415+
.convention(
416+
sourceSetName.map {
417+
File(
418+
project.getBuildResourcesDir(it),
419+
Constants.META_INF + "resources/"
420+
)
421+
}
422+
)
423+
)
424+
402425
public val npmFolder: Provider<File> = extension.npmFolder
403426
.convention(project.projectDir)
404427

@@ -650,6 +673,7 @@ public class PluginEffectiveConfiguration(
650673
"productionMode=${productionMode.get()}, " +
651674
"applicationIdentifier=${applicationIdentifier.get()}, " +
652675
"frontendOutputDirectory=${frontendOutputDirectory.get()}, " +
676+
"resourcesOutputDirectory=${resourcesOutputDirectory.get()}, " +
653677
"npmFolder=${npmFolder.get()}, " +
654678
"frontendDirectory=${frontendDirectory.get()}, " +
655679
"generateBundle=${generateBundle.get()}, " +

flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildFrontendMojo.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
import com.vaadin.pro.licensechecker.LicenseChecker;
4949
import com.vaadin.pro.licensechecker.MissingLicenseKeyException;
5050

51+
import static com.vaadin.flow.server.Constants.META_INF;
52+
5153
/**
5254
* Goal that builds the frontend bundle.
5355
*
@@ -136,6 +138,14 @@ public class BuildFrontendMojo extends FlowModeAbstractMojo
136138
@Parameter(property = InitParameters.CLEAN_BUILD_FRONTEND_FILES, defaultValue = "true")
137139
private boolean cleanFrontendFiles;
138140

141+
/**
142+
* The folder where the META-INF/resources files are copied. Used for
143+
* finding the StyleSheet referenced css files.
144+
*/
145+
@Parameter(defaultValue = "${project.build.outputDirectory}/" + META_INF
146+
+ "resources/")
147+
private File resourcesOutputDirectory;
148+
139149
@Override
140150
protected void executeInternal()
141151
throws MojoExecutionException, MojoFailureException {
@@ -276,6 +286,11 @@ public boolean compressBundle() {
276286
return true;
277287
}
278288

289+
@Override
290+
public File resourcesOutputDirectory() {
291+
return resourcesOutputDirectory;
292+
}
293+
279294
@Override
280295
public boolean checkRuntimeDependency(String groupId, String artifactId,
281296
Consumer<String> missingDependencyMessage) {

flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ public static void runNodeUpdater(PluginAdapterBuild adapter,
374374
.withFrontendIgnoreVersionChecks(
375375
adapter.isFrontendIgnoreVersionChecks())
376376
.withFrontendDependenciesScanner(frontendDependencies)
377-
.withCommercialBanner(adapter.isCommercialBannerEnabled());
377+
.withCommercialBanner(adapter.isCommercialBannerEnabled())
378+
.withMetaInfResourcesDirectory(
379+
adapter.resourcesOutputDirectory());
378380
new NodeTasks(options).execute();
379381
} catch (ExecutionFailedException exception) {
380382
throw exception;

flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBuild.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,12 @@ public interface PluginAdapterBuild extends PluginAdapterBase {
118118
boolean checkRuntimeDependency(String groupId, String artifactId,
119119
Consumer<String> missingDependencyMessageConsumer);
120120

121+
/**
122+
* The resources output directory for META-INF/resources in the classes
123+
* output directory.
124+
*
125+
* @return the META-INF/resources directory, usually
126+
* {output}/classes/META-INF/resources
127+
*/
128+
File resourcesOutputDirectory();
121129
}

flow-server/src/main/java/com/vaadin/flow/server/frontend/CssBundler.java

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,46 @@ public class CssBundler {
106106
public static String inlineImports(File themeFolder, File cssFile,
107107
JsonNode themeJson) throws IOException {
108108
return inlineImports(themeFolder, cssFile,
109-
getThemeAssetsAliases(themeJson));
109+
getThemeAssetsAliases(themeJson), null);
110+
}
111+
112+
/**
113+
* Recurse over CSS import and inlines all of them into a single CSS block.
114+
* <p>
115+
*
116+
* Unresolvable imports are put on the top of the resulting code, because
117+
* {@code @import} statements must come before any other CSS instruction,
118+
* otherwise the import is ignored by the browser.
119+
* <p>
120+
*
121+
* This overload supports resolving imports from node_modules in addition to
122+
* relative paths.
123+
*
124+
* @param themeFolder
125+
* location of theme folder on the filesystem. May be null if not
126+
* processing theme files.
127+
* @param cssFile
128+
* the CSS file to process.
129+
* @param themeJson
130+
* the theme configuration, usually stored in
131+
* {@literal theme.json} file. May be null.
132+
* @param nodeModulesFolder
133+
* the node_modules folder for resolving npm package imports. May
134+
* be null if node_modules resolution is not needed.
135+
* @return the processed stylesheet content, with inlined imports and
136+
* rewritten URLs.
137+
* @throws IOException
138+
* if filesystem resources can not be read.
139+
*/
140+
public static String inlineImports(File themeFolder, File cssFile,
141+
JsonNode themeJson, File nodeModulesFolder) throws IOException {
142+
return inlineImports(themeFolder, cssFile,
143+
getThemeAssetsAliases(themeJson), nodeModulesFolder);
110144
}
111145

112146
private static String inlineImports(File themeFolder, File cssFile,
113-
Set<String> assetAliases) throws IOException {
147+
Set<String> assetAliases, File nodeModulesFolder)
148+
throws IOException {
114149

115150
String content = Files.readString(cssFile.toPath());
116151

@@ -171,12 +206,13 @@ private static String inlineImports(File themeFolder, File cssFile,
171206
String url = getNonNullGroup(result, 3, 4, 5, 7, 8);
172207
String sanitizedUrl = sanitizeUrl(url);
173208
if (sanitizedUrl != null && sanitizedUrl.endsWith(".css")) {
174-
File potentialFile = new File(cssFile.getParentFile(),
175-
sanitizedUrl);
176-
if (potentialFile.exists()) {
209+
File potentialFile = resolveImportPath(sanitizedUrl,
210+
cssFile.getParentFile(), nodeModulesFolder);
211+
if (potentialFile != null && potentialFile.exists()) {
177212
try {
178-
return Matcher.quoteReplacement(inlineImports(
179-
themeFolder, potentialFile, assetAliases));
213+
return Matcher.quoteReplacement(
214+
inlineImports(themeFolder, potentialFile,
215+
assetAliases, nodeModulesFolder));
180216
} catch (IOException e) {
181217
getLogger().warn(
182218
"Unable to inline import: " + result.group());
@@ -264,8 +300,69 @@ private static String sanitizeUrl(String url) {
264300
return url.trim().split("\\?")[0];
265301
}
266302

303+
/**
304+
* Resolve import path to a file. First check relative to the CSS file's,
305+
* then check node_modules for non-relative path.
306+
*
307+
* @param importPath
308+
* the import path from the CSS file
309+
* @param cssFileDir
310+
* the directory containing the CSS file
311+
* @param nodeModulesFolder
312+
* the node_modules folder, may be null
313+
* @return the resolved file, or null if not found
314+
*/
315+
private static File resolveImportPath(String importPath, File cssFileDir,
316+
File nodeModulesFolder) {
317+
// First, try relative to the CSS file's directory
318+
File relativeFile = new File(cssFileDir, importPath);
319+
if (relativeFile.exists()) {
320+
return relativeFile;
321+
}
322+
323+
// If not a relative path (doesn't start with ./ or ../) and
324+
// node_modules is available, try resolving from node_modules
325+
if (nodeModulesFolder != null && !importPath.startsWith("./")
326+
&& !importPath.startsWith("../")) {
327+
File nodeModulesFile = new File(nodeModulesFolder, importPath);
328+
if (nodeModulesFile.exists()) {
329+
return nodeModulesFile;
330+
}
331+
}
332+
333+
return null;
334+
}
335+
267336
private static Logger getLogger() {
268337
return LoggerFactory.getLogger(CssBundler.class);
269338
}
270339

340+
/**
341+
* Minify CSS content by removing comments and unnecessary whitespace.
342+
* <p>
343+
* This method performs basic CSS minification:
344+
* <ul>
345+
* <li>Remove CSS comments</li>
346+
* <li>Collapse multiple whitespace characters</li>
347+
* <li>Remove whitespace around special characters like braces and
348+
* colons</li>
349+
* <li>Remove trailing semicolons before closing braces</li>
350+
* </ul>
351+
*
352+
* @param css
353+
* the CSS content to minify
354+
* @return the minified CSS content
355+
*/
356+
public static String minifyCss(String css) {
357+
// Remove CSS comments /* ... */
358+
css = css.replaceAll("/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/", "");
359+
// Collapse whitespace
360+
css = css.replaceAll("\\s+", " ");
361+
// Remove spaces around special characters
362+
css = css.replaceAll("\\s*([{};:,>~+])\\s*", "$1");
363+
// Remove trailing semicolons before }
364+
css = css.replaceAll(";}", "}");
365+
return css.trim();
366+
}
367+
271368
}

flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public class NodeTasks implements FallibleCommand {
8989
TaskGenerateBootstrap.class,
9090
TaskRunDevBundleBuild.class,
9191
TaskPrepareProdBundle.class,
92+
TaskProcessStylesheetCss.class,
9293
TaskCleanFrontendFiles.class,
9394
TaskRemoveOldFrontendGeneratedFiles.class
9495
));
@@ -144,6 +145,8 @@ public NodeTasks(Options options) {
144145
commands.add(new TaskGenerateCommercialBanner(options));
145146
BundleUtils.copyPackageLockFromBundle(options);
146147
}
148+
// Process @StyleSheet CSS files (minify and inline @imports)
149+
commands.add(new TaskProcessStylesheetCss(options));
147150
} else if (options.isBundleBuild()) {
148151
// The dev bundle check needs the frontendDependencies to be
149152
// able to

flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ public class Options implements Serializable {
135135
*/
136136
private File javaResourceFolder;
137137

138+
/**
139+
* META-INF/resources directory.
140+
*/
141+
private File resourcesDirectory;
142+
138143
/**
139144
* Additional npm packages to run postinstall for.
140145
*/
@@ -1110,4 +1115,25 @@ public boolean copyAssets() {
11101115
}
11111116
return copyAssets;
11121117
}
1118+
1119+
/**
1120+
* Set where the META-INF/resources files are copied by the build.
1121+
*
1122+
* @param resourcesDirectory
1123+
* META-INF resources directory
1124+
* @return this builder
1125+
*/
1126+
public Options withMetaInfResourcesDirectory(File resourcesDirectory) {
1127+
this.resourcesDirectory = resourcesDirectory;
1128+
return this;
1129+
}
1130+
1131+
/**
1132+
* Get the resources directory if defined.
1133+
*
1134+
* @return META-INF resources directory
1135+
*/
1136+
public File getMetaInfResourcesDirectory() {
1137+
return resourcesDirectory;
1138+
}
11131139
}

0 commit comments

Comments
 (0)