diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8266bb154d..fa0ddbeb66 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
* Focus properly Widget Editor modals when opened. Keep the previous active focus on the modal when closing the widget editor.
* a11y improvements for context menus.
* Fixes broken widget preview URL when the image is overridden (module improve) and external build module is registered.
+* Inject dynamic custom bundle CSS when using external build module with no CSS entry point.
### Adds
diff --git a/modules/@apostrophecms/template/lib/bundlesLoader.js b/modules/@apostrophecms/template/lib/bundlesLoader.js
index fe67db0984..6724e6e6aa 100644
--- a/modules/@apostrophecms/template/lib/bundlesLoader.js
+++ b/modules/@apostrophecms/template/lib/bundlesLoader.js
@@ -1,6 +1,12 @@
const { stripIndent } = require('common-tags');
module.exports = (self) => {
+ // FIXME: This entire function should be separated for external build modules.
+ // Use the next opportunity to clean up and let the legacy system be and
+ // introduce a new one for external build modules (e.g. `insertBundlesMarkupByManifest`).
+ // The only check for external build modules should be at the very top of
+ // `insertBundlesMarkup` function, resulting in a call to our new
+ // function.
function insertBundlesMarkup({
page = {},
template = '',
@@ -30,7 +36,7 @@ module.exports = (self) => {
});
if (scene === 'apos') {
- return loadAllBundles({
+ return loadAllBundles(self, {
content,
scriptsPlaceholder,
stylesheetsPlaceholder,
@@ -75,6 +81,10 @@ module.exports = (self) => {
};
}, widgetsBundles);
+ const cssExtraBundles = self.apos.asset.hasBuildModule()
+ ? Array.from(new Set([ ...extraBundles.js, ...extraBundles.css ]))
+ : extraBundles.css;
+
const { jsBundles, cssBundles } = Object.entries(configs)
.reduce((acc, [ name, { templates } ]) => {
if (templates && !templates.includes(templateType)) {
@@ -90,7 +100,7 @@ module.exports = (self) => {
});
const cssMarkup = stylesheetsPlaceholder &&
- extraBundles.css.includes(name) &&
+ cssExtraBundles.includes(name) &&
renderMarkup({
fileName: name,
ext: 'css'
@@ -175,7 +185,7 @@ function renderBundleMarkupByManifest(self, modulePreload) {
};
}
-function loadAllBundles({
+function loadAllBundles(self, {
content,
extraBundles,
scriptsPlaceholder,
@@ -199,11 +209,15 @@ function loadAllBundles({
`;
};
+ const cssExtraBundles = self.apos.asset.hasBuildModule()
+ ? Array.from(new Set([ ...extraBundles.js, ...extraBundles.css ]))
+ : extraBundles.css;
+
const jsBundles = extraBundles.js.reduce(
(acc, bundle) => reduceToMarkup(acc, bundle, 'js'), jsMainBundle
);
- const cssBundles = extraBundles.css.reduce(
+ const cssBundles = cssExtraBundles.reduce(
(acc, bundle) => reduceToMarkup(acc, bundle, 'css'), cssMainBundle
);
diff --git a/test/asset-external.js b/test/asset-external.js
index 26d55efa02..37cad57b98 100644
--- a/test/asset-external.js
+++ b/test/asset-external.js
@@ -11,12 +11,12 @@ describe('Asset - External Build', function () {
this.timeout(t.timeout);
after(async function () {
- // await fs.remove(publicBuildPath);
+ await fs.remove(publicBuildPath);
await t.destroy(apos);
});
beforeEach(async function () {
- // await fs.remove(publicBuildPath);
+ await fs.remove(publicBuildPath);
});
it('should register external build module', async function () {
@@ -260,4 +260,185 @@ describe('Asset - External Build', function () {
'`build` configuration is not identical to the legacy `webpack` configuration'
);
});
+
+ it('should inject custom bundles dynamic CSS (PRO-6904)', async function () {
+ await t.destroy(apos);
+
+ apos = await t.create({
+ root: module,
+ autoBuild: true,
+ modules: {
+ 'asset-vite': {
+ before: '@apostrophecms/asset',
+ handlers(self) {
+ return {
+ '@apostrophecms/asset:afterInit': {
+ async registerExternalBuild() {
+ self.apos.asset.configureBuildModule(self, {
+ alias: 'vite',
+ devServer: false,
+ hmr: false
+ });
+ }
+ }
+ };
+ },
+ methods(self) {
+ return {
+ async build() {
+ return {
+ entrypoints: []
+ };
+ },
+ async watch() { },
+ async startDevServer() {
+ return {
+ entrypoints: []
+ };
+ },
+ async entrypoints() {
+ return [];
+ }
+ };
+ }
+ }
+ }
+ });
+
+ // Mock the build manifest
+ apos.asset.currentBuildManifest = {
+ sourceMapsRoot: null,
+ devServerUrl: null,
+ hmrTypes: [],
+ entrypoints: [
+ {
+ name: 'src',
+ type: 'index',
+ scenes: [ 'apos', 'public' ],
+ outputs: [ 'css', 'js' ],
+ condition: 'module',
+ manifest: {
+ root: 'dist',
+ name: 'src',
+ devServer: false
+ },
+ bundles: [
+ 'apos-src-module-bundle.js',
+ 'apos-bundle.css',
+ 'public-src-module-bundle.js',
+ 'public-bundle.css'
+ ]
+ },
+ {
+ name: 'counter-react',
+ type: 'custom',
+ scenes: [ 'counter-react' ],
+ outputs: [ 'css', 'js' ],
+ condition: 'module',
+ prologue: '',
+ ignoreSources: [],
+ manifest: {
+ root: 'dist',
+ name: 'counter-react',
+ devServer: false
+ },
+ bundles: [ 'counter-react-module-bundle.js' ]
+ },
+ {
+ name: 'counter-svelte',
+ type: 'custom',
+ scenes: [ 'counter-svelte' ],
+ outputs: [ 'css', 'js' ],
+ condition: 'module',
+ manifest: {
+ root: 'dist',
+ name: 'counter-svelte',
+ devServer: false
+ },
+ bundles: [
+ 'counter-svelte-module-bundle.js',
+ 'counter-svelte-bundle.css'
+ ]
+ },
+ {
+ name: 'counter-vue',
+ type: 'custom',
+ scenes: [ 'counter-vue' ],
+ outputs: [ 'css', 'js' ],
+ condition: 'module',
+ manifest: {
+ root: 'dist',
+ name: 'counter-vue',
+ devServer: false
+ },
+ bundles: [ 'counter-vue-module-bundle.js', 'counter-vue-bundle.css' ]
+ },
+ {
+ name: 'apos',
+ type: 'apos',
+ scenes: [ 'apos' ],
+ outputs: [ 'js' ],
+ condition: 'module',
+ manifest: {
+ root: 'dist',
+ name: 'apos',
+ devServer: false
+ },
+ bundles: [ 'apos-module-bundle.js', 'apos-bundle.css' ]
+ }
+ ]
+ };
+
+ // `counter-svelte` has `ui/src/counter-svelte.scss`.
+ // `counter-vue` doesn't have any CSS but has dynamic CSS, extracted
+ // from Vue components.
+ apos.asset.extraBundles = {
+ js: [ 'counter-react', 'counter-svelte', 'counter-vue' ],
+ css: [ 'counter-svelte ' ]
+ };
+ apos.asset.rebundleModules = [];
+
+ const payload = {
+ page: {
+ type: '@apostrophecms/home-page'
+ },
+ scene: 'apos',
+ template: '@apostrophecms/home-page:page',
+ content: '[stylesheets-placeholder:1]\n[scripts-placeholder:1]',
+ scriptsPlaceholder: '[scripts-placeholder:1]',
+ stylesheetsPlaceholder: '[stylesheets-placeholder:1]',
+ widgetsBundles: { 'counter-vue': {} }
+ };
+
+ const injected = apos.template.insertBundlesMarkup(payload);
+
+ // All bundles are injected, including the dynamic CSS from `counter-vue`
+ const actual = injected.replace(/>\s+<');
+ const expected = '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '';
+
+ assert.equal(actual, expected, 'Bundles are not injected correctly');
+
+ const injectedPublic = apos.template.insertBundlesMarkup({
+ ...payload,
+ scene: 'public'
+ });
+
+ // Showcases that the only the dynamic Vue CSS is injected, because the widget
+ // owning the bundle counter-vue is present.
+ const actualPublic = injectedPublic.replace(/>\s+<');
+ const expectedPublic = '' +
+ '' +
+ '' +
+ '';
+
+ assert.equal(actualPublic, expectedPublic, 'Bundles are not injected correctly');
+
+ });
});