How can I grant access to my const to be used inside a function? In this case I want to access my const catName inside my function fetchListings. I'm getting this error:
Question Updated:
ReferenceError: catName is not defined
<script context="module">
const fetchListings = async () => {
try {
// get reference to listings collection
const listingsRef = collection(db, 'listings');
// create a query to get all listings
const q = query(
listingsRef,
where('type', '==', catName),
orderBy(timestamp, 'desc'),
limit(10)
);
// execute query
const querySnap = await getDocs(q);
let lists = [];
querySnap.forEach((doc) => {
console.log(doc);
});
} catch (error) {
console.log(error);
}
};
fetchListings();
</script>
<script>
import { page } from '$app/stores';
// export the const catName to the function above
export const catName = $page.params.catName;
</script>
Hi {catName}!
The problem you are running into is coming from how the <script context="module"> works.
The module level script tag serves as a one-time-per-app setup script. That means it will run only one time, when your app is initialized and it will be run before any of the regular <script> tag code is run. See: https://svelte.dev/docs#component-format-script-context-module
This mean that the <script context="module"> won't have any access to what's defined or created in the normal <script> tags' code. Thus the not defined error for your constant, which is defined in the regular <script> tag.
Based on this, your code would need to be refactored (reorganized). My understanding is that you put the fetchListings in the module context because you want to pre-fetch the results and only do it once during the startup of your app.
To accomplish that you can refactor your code like this:
<script context="module">
let preFetched=false
</script>
<script>
import { page } from '$app/stores';
// export is not needed
const catName = $page.params.catName;
async function fetchListings() => {
// Your code ...
}
if(!preFetched){
fetchListings()
preFetched=true
}
</script>
Hi {catName }!
This ensures that the fetchListings function only runs once. The trick is that variables, constants, etc defined in the module context are accessible to all instances of that model. So when the first instance is being created it will run the fetchListings function, and sets the preFetched variable to false, so the subsequent instances will no do that.
This is just one possible way to do it. Depending on what exactly you want to accomplish you might want to organize things differently. But with the understanding of what does the <script context="module"> do and when it runs, you should be able to come up with your own solution best suited for your needs.
Related
I'm looking for a way to pass functions as parameters to the script tag. For example, to make the following work:
<script src="http://path/to/widget.js?param_a=1¶m_b=3" data-myfunc={myfunction()}></script>
<script>
myfunction() {
console.log("hello world")
}
</script>
And then trigger the function from the script.
Since we can pass values in attributes and capture using getAttributes : ref
Try this
<script>
// move function definition above and pass function ref - don't call that function
myfunction(){
console.log("hello world")
}
</script>
<script src="http://path/to/widget.js?param_a=1¶m_b=3" data-myfunc={myfunction}></script>
Yes there is a way!
you can delete the " () "
just turn :
<script src="http://path/to/widget.js?param_a=1¶m_b=3" data-myfunc={myfunction()}></script>
into:
<script src="http://path/to/widget.js?param_a=1¶m_b=3" data-myfunc={myfunction}></script>
And over!
It's my pleasure to help you!
by the way if you are interested, please help me also:
The is my question
that's pretty easy however it won't be accurate as you don't know which script tag will work first or if it will be compiled using inline or not.
if it uses inline your code will not work and you have to use the server to render javascript instead
here is an example using pure javascript. in my understanding you want after loading script /widget.js it will execute function stored in data-myfunc:
widget.js
if (document.currentScript) {
const script = document.currentScript.getAttribute('data-myfunc')
if (script) {
new Function(`
// sandbox faker
const window = undefined;
const document = undefined;
const Document = undefined;
const Window = undefined;
// run script
${script}
`)()
}
} else {
console.warn('widget.js loaded inline. Not working')
}
note if you want to declare the function myFunc after the script /widget.js you have to edit my code to use events like DOMContentLoaded to make sure the function exists
I'm trying to make unit and e2e test on a project, i decided to use jest and puppeteer (with also jest-puppeteer) to achive this.
My problem is that I initialize a var, named tools, in an script of index.html and i want to get it to do some test after, but he return me an error that is "tools" is not defined.
I already tryed to see on the web if a solution exist but without success.
Can I have somme help ? :')
Code extracts:
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<script src="./js/Variables.js"></script>
<script src="./js/Tools.js"></script>
</head>
<body>
<script>
tools = new Tools();
</script>
</body>
</html>
// Variables.js
let tools;
// Tools.js
class Tools {
constructor(){
// do some stuff
}
test(){
return "test string";
}
}
// app.test.js
beforeAll(async () => {
await page.goto('http://myPage/');
});
test("can i get \"tools\"", () => {
console.log(tools); // tools is not defined
expect(tools.test()).toBe("test string");
});
EDIT 22/07/2022 15:38
I finally managed to get something BUT now i can't use functions on it, the error says that tools.test() is not a function, it seems to retrieve only his "pure" value and not the Tools instance.
test("can i get \"tools\"", async () => {
let tools = await page.evaluate('tools');
console.log(tools); // gets {} (doesn't seems to retrieve instance)
expect(tools.test()).toBe("test string"); // TypeError: tools.test() is not a function
});
I can use class method by using
let toolsTest = await page.evaluate(() => tools.test());
BUT it's not really what i want... I really want to get an instance to test some sh!t on it.
However let tools = await page.evaluate(() => tools); still doesn't give me the instance, there is really no way to achive this ?
So i really need to know how to get variables in script tag to use them with jest. Maybe another test library can do the job such as mocha ?
I've been experimenting with new native ECMAScript module support that has recently been added to browsers. It's pleasant to finally be able import scripts directly and cleanly from JavaScript.
/example.html 🔍
<script type="module">
import {example} from '/example.js';
example();
</script>
/example.js
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
However, this only allows me to import modules that are defined by separate external JavaScript files. I usually prefer to inline some scripts used for the initial rendering, so their requests don't block the rest of the page. With a traditional informally-structured library, that might look like this:
/inline-traditional.html 🔍
<body>
<script>
var example = {};
example.example = function() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script>
example.example();
</script>
However, naively inlining modules files obviously won't work, since it would remove the filename used to identify the module to other modules. HTTP/2 server push may be the canonical way to handle this situation, but it's still not an option in all environments.
Is it possible to perform an equivalent transformation with ECMAScript modules?
Is there any way for a <script type="module"> to import a module exported by another in the same document?
I imagine this could work by allowing the script to specify a file path, and behave as though it had already been downloaded or pushed from the path.
/inline-name.html 🔍
<script type="module" name="/example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '/example.js';
example();
</script>
Or maybe by an entirely different reference scheme, such as is used for local SVG references:
/inline-id.html 🔍
<script type="module" id="example">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '#example';
example();
</script>
But neither of these hypotheticals actually work, and I haven't seen an alternative which does.
Hacking Together Our Own import from '#id'
Exports/imports between inline scripts aren't natively supported, but it was a fun exercise to hack together an implementation for my documents. Code-golfed down to a small block, I use it like this:
<script type="module" data-info="https://stackoverflow.com/a/43834063">let l,e,t
='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,
s,o;for(o of d.querySelectorAll(t+'[type=inline-module]'))l=d.createElement(t),o
.id?l.id=o.id:0,l.type='module',l[x]=o[x].replace(p,(u,a,z)=>(e=d.querySelector(
t+z+'[type=module][src]'))?a+`/* ${z} */'${e.src}'`:u),l.src=URL.createObjectURL
(new Blob([l[x]],{type:'application/java'+t})),o.replaceWith(l)//inline</script>
<script type="inline-module" id="utils">
let n = 1;
export const log = message => {
const output = document.createElement('pre');
output.textContent = `[${n++}] ${message}`;
document.body.appendChild(output);
};
</script>
<script type="inline-module" id="dogs">
import {log} from '#utils';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
<script type="inline-module">
import {log} from '#utils';
import {names as dogNames} from '#dogs';
log(`Imported dog names: ${dogNames.join(", ")}.`);
</script>
Instead of <script type="module">, we need to define our script elements using a custom type like <script type="inline-module">. This prevents the browser from trying to execute their contents itself, leaving them for us to handle. The script (full version below) finds all inline-module script elements in the document, and transforms them into regular script module elements with the behaviour we want.
Inline scripts can't be directly imported from each other, so we need to give the scripts importable URLs. We generate a blob: URL for each of them, containing their code, and set the src attribute to run from that URL instead of running inline. The blob: URLs acts like normal URLs from the server, so they can be imported from other modules. Each time we see a subsequent inline-module trying to import from '#example', where example is the ID of a inline-module we've transformed, we modify that import to import from the blob: URL instead. This maintains the one-time execution and reference deduplication that modules are supposed to have.
<script type="module" id="dogs" src="blob:https://example.com/9dc17f20-04ab-44cd-906e">
import {log} from /* #utils */ 'blob:https://example.com/88fd6f1e-fdf4-4920-9a3b';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
The execution of module script elements is always deferred until after the document is parsed, so we don't need to worry about trying to support the way that traditional script elements can modify the document while it's still being parsed.
export {};
for (const original of document.querySelectorAll('script[type=inline-module]')) {
const replacement = document.createElement('script');
// Preserve the ID so the element can be selected for import.
if (original.id) {
replacement.id = original.id;
}
replacement.type = 'module';
const transformedSource = original.textContent.replace(
// Find anything that looks like an import from '#some-id'.
/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,
(unmodified, action, selector) => {
// If we can find a suitable script with that id...
const refEl = document.querySelector('script[type=module][src]' + selector);
return refEl ?
// ..then update the import to use that script's src URL instead.
`${action}/* ${selector} */ '${refEl.src}'` :
unmodified;
});
// Include the updated code in the src attribute as a blob URL that can be re-imported.
replacement.src = URL.createObjectURL(
new Blob([transformedSource], {type: 'application/javascript'}));
// Insert the updated code inline, for debugging (it will be ignored).
replacement.textContent = transformedSource;
original.replaceWith(replacement);
}
Warnings: this simple implementation doesn't handle script elements added after the initial document has been parsed, or allow script elements to import from other script elements that occur after them in the document. If you have both module and inline-module script elements in a document, their relative execution order may not be correct. The source code transformation is performed using a crude regex that won't handle some edge cases such as periods in IDs.
This is possible with service workers.
Since a service worker should be installed before it will be able to process a page, this requires to have a separate page to initialize a worker to avoid chicken/egg problem - or a page can reloaded when a worker is ready.
Example
Here's a demo that is supposed to be workable in modern browsers that support native ES modules and async..await (namely Chrome):
index.html
<html>
<head>
<script>
(async () => {
try {
const swInstalled = await navigator.serviceWorker.getRegistration('./');
await navigator.serviceWorker.register('sw.js', { scope: './' })
if (!swInstalled) {
location.reload();
}
} catch (err) {
console.error('Worker not registered', err);
}
})();
</script>
</head>
<body>
World,
<script type="module" data-name="./example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from './example.js';
example();
</script>
</body>
</html>
sw.js
self.addEventListener('fetch', e => {
// parsed pages
if (/^https:\/\/run.plnkr.co\/\w+\/$/.test(e.request.url)) {
e.respondWith(parseResponse(e.request));
// module files
} else if (cachedModules.has(e.request.url)) {
const moduleBody = cachedModules.get(e.request.url);
const response = new Response(moduleBody,
{ headers: new Headers({ 'Content-Type' : 'text/javascript' }) }
);
e.respondWith(response);
} else {
e.respondWith(fetch(e.request));
}
});
const cachedModules = new Map();
async function parseResponse(request) {
const response = await fetch(request);
if (!response.body)
return response;
const html = await response.text(); // HTML response can be modified further
const moduleRegex = /<script type="module" data-name="([\w./]+)">([\s\S]*?)<\/script>/;
const moduleScripts = html.match(new RegExp(moduleRegex.source, 'g'))
.map(moduleScript => moduleScript.match(moduleRegex));
for (const [, moduleName, moduleBody] of moduleScripts) {
const moduleUrl = new URL(moduleName, request.url).href;
cachedModules.set(moduleUrl, moduleBody);
}
const parsedResponse = new Response(html, response);
return parsedResponse;
}
Script bodies are being cached (native Cache can be used as well) and returned for respective module requests.
Concerns
The approach is inferior to the application built and chunked with bundling tool like Webpack or Rollup in terms of performance, flexibility, solidity and browser support - especially if blocking concurrent requests are the primary concern.
Inline scripts increase bandwidth usage. This is naturally avoided when scripts are loaded once and cached by the browser.
Inline scripts aren't modular and contradict the concept of ECMAScript modules (unless they are generated from real modules by server-side template).
Service worker initialization should be performed on a separate page to avoid unnecessary requests.
The solution is limited to a single page and doesn't take <base> into account.
A regular expression is used for demonstration purposes only. When used like in the example above it enables the execution of arbitrary JavaScript code that is available on the page. A proven library like parse5 should be used instead (it will result in performance overhead, and still, there may be security concerns). Never use regular expressions to parse the DOM.
I don't believe that's possible.
For inline scripts you're stuck with one of the more traditional ways of modularizing code, like the namespacing you demonstrated using object literals.
With webpack you can do code splitting which you could use to grab a very minimal chunk of code on page load and then incrementally grab the rest as needed. Webpack also has the advantage of allowing you to use the module syntax (plus a ton of other ES201X improvements) in way more environments than just Chrome Canary.
I tweaked Jeremy's answer with the use of this article to prevent scripts from executing
<script data-info="https://stackoverflow.com/a/43834063">
// awsome guy on [data-info] wrote 90% of this but I added the mutation/module-type part
let l,e,t='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,s,o;
let evls = event => (
event.target.type === 'javascript/blocked',
event.preventDefault(),
event.target.removeEventListener( 'beforescriptexecute', evls ) )
;(new MutationObserver( mutations =>
mutations.forEach( ({ addedNodes }) =>
addedNodes.forEach( node =>
( node.nodeType === 1 && node.matches( t+'[module-type=inline]' )
&& (
node.type = 'javascript/blocked',
node.addEventListener( 'beforescriptexecute', evls ),
o = node,
l=d.createElement(t),
o.id?l.id=o.id:0,
l.type='module',
l[x]=o[x].replace(p,(u,a,z)=>
(e=d.querySelector(t+z+'[type=module][src]'))
?a+`/* ${z} */'${e.src}'`
:u),
l.src=URL.createObjectURL(
new Blob([l[x]],
{type:'application/java'+t})),
o.replaceWith(l)
)//inline
) ) )))
.observe( document.documentElement, {
childList: true,
subtree: true
} )
// for(o of d.querySelectorAll(t+'[module-type=inline]'))
// l=d.createElement(t),
// o.id?l.id=o.id:0,
// l.type='module',
// l[x]=o[x].replace(p,(u,a,z)=>
// (e=d.querySelector(t+z+'[type=module][src]'))
// ?a+`/* ${z} */'${e.src}'`
// :u),
// l.src=URL.createObjectURL(
// new Blob([l[x]],
// {type:'application/java'+t})),
// o.replaceWith(l)//inline</script>
I'm hoping that this solves the dynamic-script-appending issue (using MutationObserver), vs-code not syntax-highlighting (preserving type=module) and I imagine that using the same MutationObserver one could execute scripts once the imported ids are added to the DOM.
Please tell me if this has issues!
We can use blob and importmap to import inline scripts.
https://github.com/xitu/inline-module
<div id="app"></div>
<script type="inline-module" id="foo">
const foo = 'bar';
export {foo};
</script>
<script src="https://unpkg.com/inline-module/index.js" setup></script>
<script type="module">
import {foo} from '#foo';
app.textContent = foo;
</script>
I am building an Atom Electron app. Right now I have this in the preload.js of one of my webviews:
var { requireTaskPool } = require('electron-remote');
var work = '';
var _ = require('lodash');
work = requireTaskPool(require.resolve('./local/path/to/js/file.js'));
function scriptRun() {
console.log('Preload: Script Started');
// `work` will get executed concurrently in separate background processes
// and resolve with a promise
_.times(1, () => {
work(currentTab).then(result => {
console.log(`Script stopped. Total time running was ${result} ms`);
});
});
}
module.exports = scriptRun;
scriptRun();
It gets a local script and then executes it in a background process.
I want to do the same exact thing, except I want to retrieve the script from an external source like so
work = requireTaskPool(require.resolve('https://ex.com/path/to/js/file.js'));
When I do this, I get errors like:
Uncaught Error: Cannot find module 'https://ex.com/path/to/js/file.js'
How can I load external scripts? And then use the loaded scripts with my work function. My feeling is that require only works with local files. If AJAX is the answer, can I see an example of how to get a script, then pass it into my work without executing it prior?
I was able to load a remote js file and execute the function defined in it, hopefully it will provide you enough to start with...
my remote dummy.js, available online somewhere:
const dummy = () => console.log('dummy works');
my download.js:
const vm = require("vm");
const rp = require('request-promise');
module.exports = {
downloadModule: async () => {
try {
let body = await rp('http://somewhere.online/dummy.js');
let script = vm.createScript(body);
script.runInThisContext();
// this is the actual dummy method loaded from remote dummy.js
// now available in this context:
return dummy;
} catch (err) {
console.log('err', err);
}
return null;
}
};
You need to add the request-promise package.
Then in my main.js I use it like this:
const {downloadModule} = require('./download');
downloadModule().then((dummy) => {
if (dummy) dummy();
else console.log('no dummy');
});
When I run it, this is what I get:
$ electron .
dummy works
I wanted to create an actual module and require it, but I have not had the time to play with this further. If I accomplish that I will add it here.
You have not provided any details on your file.js. But I can give you the general idea.
There are two things that you need at minimum to call your package a module:
file.js (of course you have it) and
package.json
The structure of your file.js should be something like this:
//load your dependencies here
var something = require("something");
//module.exports is necessary to export your code,
//so that you can fetch this code in another file by using require.
module.exports = function() {
abc: function(){
//code for abc function
},
xyz: function(){
//code for xyz function
}
}
Now if you put your package on any website, you can access it as:
npm install https://ex.com/path/to/js/file.js
Now, a copy of your package will be put into node-modules folder.
So, now you can access it as:
var x = require('name-of-your-package-in-node-modules');
Now, you can also do:
var abc = require('name-of-your-package-in-node-modules').abc;
or
var xyz = require('name-of-your-package-in-node-modules').xyz;
I've been experimenting with new native ECMAScript module support that has recently been added to browsers. It's pleasant to finally be able import scripts directly and cleanly from JavaScript.
/example.html 🔍
<script type="module">
import {example} from '/example.js';
example();
</script>
/example.js
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
However, this only allows me to import modules that are defined by separate external JavaScript files. I usually prefer to inline some scripts used for the initial rendering, so their requests don't block the rest of the page. With a traditional informally-structured library, that might look like this:
/inline-traditional.html 🔍
<body>
<script>
var example = {};
example.example = function() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script>
example.example();
</script>
However, naively inlining modules files obviously won't work, since it would remove the filename used to identify the module to other modules. HTTP/2 server push may be the canonical way to handle this situation, but it's still not an option in all environments.
Is it possible to perform an equivalent transformation with ECMAScript modules?
Is there any way for a <script type="module"> to import a module exported by another in the same document?
I imagine this could work by allowing the script to specify a file path, and behave as though it had already been downloaded or pushed from the path.
/inline-name.html 🔍
<script type="module" name="/example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '/example.js';
example();
</script>
Or maybe by an entirely different reference scheme, such as is used for local SVG references:
/inline-id.html 🔍
<script type="module" id="example">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '#example';
example();
</script>
But neither of these hypotheticals actually work, and I haven't seen an alternative which does.
Hacking Together Our Own import from '#id'
Exports/imports between inline scripts aren't natively supported, but it was a fun exercise to hack together an implementation for my documents. Code-golfed down to a small block, I use it like this:
<script type="module" data-info="https://stackoverflow.com/a/43834063">let l,e,t
='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,
s,o;for(o of d.querySelectorAll(t+'[type=inline-module]'))l=d.createElement(t),o
.id?l.id=o.id:0,l.type='module',l[x]=o[x].replace(p,(u,a,z)=>(e=d.querySelector(
t+z+'[type=module][src]'))?a+`/* ${z} */'${e.src}'`:u),l.src=URL.createObjectURL
(new Blob([l[x]],{type:'application/java'+t})),o.replaceWith(l)//inline</script>
<script type="inline-module" id="utils">
let n = 1;
export const log = message => {
const output = document.createElement('pre');
output.textContent = `[${n++}] ${message}`;
document.body.appendChild(output);
};
</script>
<script type="inline-module" id="dogs">
import {log} from '#utils';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
<script type="inline-module">
import {log} from '#utils';
import {names as dogNames} from '#dogs';
log(`Imported dog names: ${dogNames.join(", ")}.`);
</script>
Instead of <script type="module">, we need to define our script elements using a custom type like <script type="inline-module">. This prevents the browser from trying to execute their contents itself, leaving them for us to handle. The script (full version below) finds all inline-module script elements in the document, and transforms them into regular script module elements with the behaviour we want.
Inline scripts can't be directly imported from each other, so we need to give the scripts importable URLs. We generate a blob: URL for each of them, containing their code, and set the src attribute to run from that URL instead of running inline. The blob: URLs acts like normal URLs from the server, so they can be imported from other modules. Each time we see a subsequent inline-module trying to import from '#example', where example is the ID of a inline-module we've transformed, we modify that import to import from the blob: URL instead. This maintains the one-time execution and reference deduplication that modules are supposed to have.
<script type="module" id="dogs" src="blob:https://example.com/9dc17f20-04ab-44cd-906e">
import {log} from /* #utils */ 'blob:https://example.com/88fd6f1e-fdf4-4920-9a3b';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
The execution of module script elements is always deferred until after the document is parsed, so we don't need to worry about trying to support the way that traditional script elements can modify the document while it's still being parsed.
export {};
for (const original of document.querySelectorAll('script[type=inline-module]')) {
const replacement = document.createElement('script');
// Preserve the ID so the element can be selected for import.
if (original.id) {
replacement.id = original.id;
}
replacement.type = 'module';
const transformedSource = original.textContent.replace(
// Find anything that looks like an import from '#some-id'.
/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,
(unmodified, action, selector) => {
// If we can find a suitable script with that id...
const refEl = document.querySelector('script[type=module][src]' + selector);
return refEl ?
// ..then update the import to use that script's src URL instead.
`${action}/* ${selector} */ '${refEl.src}'` :
unmodified;
});
// Include the updated code in the src attribute as a blob URL that can be re-imported.
replacement.src = URL.createObjectURL(
new Blob([transformedSource], {type: 'application/javascript'}));
// Insert the updated code inline, for debugging (it will be ignored).
replacement.textContent = transformedSource;
original.replaceWith(replacement);
}
Warnings: this simple implementation doesn't handle script elements added after the initial document has been parsed, or allow script elements to import from other script elements that occur after them in the document. If you have both module and inline-module script elements in a document, their relative execution order may not be correct. The source code transformation is performed using a crude regex that won't handle some edge cases such as periods in IDs.
This is possible with service workers.
Since a service worker should be installed before it will be able to process a page, this requires to have a separate page to initialize a worker to avoid chicken/egg problem - or a page can reloaded when a worker is ready.
Example
Here's a demo that is supposed to be workable in modern browsers that support native ES modules and async..await (namely Chrome):
index.html
<html>
<head>
<script>
(async () => {
try {
const swInstalled = await navigator.serviceWorker.getRegistration('./');
await navigator.serviceWorker.register('sw.js', { scope: './' })
if (!swInstalled) {
location.reload();
}
} catch (err) {
console.error('Worker not registered', err);
}
})();
</script>
</head>
<body>
World,
<script type="module" data-name="./example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from './example.js';
example();
</script>
</body>
</html>
sw.js
self.addEventListener('fetch', e => {
// parsed pages
if (/^https:\/\/run.plnkr.co\/\w+\/$/.test(e.request.url)) {
e.respondWith(parseResponse(e.request));
// module files
} else if (cachedModules.has(e.request.url)) {
const moduleBody = cachedModules.get(e.request.url);
const response = new Response(moduleBody,
{ headers: new Headers({ 'Content-Type' : 'text/javascript' }) }
);
e.respondWith(response);
} else {
e.respondWith(fetch(e.request));
}
});
const cachedModules = new Map();
async function parseResponse(request) {
const response = await fetch(request);
if (!response.body)
return response;
const html = await response.text(); // HTML response can be modified further
const moduleRegex = /<script type="module" data-name="([\w./]+)">([\s\S]*?)<\/script>/;
const moduleScripts = html.match(new RegExp(moduleRegex.source, 'g'))
.map(moduleScript => moduleScript.match(moduleRegex));
for (const [, moduleName, moduleBody] of moduleScripts) {
const moduleUrl = new URL(moduleName, request.url).href;
cachedModules.set(moduleUrl, moduleBody);
}
const parsedResponse = new Response(html, response);
return parsedResponse;
}
Script bodies are being cached (native Cache can be used as well) and returned for respective module requests.
Concerns
The approach is inferior to the application built and chunked with bundling tool like Webpack or Rollup in terms of performance, flexibility, solidity and browser support - especially if blocking concurrent requests are the primary concern.
Inline scripts increase bandwidth usage. This is naturally avoided when scripts are loaded once and cached by the browser.
Inline scripts aren't modular and contradict the concept of ECMAScript modules (unless they are generated from real modules by server-side template).
Service worker initialization should be performed on a separate page to avoid unnecessary requests.
The solution is limited to a single page and doesn't take <base> into account.
A regular expression is used for demonstration purposes only. When used like in the example above it enables the execution of arbitrary JavaScript code that is available on the page. A proven library like parse5 should be used instead (it will result in performance overhead, and still, there may be security concerns). Never use regular expressions to parse the DOM.
I don't believe that's possible.
For inline scripts you're stuck with one of the more traditional ways of modularizing code, like the namespacing you demonstrated using object literals.
With webpack you can do code splitting which you could use to grab a very minimal chunk of code on page load and then incrementally grab the rest as needed. Webpack also has the advantage of allowing you to use the module syntax (plus a ton of other ES201X improvements) in way more environments than just Chrome Canary.
I tweaked Jeremy's answer with the use of this article to prevent scripts from executing
<script data-info="https://stackoverflow.com/a/43834063">
// awsome guy on [data-info] wrote 90% of this but I added the mutation/module-type part
let l,e,t='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,s,o;
let evls = event => (
event.target.type === 'javascript/blocked',
event.preventDefault(),
event.target.removeEventListener( 'beforescriptexecute', evls ) )
;(new MutationObserver( mutations =>
mutations.forEach( ({ addedNodes }) =>
addedNodes.forEach( node =>
( node.nodeType === 1 && node.matches( t+'[module-type=inline]' )
&& (
node.type = 'javascript/blocked',
node.addEventListener( 'beforescriptexecute', evls ),
o = node,
l=d.createElement(t),
o.id?l.id=o.id:0,
l.type='module',
l[x]=o[x].replace(p,(u,a,z)=>
(e=d.querySelector(t+z+'[type=module][src]'))
?a+`/* ${z} */'${e.src}'`
:u),
l.src=URL.createObjectURL(
new Blob([l[x]],
{type:'application/java'+t})),
o.replaceWith(l)
)//inline
) ) )))
.observe( document.documentElement, {
childList: true,
subtree: true
} )
// for(o of d.querySelectorAll(t+'[module-type=inline]'))
// l=d.createElement(t),
// o.id?l.id=o.id:0,
// l.type='module',
// l[x]=o[x].replace(p,(u,a,z)=>
// (e=d.querySelector(t+z+'[type=module][src]'))
// ?a+`/* ${z} */'${e.src}'`
// :u),
// l.src=URL.createObjectURL(
// new Blob([l[x]],
// {type:'application/java'+t})),
// o.replaceWith(l)//inline</script>
I'm hoping that this solves the dynamic-script-appending issue (using MutationObserver), vs-code not syntax-highlighting (preserving type=module) and I imagine that using the same MutationObserver one could execute scripts once the imported ids are added to the DOM.
Please tell me if this has issues!
We can use blob and importmap to import inline scripts.
https://github.com/xitu/inline-module
<div id="app"></div>
<script type="inline-module" id="foo">
const foo = 'bar';
export {foo};
</script>
<script src="https://unpkg.com/inline-module/index.js" setup></script>
<script type="module">
import {foo} from '#foo';
app.textContent = foo;
</script>