Skip to content

Commit

Permalink
Merge pull request #125 from quarkiverse/public-path
Browse files Browse the repository at this point in the history
Handle public path for bundling and optionally allow CDN
  • Loading branch information
ia3andy authored Nov 28, 2023
2 parents e24dbd4 + 6422c90 commit 44ad12f
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkiverse.web.bundler.deployment;

import static io.quarkiverse.web.bundler.deployment.util.PathUtils.addTrailingSlash;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.join;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.prefixWithSlash;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.removeLeadingSlash;
import static java.util.function.Predicate.not;

Expand All @@ -12,8 +14,13 @@
import java.util.function.Predicate;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;

import io.quarkus.maven.dependency.Dependency;
import io.quarkus.runtime.annotations.ConfigDocDefault;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
Expand Down Expand Up @@ -42,20 +49,23 @@ default String fromWebRoot(String dir) {
Map<String, EntryPointConfig> bundle();

/**
* Any static file to be served under this path
* Resources located in {quarkus.web-bundler.web-root}/{quarkus.web-bundler.static} will be served by Quarkus.
* This directory path is also used as prefix for serving
* (e.g. {quarkus.web-bundler.web-root}/static/foo.png will be served on {quarkus.http.root-path}/static/foo.png)
*/
@WithName("static")
@WithDefault("static")
@NotBlank
@Pattern(regexp = "")
String staticDir();

/**
* Bundle files will be served under this path
* When configured with an internal path (e.g. 'foo/bar'), Bundle files will be served on this path by Quarkus (prefixed by
* {quarkus.http.root-path}).
* When configured with an external URL (e.g. 'https://my.cdn.org/'), Bundle files will NOT be served by Quarkus
* and all resolved paths in the bundle and mapping will automatically point to this url (a CDN for example).
*/
@WithName("bundle")
@WithDefault("static/bundle")
@NotBlank
String bundleDir();
String bundlePath();

/**
* The config for presets
Expand All @@ -71,6 +81,7 @@ default String fromWebRoot(String dir) {
* This defines the list of external paths for esbuild (https://esbuild.github.io/api/#external).
* Instead of being bundled, the import will be preserved.
*/
@ConfigDocDefault("{quarkus.http.root-path}static/*")
Optional<List<String>> externalImports();

/**
Expand All @@ -91,15 +102,34 @@ default String fromWebRoot(String dir) {
@WithDefault("UTF-8")
Charset charset();

default String httpRootPath() {
Config allConfig = ConfigProvider.getConfig();
final String rootPath = allConfig.getOptionalValue("quarkus.http.root-path", String.class)
.orElse("/");
return prefixWithSlash(rootPath);
}

default String publicBundlePath() {
return isExternalBundlePath() ? bundlePath() : join(httpRootPath(), bundlePath());
}

default boolean isExternalBundlePath() {
return bundlePath().matches("^https?://.*");
}

default boolean shouldQuarkusServeBundle() {
return !isExternalBundlePath();
}

interface PresetsConfig {

/**
* Configuration preset to allow defining the web app with scripts and styles to bundle.
* - {web-root}/app/**\/*
*
* <p>
* If an index.js/ts is detected, it will be used as entry point for your app.
* If not found the entry point will be auto-generated with all the files in the app directory.
*
* <p>
* => processed and added to static/[key].js and static/[key].css (key is "main" by default)
*/
PresetConfig app();
Expand All @@ -110,7 +140,7 @@ interface PresetsConfig {
* - /{web-root}/components/[name]/[name].js/ts
* - /{web-root}/components/[name]/[name].scss/css
* - /{web-root}/components/[name]/[name].html (Qute tag)
*
* <p>
* => processed and added to static/[key].js and static/[key].css (key is "main" by default)
*/
PresetConfig components();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static io.quarkiverse.web.bundler.deployment.ProjectResourcesScanner.readTemplateContent;
import static io.quarkiverse.web.bundler.deployment.items.BundleWebAsset.BundleType.MANUAL;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.join;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.prefixWithSlash;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.surroundWithSlashes;
import static io.quarkiverse.web.bundler.runtime.qute.WebBundlerQuteContextRecorder.WEB_BUNDLER_ID_PREFIX;
Expand Down Expand Up @@ -156,13 +157,15 @@ void bundle(WebBundlerConfig config,
loaders.put(".scss", EsBuildConfig.Loader.CSS);
final EsBuildConfigBuilder esBuildConfigBuilder = new EsBuildConfigBuilder()
.loader(loaders)
.publicPath(config.publicBundlePath())
.splitting(config.bundleSplitting())
.addExternal(surroundWithSlashes(config.staticDir()) + "*")
.minify(launchMode.getLaunchMode().equals(LaunchMode.NORMAL));
if (config.externalImports().isPresent()) {
for (String e : config.externalImports().get()) {
esBuildConfigBuilder.addExternal(e);
}
} else {
esBuildConfigBuilder.addExternal(join(config.httpRootPath(), "static/*"));
}
final BundleOptionsBuilder options = new BundleOptionsBuilder()
.setWorkFolder(targetDir)
Expand Down Expand Up @@ -295,22 +298,25 @@ void handleBundleDistDir(WebBundlerConfig config, BuildProducer<GeneratedBundleB
boolean changed) {
try {
Map<String, String> bundle = new HashMap<>();
final String bundlePublicPath = surroundWithSlashes(config.bundleDir());
List<String> names = new ArrayList<>();
StringBuilder mappingString = new StringBuilder();
try (Stream<Path> stream = Files.find(bundleDir, 20, (p, i) -> Files.isRegularFile(p))) {
stream.forEach(path -> {
final String relativePath = bundleDir.relativize(path).toString();
final String key = relativePath.replaceAll("-[^-.]+\\.", ".");
final String publicPath = bundlePublicPath + relativePath;
final String publicBundleAssetPath = join(config.publicBundlePath(), relativePath);
final String fileName = path.getFileName().toString();
final String ext = fileName.substring(fileName.indexOf("."));
if (Bundle.BUNDLE_MAPPING_EXT.contains(ext)) {
mappingString.append(" ").append(key).append(" => ").append(publicPath).append("\n");
bundle.put(key, publicPath);
mappingString.append(" ").append(key).append(" => ").append(publicBundleAssetPath).append("\n");
bundle.put(key, publicBundleAssetPath);
}
names.add(publicBundleAssetPath);
if (config.shouldQuarkusServeBundle()) {
// The root-path will already be added by the static resources handler
final String resourcePath = surroundWithSlashes(config.bundlePath()) + relativePath;
makePublic(staticResourceProducer, resourcePath, path.normalize(), WatchMode.DISABLED, changed);
}
names.add(publicPath);
makePublic(staticResourceProducer, publicPath, path.normalize(), WatchMode.DISABLED, changed);
});
}
if (started != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public static String addTrailingSlash(String path) {
return path.endsWith("/") ? path : path + "/";
}

public static String join(String path1, String path2) {
return addTrailingSlash(path1) + removeLeadingSlash(path2);
}

public static String removeLeadingSlash(String path) {
return path.startsWith("/") ? path.substring(1) : path;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkiverse.web.bundler.deployment.util;

import static io.quarkiverse.web.bundler.deployment.util.PathUtils.addTrailingSlash;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.join;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.prefixWithSlash;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.removeLeadingSlash;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.removeTrailingSlash;
Expand All @@ -20,6 +21,10 @@ void test() {
assertEquals("hello", removeLeadingSlash("hello"));
assertEquals("hello/", addTrailingSlash("hello"));
assertEquals("hello/", addTrailingSlash("hello/"));
assertEquals("hello/foo", join("hello/", "foo"));
assertEquals("hello/foo/", join("hello/", "foo/"));
assertEquals("http://hello/foo/", join("http://hello", "/foo/"));
assertEquals("http://hello/foo/", join("http://hello", "foo/"));
}

}
21 changes: 14 additions & 7 deletions docs/modules/ROOT/pages/advanced-guides.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ The Web Root is `src/main/resources/web`, this is where the Web Bundler will loo
[#static]
== Static files

Files in `src/main/resources/web/static/&#42;&#42;` will be served statically under http://localhost:8080/static/ (you can choose another directory name xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.static[Config Reference])
There are 2 ways to add static files (fonts, images, music, video, ...) to your app:
- Files in `src/main/resources/web/static/&#42;&#42;` will be served statically under http://localhost:8080/static/ (you can choose another directory name xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.static[Config Reference]). For convenience, those static files are excluded (marked as external) from the bundling by default. This allows to reference them from scripts or styles without errors (e.g. `import '/static/foo.png';`).
- Other files imported from scripts or styles will be bundled and processed by the configured loaders (see <<loaders>>) allowing different options (like embedding them as data-url).



NOTE:

== Bundling

Expand Down Expand Up @@ -120,14 +126,13 @@ quarkus.web-bundler.presets.components.key=components // <2>
[#loaders]
=== How is it bundled (Loaders)

Depending on the app file extensions the Web Bundler will use xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.loaders.js[pre-configured loaders] to bundle the app.
Bases on the files extensions, the Web Bundler will use xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.loaders.js[pre-configured loaders] to bundle them. For scripts and styles, the default configuration should be enough.

By default, you can import and use fonts and images from your scripts and styles (svg, gif, png, jpg, ...) using their relative path, they will be automatically copied and served using the xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.loaders.file[file loader].
For other assets (svg, gif, png, jpg, ttf, ...) imported from your scripts and styles using their relative path, you may choose the loader based on the file extension allowing different options (e.g. serving, embedding the file as data-url, binary, base64, ...). By default, they will automatically be copied and served using the xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.loaders.file[file loader].

In a css file, using `url('./example.png')` will be processed by a loader (see xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.loaders.file[file loader]), the file will be copied with a static name and the path will be replaced by the new file static path (e.g. `/static/bundle/assets/example-QH383.png`).

NOTE: For convenience, when using a static file (e.g. `url('/static/example.png')`, the path will not be processed because all files under `/static/&#42;&#42;` are marked as external (to be ignored from the bundling). Since `/static/example.png` will be served (See <<static>>), it is ok.
For example, `url('./example.png')` in a style or `import example from './example.png';` in a script will be processed, the file will be copied with a static name and the path will be replaced by the new file static path (e.g. `/static/bundle/assets/example-QH383.png`). The `example` variable will contain the public path to this file to be used in a component img `src` for example.

NOTE: For convenience, when using a file located in the static directory (e.g. `url('/static/example.png')`, the path will not be processed because all files under `/static/&#42;&#42;` are marked as external (to be ignored from the bundling). Since `/static/example.png` will be served by Quarkus (See <<static>>), it is ok.

=== SCSS, SASS

Expand Down Expand Up @@ -259,7 +264,9 @@ quarkus.web-bundler.dependencies.type=webjars
[#bundle-paths]
== Bundle Paths

After the bundling is done, the bundle files are served under `/static/bundle/...`.
After the bundling is done, the bundle files will be served by Quarkus under `{quarkus.http.root-path}/static/bundle/...` by default (xref:config-reference.adoc#quarkus-web-bundler_quarkus.web-bundler.bundle-path[Config Reference]).

This may also be configured with an external URL (e.g. 'https://my.cdn.org/'), in which case, Bundle files will NOT be served by Quarkus and all resolved paths in the bundle and mapping will automatically point to this url (a CDN for example).

In production, it is a good practise to have a hash inserted in the scripts and styles file names (E.g.: `main-XKHKUJNQ.js`) to differentiate builds (make them static). This way they can be cached without a risk of missing the most recent builds.

Expand Down
14 changes: 7 additions & 7 deletions docs/modules/ROOT/pages/includes/quarkus-web-bundler.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-web-bundler_quarkus.web-bundler

[.description]
--
Any static file to be served under this path
Resources located in ++{++quarkus.web-bundler.web-root++}++/++{++quarkus.web-bundler.static++}++ will be served by Quarkus. This directory path is also used as prefix for serving (e.g. ++{++quarkus.web-bundler.web-root++}++/static/foo.png will be served on ++{++quarkus.http.root-path++}++/static/foo.png)

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_WEB_BUNDLER_STATIC+++[]
Expand All @@ -42,19 +42,19 @@ endif::add-copy-button-to-env-var[]
|`static`


a|icon:lock[title=Fixed at build time] [[quarkus-web-bundler_quarkus.web-bundler.bundle]]`link:#quarkus-web-bundler_quarkus.web-bundler.bundle[quarkus.web-bundler.bundle]`
a|icon:lock[title=Fixed at build time] [[quarkus-web-bundler_quarkus.web-bundler.bundle-path]]`link:#quarkus-web-bundler_quarkus.web-bundler.bundle-path[quarkus.web-bundler.bundle-path]`

[.description]
--
Bundle files will be served under this path
When configured with an internal path (e.g. 'foo/bar'), Bundle files will be served on this path by Quarkus (prefixed by ++{++quarkus.http.root-path++}++). When configured with an external URL (e.g. 'https://my.cdn.org/'), Bundle files will NOT be served by Quarkus and all resolved paths in the bundle and mapping will automatically point to this url (a CDN for example).

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_WEB_BUNDLER_BUNDLE+++[]
Environment variable: env_var_with_copy_button:+++QUARKUS_WEB_BUNDLER_BUNDLE_PATH+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_WEB_BUNDLER_BUNDLE+++`
Environment variable: `+++QUARKUS_WEB_BUNDLER_BUNDLE_PATH+++`
endif::add-copy-button-to-env-var[]
--|String
--|string
|`static/bundle`


Expand Down Expand Up @@ -375,7 +375,7 @@ ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_WEB_BUNDLER_EXTERNAL_IMPORTS+++`
endif::add-copy-button-to-env-var[]
--|list of string
|
|`{quarkus.http.root-path}static/*`


a|icon:lock[title=Fixed at build time] [[quarkus-web-bundler_quarkus.web-bundler.bundle-splitting]]`link:#quarkus-web-bundler_quarkus.web-bundler.bundle-splitting[quarkus.web-bundler.bundle-splitting]`
Expand Down
3 changes: 2 additions & 1 deletion integration-tests/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
quarkus.log.category."io.quarkiverse.web.bundler".level=DEBUG
quarkus.log.category."io.mvnpm.esbuild".level=DEBUG
quarkus.web-bundler.bundle.page-1=true
quarkus.web-bundler.bundle.page-1=true
quarkus.http.root-path=/foo
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<div class="container-sm">
<h1>Hello QWA</h1>
<p id="message">Wait for it...</p>
<a href="/" >index</a>
<a href="/foo" >index</a>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ h1 {
}

#message {
background-image: url('/static/images/logo.svg');
background-image: url('/foo/static/images/logo.svg');
color: coral;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="calendar">
<h1>Calendar</h1>
<a href="page1" id="page1">page1</a>
<a href="/foo/page1" id="page1">page1</a>
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkiverse.web.bundler.it;

import jakarta.inject.Inject;
import jakarta.ws.rs.core.UriBuilder;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -32,7 +33,9 @@ void testBundled() {
"main.js");

for (String name : bundle.mapping().names()) {
RestAssured.get(bundle.resolve(name))
RestAssured.given()
.basePath("")
.get(UriBuilder.fromUri(bundle.resolve(name)).build())
.then()
.statusCode(200);
}
Expand Down

0 comments on commit 44ad12f

Please sign in to comment.