Skip to content

Commit 4597cf0

Browse files
authored
Merge pull request #236 from mjancarik/module-support
feat: 🎸 Added support for loading 'module' assets
2 parents 7c7a3f4 + d0ae207 commit 4597cf0

4 files changed

Lines changed: 168 additions & 4 deletions

File tree

packages/cli/src/templates/head.ejs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
1818
<%if (asset.type==='script' ) { %>
1919
<%if (typeof asset.source==='string' ) { %>
20-
<script src='<%= asset.source %>' defer='true' data-name="<%= asset.name %>"></script>
20+
<script src='<%= asset.source %>' <%- asset.module ? 'type="module"' : 'defer="true"' %> data-name="<%= asset.name %>"></script>
2121
<% } %>
2222
<%if (typeof asset.source==='object' ) { %>
23-
<script src='<%= asset.source.es13 %>' defer='true' data-name="<%= asset.name %>"></script>
23+
<script src='<%= asset.source.es13 %>' <%- asset.module ? 'type="module"' : 'defer="true"' %> data-name="<%= asset.name %>"></script>
2424
<% } %>
2525
<% } %>
2626
@@ -36,4 +36,4 @@
3636
</script>
3737
<% } %>
3838
<% } %>
39-
<% }); %>
39+
<% }); %>

packages/core/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface BaseWidgetAsset {
22
type: 'stylesheet' | 'script' | 'inlineStyle';
33
optional?: boolean;
4+
module?: boolean;
45
test?: string;
56
attr?: Record<string, string | boolean>;
67
}

packages/integration/src/__tests__/indexSpec.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
isLoadedSymbol,
23
loadAssets,
34
loadJsonAssets,
45
loadScriptAssets,
@@ -222,6 +223,30 @@ describe('Merkur component', () => {
222223
fakeAssetListeners = {};
223224
assetLoadingDeferreds = {};
224225

226+
// Add module script test assets
227+
assetsDictionary['module.js'] = {
228+
name: 'module.js',
229+
type: 'script',
230+
module: true,
231+
source: 'http://localhost:4444/static/es13/module.123456.js',
232+
};
233+
assetsDictionary['inline-module.js'] = {
234+
name: 'inline-module.js',
235+
type: 'inlineScript',
236+
module: true,
237+
source: 'console.log("Hello from module!");',
238+
};
239+
assetsDictionary['module-with-attr.js'] = {
240+
name: 'module-with-attr.js',
241+
type: 'script',
242+
module: true,
243+
source: 'http://localhost:4444/static/es13/module-attr.123456.js',
244+
attr: {
245+
crossorigin: 'anonymous',
246+
defer: true, // This should be overridden
247+
},
248+
};
249+
225250
jest.spyOn(console, 'warn').mockImplementation(() => {});
226251
jest
227252
.spyOn(document, 'createElement')
@@ -558,6 +583,96 @@ describe('Merkur component', () => {
558583
}),
559584
]);
560585
});
586+
587+
it('should create module script elements with type="module"', async () => {
588+
const scriptsPromise = loadScriptAssets([assetsDictionary['module.js']]);
589+
590+
expect(scriptsPromise).toBeInstanceOf(Promise);
591+
expect(document.createElement).toHaveBeenCalledTimes(1);
592+
expect(document.head.appendChild).toHaveBeenCalledTimes(1);
593+
594+
resolveFakeAssets();
595+
const sources = await scriptsPromise;
596+
597+
expect(fakeAssetObjects[0].type).toBe('module');
598+
expect(fakeAssetObjects[0].defer).toBeUndefined(); // defer should not be set for modules
599+
expect(sources).toStrictEqual([
600+
getAssetWithElement('module.js', fakeAssetObjects[0]),
601+
]);
602+
});
603+
604+
it('should create inline module script elements with type="module"', async () => {
605+
const scriptsPromise = loadScriptAssets([
606+
assetsDictionary['inline-module.js'],
607+
]);
608+
609+
expect(scriptsPromise).toBeInstanceOf(Promise);
610+
expect(document.createElement).toHaveBeenCalledTimes(1);
611+
expect(document.head.appendChild).toHaveBeenCalledTimes(1);
612+
613+
const sources = await scriptsPromise;
614+
615+
expect(fakeAssetObjects[0].type).toBe('module');
616+
expect(fakeAssetObjects[0].textContent).toBe(
617+
'console.log("Hello from module!");',
618+
);
619+
expect(sources).toStrictEqual([
620+
getAssetWithElement('inline-module.js', fakeAssetObjects[0]),
621+
]);
622+
});
623+
624+
it('should handle module scripts with custom attributes correctly', async () => {
625+
const scriptsPromise = loadScriptAssets([
626+
assetsDictionary['module-with-attr.js'],
627+
]);
628+
629+
expect(document.createElement).toHaveBeenCalledTimes(1);
630+
expect(document.head.appendChild).toHaveBeenCalledTimes(1);
631+
632+
resolveFakeAssets();
633+
const sources = await scriptsPromise;
634+
635+
expect(fakeAssetObjects[0].type).toBe('module');
636+
expect(fakeAssetObjects[0].defer).toBeUndefined(); // defer should be overridden for modules
637+
expect(fakeAssetObjects[0].crossorigin).toBe('anonymous'); // custom attributes should be preserved
638+
expect(sources).toStrictEqual([
639+
getAssetWithElement('module-with-attr.js', fakeAssetObjects[0], {
640+
attr: {
641+
crossorigin: 'anonymous',
642+
defer: false, // defer should be overridden for modules
643+
},
644+
}),
645+
]);
646+
});
647+
648+
it('should return a promise that rejects when a module script fails to load', async () => {
649+
expect.assertions(3);
650+
const scriptsPromise = loadScriptAssets([assetsDictionary['module.js']]);
651+
652+
expect(document.createElement).toHaveBeenCalledTimes(1);
653+
654+
rejectFakeAssets();
655+
656+
try {
657+
await scriptsPromise;
658+
} catch (error) {
659+
expect(fakeAssetObjects[0].remove).toHaveBeenCalledTimes(1);
660+
expect(error).toBeInstanceOf(Error);
661+
}
662+
});
663+
664+
it('should resolve if a module script (not created by loadScriptAssets) is already present in the DOM', async () => {
665+
const script = fakeAssetObjectGenerator('script');
666+
script.src = assetsDictionary['module.js'].source;
667+
script.type = 'module';
668+
script[isLoadedSymbol] = true; // Mark as already loaded
669+
670+
const scriptsPromise = loadScriptAssets([assetsDictionary['module.js']]);
671+
const sources = await scriptsPromise;
672+
673+
expect(document.createElement).toHaveBeenCalledTimes(0);
674+
expect(sources).toStrictEqual([getAssetWithElement('module.js', script)]);
675+
});
561676
});
562677

563678
describe('loadJsonAssets() function', () => {
@@ -1004,5 +1119,40 @@ describe('Merkur component', () => {
10041119

10051120
expect(console.warn).not.toHaveBeenCalled();
10061121
});
1122+
1123+
it('should load module scripts correctly', async () => {
1124+
const assetsPromise = loadAssets([
1125+
assetsDictionary['module.js'],
1126+
assetsDictionary['inline-module.js'],
1127+
assetsDictionary['module-with-attr.js'],
1128+
]);
1129+
1130+
resolveFakeAssets();
1131+
const sources = await assetsPromise;
1132+
1133+
expect(document.createElement).toHaveBeenCalledTimes(3);
1134+
expect(sources).toStrictEqual([
1135+
getAssetWithElement('module.js', fakeAssetObjects[0]),
1136+
getAssetWithElement('inline-module.js', fakeAssetObjects[1]),
1137+
getAssetWithElement('module-with-attr.js', fakeAssetObjects[2], {
1138+
attr: {
1139+
crossorigin: 'anonymous',
1140+
defer: false, // defer should be overridden for modules
1141+
},
1142+
}),
1143+
]);
1144+
1145+
// Check that all scripts have the correct type
1146+
expect(fakeAssetObjects[0].type).toBe('module');
1147+
expect(fakeAssetObjects[1].type).toBe('module');
1148+
expect(fakeAssetObjects[2].type).toBe('module');
1149+
1150+
// Check that defer is not set for module scripts
1151+
expect(fakeAssetObjects[0].defer).toBeUndefined();
1152+
expect(fakeAssetObjects[1].defer).toBeUndefined();
1153+
expect(fakeAssetObjects[2].defer).toBeUndefined();
1154+
1155+
expect(console.warn).not.toHaveBeenCalled();
1156+
});
10071157
});
10081158
});

packages/integration/src/index.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ function _findScriptElement(scriptElements, asset) {
129129
function _loadScript(asset, root) {
130130
const script = document.createElement('script');
131131

132+
// Set script type to module if specified
133+
if (asset.module) {
134+
script.type = 'module';
135+
}
136+
132137
if (asset.type === 'inlineScript') {
133138
script.textContent = asset.source;
134139
root.appendChild(script);
@@ -137,7 +142,10 @@ function _loadScript(asset, root) {
137142
}
138143

139144
script[loadingPromiseSymbol] = new Promise((resolve, reject) => {
140-
script.defer = true;
145+
// Don't set defer for module scripts as it can interfere with module loading
146+
if (!asset.module) {
147+
script.defer = true;
148+
}
141149
_addListenersToAssetElement(asset, script, resolve, reject);
142150
script.src = asset.source;
143151

@@ -173,6 +181,11 @@ async function loadScriptAssets(assets, root = document.head) {
173181
return _attachElementToAsset(asset, null);
174182
}
175183

184+
// Module scripts should not use defer attribute as it can cause issues
185+
if (asset.module && asset.attr && asset.attr.defer !== false) {
186+
asset = { ...asset, attr: { ...asset.attr, defer: false } };
187+
}
188+
176189
const { source } = asset;
177190
const _asset = Object.assign({}, asset);
178191

0 commit comments

Comments
 (0)