-
-
Notifications
You must be signed in to change notification settings - Fork 167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC-0002] Farm's Runtime Module System #9
Comments
We just need to transform esm to commonjs I think, and leave commonjs unchanged, we can provide a commonjs compatible moudle system, as swc itself supports transforming esm to commonjs, so we can save a lot of work. And I think, the final generated file's module system would be esm by default(with web and commonjs format supported for compatibility), and it wraps our commonjs like module system which contains many modules. So we can use native esm support of browser and nodejs to fetch/load our final generated file but without losing original module granularity. |
Yes, but there are still some ESM export or import statements that CommonJS cannot support, for example, top-level await export ( ESM or CommonJS wrapper will be described in this RFC. |
I have an idea, dynamic import will be transformed to function a() {
import('./b').then(module => console.log(module))
} will be transformed to function a() {
import('vendors/xxxx.mjs').then(file => file.load('./b')).then(module => console.log(module))
} This way we do not need to design how to load files in browser or node.js, they both native support esm, top level await is also not a problem any more. I think we may drop commonjs support for nodejs and only support esm for SSR, only provide web target for legacy browsers. In browser, all the inital modules will be registered in initial html script tags, dynamic modules will be loaded by And the generated file looks like: // if the target is node, will load static resource dependency as below
import 'xxxxxx.mjs'; // only add this in node.js
globalThis.registerModules({
'./b': function(module, exports, require) {
exports.b = 'b';
// dynamic load module c
const C = import('c.hash.mjs').then(file.load('./c'));
const d = require('./d');
},
'./d': function(module, exports, require) { /* ... */ }
});
export function load(moduleId) {
return globalThis.requireModule(moduleId);
} |
For browsers don't support But how to transform top-level await with a module exported by another one in legacy browsers? For example: export default await import('another-module') The module above should export a sync default export, not a promise. I cannot find a way to load this module correctly in legacy browsers. |
For legacy browsers, we transform the modules as follow: export function a() {
import('./b').then(module => console.log(module))
}
export default await import('d') will be transformed to globalThis.registerModules({
'./a': async function(module, exports, require, __farm_dynamic_import__) {
exports.a = function a() {
__farm_dynamic_import__ ('vendors/xxxx.mjs').then(file => file.load('./b')).then(module => console.log(module))
}
exports.default = await __farm_dynamic_import__('...') ...;
},
});
for top level await, the module will be transformed to a |
But for the importer of module 'a', the module initializer is still an async function (returning a promise), the importer still have to use // source code of b
export default 'b';
// source code of a
export default await import('b');
// transformed module a
globalThis.registerModules({
'./a': async function(module, exports, require, __farm_dynamic_import__) { // but this function returns a promise, not a string
exports.a = function a() {
__farm_dynamic_import__ ('vendors/xxxx.mjs').then(file => file.load('./b')).then(module => console.log(module))
}
exports.default = await __farm_dynamic_import__('...') ...; // this is a string value
},
}); // importer of module a
// source code
import a from 'a';
console.log(a, typeof a); // this should be 'b' and 'string'
// transformed code
const a = moduleMap['a'](); // this is a promise
console.log(a, typeof a); // this should be Promise and object
a.then(v => console.log(v, typeof v)); // this is 'b' and 'string' So, there is no way to emulate top-level await in environment that doesn't support top-level await natively, we have to throw error when user use it for legacy browsers. |
I think the transformed code of the importer of should look like: // transformed importer module a
globalThis.registerModules({
'./importer-of-a': async function(module, exports, require, __farm_dynamic_import__) { // but this function returns a promise, not a string
var a = require('./a').a;
var other = require('./other');
// above async modules should be loaded concurrently
// await a before use
var a = await a;
exports.default = a; // a is a string value
},
}); This works the same as Webpack supports top level await too, we may investigate how webpack handle it. Anyway, if a module is a async module, then the parent module will be async too until the root main module. We just need to require the main module as normal: globalThis.requireModule('./main'); |
Closed in favor of farm-fe/rfcs#1 |
Farm's Runtime Module System
Abstract
As discussed in #3, Farm will design its own runtime module system to simulate ESM and CommonJS in web browsers, this RFC describes how this module system works.
In compile time, Farm transforms ESM or CommonJS modules to Farm's standard module, especially, ESM's
export
/import
statements will be replaced as they are defined in ECMA specification, leave it as is will lead to runtime errors.In runtime, Farm manages all modules with runtime code, in general, a farm's module has three stages in its whole lifetime, which are register, load and run.
Architecture
Farm's module system is composed with two parts:
Compile time transformation
All modules will be transformed to a CommonJS-like module style:
require('REQUEST')
exports.foo = 1
/module.exports = {}
/module.exports = function () {}
ESM modules will be transformed to CommonJS-like module style,
import
andexport
statements defined in ECMAScript specification will be mapped to CommonJS-like module syntax:import
ImportClause FromClauseimport foo from 'IDENTIFIER'
->const { foo } = require('IDENTIFIER')
import * as ns from 'IDENTIFIER'
->const ns = require('IDENTIFIER')
import { foo, bar } from 'IDENTIFIER'
->const { foo, bar } = require('IDENTIFIER')
import foo, * as ns from 'IDENTIFIER'
->const ns = require('IDENTIFIER'); const foo = ns.default
import foo, { foo, bar, baz as z } from 'IDENTIFIER'
->const { default: foo, foo, bar, baz: z } = require('IDENTIFIER')
import
ModuleSpecifier:import 'IDENTIFIER'
->require('IDENTIFIER')
export
ExportFromClause FromClauseexport * from 'IDENTIFIER';
ex
from 'IDENTIFIER',module.exports[ex] = require('IDENTIFIER')[ex]
export * as ns from 'IDENTIFIER';
- >exports.ns = require('IDENTIFIER')
export { foo, bar, baz as z} from 'IDENTIFIER';
->exports.foo = require('IDENTIFIER').foo; exports.bar = require('IDENTIFIER').bar; exports.z = require('IDENTIFIER').baz
export
NamedExports:export { foo, bar, baz as z };
->exports.foo = foo; exports.bar = bar; exports.z = baz
export
VariableStatement:export var baz = 1
->export.baz = 1
export
Declaration:export function foo() {}
->exports.foo = function foo() {}
export function foo* () {}
->exports.foo= function foo* () {}
export async function foo() {}
->exports.foo= async function foo() {}
export async function foo* () {}
->exports.foo= async function foo* () {}
export class foo {}
->exports.foo= class foo {}
export let foo = 1
->exports.foo = 1
export let foo, bar, baz = 1
->exports.foo = undefined; exports.bar = undefined; exports.baz = 1
export const bar = 1
->exports.bar = 1
export const foo = 1, bar = 1
->exports.foo = 1; exports.bar = 1
export default
HoistableDeclaration:export default function foo() {}
->exports.default= function foo() {}
export default function foo* () {}
->exports.default= function foo* () {}
export default async function foo() {}
->exports.default= async function foo() {}
export default async function foo* () {}
->exports.default= async function foo* () {}
export default
ClassDeclaration:export default class foo {}
->exports.default= class foo {}
export default [lookahead ∉ { function, async [no [LineTerminator](https://tc39.es/ecma262/#prod-LineTerminator) here] function, class }]
AssignmentExpression:export default foo ? bar : baz
->exports.default = foo ? bar : baz
export default () => 1
->exports.default = () => 1
export default async () => 1
->exports.default = async () => 1
export default a = 1
(wherea
is a variable declared withvar
orlet
) ->exports.default = a = 1
export default a += 1
(wherea
is a variable declared withvar
orlet
, AssignmentExpresssion: one of*= /= %= += -= <<= >>= >>>= &= ^= |= **=
) ->exports.default = a += 1
&&=
AssignmentExpression:export default a &&= 1
(wherea
is a variable declared withvar
orlet
) ->exports.default = a &&= 1
||=
AssignmentExpression:export default a ||= 1
(wherea
is a variable declared withvar
orlet
) ->exports.default = a ||= 1
??=
AssignmentExpression:export default a ??= 1
(wherea
is a variable declared withvar
orlet
) ->exports.default = a ??= 1
Examples
Runtime management
Examples
The text was updated successfully, but these errors were encountered: