diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java index 7a58f67..112a738 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java @@ -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; @@ -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; @@ -42,20 +49,23 @@ default String fromWebRoot(String dir) { Map 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 @@ -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> externalImports(); /** @@ -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/**\/* - * + *

* 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. - * + *

* => processed and added to static/[key].js and static/[key].css (key is "main" by default) */ PresetConfig app(); @@ -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) - * + *

* => processed and added to static/[key].js and static/[key].css (key is "main" by default) */ PresetConfig components(); diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerProcessor.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerProcessor.java index 3fe2646..4b23809 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerProcessor.java @@ -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; @@ -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) @@ -295,22 +298,25 @@ void handleBundleDistDir(WebBundlerConfig config, BuildProducer bundle = new HashMap<>(); - final String bundlePublicPath = surroundWithSlashes(config.bundleDir()); List names = new ArrayList<>(); StringBuilder mappingString = new StringBuilder(); try (Stream 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) { diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/util/PathUtils.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/util/PathUtils.java index af0c521..ac17aa1 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/util/PathUtils.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/util/PathUtils.java @@ -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; } diff --git a/deployment/src/test/java/io/quarkiverse/web/bundler/deployment/util/ConfiguredPathsTest.java b/deployment/src/test/java/io/quarkiverse/web/bundler/deployment/util/ConfiguredPathsTest.java index 0cc24f9..6b62b2e 100644 --- a/deployment/src/test/java/io/quarkiverse/web/bundler/deployment/util/ConfiguredPathsTest.java +++ b/deployment/src/test/java/io/quarkiverse/web/bundler/deployment/util/ConfiguredPathsTest.java @@ -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; @@ -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/")); } } diff --git a/docs/modules/ROOT/pages/advanced-guides.adoc b/docs/modules/ROOT/pages/advanced-guides.adoc index a487bfe..0d26f7e 100644 --- a/docs/modules/ROOT/pages/advanced-guides.adoc +++ b/docs/modules/ROOT/pages/advanced-guides.adoc @@ -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/**` 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/**` 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 <>) allowing different options (like embedding them as data-url). + + + +NOTE: == Bundling @@ -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/**` are marked as external (to be ignored from the bundling). Since `/static/example.png` will be served (See <>), 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/**` are marked as external (to be ignored from the bundling). Since `/static/example.png` will be served by Quarkus (See <>), it is ok. === SCSS, SASS @@ -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. diff --git a/docs/modules/ROOT/pages/includes/quarkus-web-bundler.adoc b/docs/modules/ROOT/pages/includes/quarkus-web-bundler.adoc index 540c189..2477553 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-web-bundler.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-web-bundler.adoc @@ -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+++[] @@ -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` @@ -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]` diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index b782dda..15077ef 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -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 \ No newline at end of file +quarkus.web-bundler.bundle.page-1=true +quarkus.http.root-path=/foo \ No newline at end of file diff --git a/integration-tests/src/main/resources/templates/WebResource/page1.html b/integration-tests/src/main/resources/templates/WebResource/page1.html index 9e2857a..68d50bc 100644 --- a/integration-tests/src/main/resources/templates/WebResource/page1.html +++ b/integration-tests/src/main/resources/templates/WebResource/page1.html @@ -11,7 +11,7 @@

Hello QWA

Wait for it...

- index + index
\ No newline at end of file diff --git a/integration-tests/src/main/resources/web/app/styles/global.css b/integration-tests/src/main/resources/web/app/styles/global.css index 4370f89..f73a742 100644 --- a/integration-tests/src/main/resources/web/app/styles/global.css +++ b/integration-tests/src/main/resources/web/app/styles/global.css @@ -13,6 +13,6 @@ h1 { } #message { - background-image: url('/static/images/logo.svg'); + background-image: url('/foo/static/images/logo.svg'); color: coral; } \ No newline at end of file diff --git a/integration-tests/src/main/resources/web/components/calendar/calendar.html b/integration-tests/src/main/resources/web/components/calendar/calendar.html index 64d0d89..76212ee 100644 --- a/integration-tests/src/main/resources/web/components/calendar/calendar.html +++ b/integration-tests/src/main/resources/web/components/calendar/calendar.html @@ -1,4 +1,4 @@

Calendar

- page1 + page1
diff --git a/integration-tests/src/test/java/io/quarkiverse/web/bundler/it/BundleTest.java b/integration-tests/src/test/java/io/quarkiverse/web/bundler/it/BundleTest.java index d1600d0..dfdaa4f 100644 --- a/integration-tests/src/test/java/io/quarkiverse/web/bundler/it/BundleTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/web/bundler/it/BundleTest.java @@ -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; @@ -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); }