diff --git a/Makefile b/Makefile index a6b769ac77..feb73650a9 100644 --- a/Makefile +++ b/Makefile @@ -19,11 +19,21 @@ landing-page: rm -rf lively.freezer/landing-page env CI=true npm --prefix lively.freezer run build-landing-page +landing-page-debug: + rm -rf lively.server/.module_cache + rm -rf lively.freezer/landing-page + env CI=true DEBUG=true npm --prefix lively.freezer run build-landing-page + loading-screen: rm -rf lively.server/.module_cache rm -rf lively.freezer/loading-screen env CI=true npm --prefix lively.freezer run build-loading-screen +loading-screen-debug: + rm -rf lively.server/.module_cache + rm -rf lively.freezer/loading-screen + env CI=true DEBUG=true npm --prefix lively.freezer run build-loading-screen + clear-freezer-dir: rm -rf lively.freezer/landing-page rm -rf lively.freezer/loading-screen diff --git a/flatn/flatn-cjs.js b/flatn/flatn-cjs.js index f451c7a8ac..b895176706 100644 --- a/flatn/flatn-cjs.js +++ b/flatn/flatn-cjs.js @@ -14618,7 +14618,7 @@ function sortByReference (depGraph, startNode) { /** * An interval defining an upper and a lower bound. * @typedef { number[] } Interval - * @property {number} 0 - The lower bound of the interval. + * @property {number} 0 - The lower bound of the interval. * @property {number} 1 - The upper bound of the interval. */ diff --git a/lively.ast/lib/query.js b/lively.ast/lib/query.js index 8635b8b262..473162596c 100644 --- a/lively.ast/lib/query.js +++ b/lively.ast/lib/query.js @@ -495,31 +495,23 @@ function imports (scope) { return imports; } -function exports (scope, resolve = false) { - if (resolve) resolveReferences(scope); - - const exports = []; - for (const node of scope.exportDecls) { - var exportsStmt = statementOf(scope.node, node); - if (!exportsStmt) continue; - - var from = exportsStmt.source ? exportsStmt.source.value : null; +function handleExportStmt (exportsStmt, scope, node = exportsStmt) { + var from = exportsStmt.source ? exportsStmt.source.value : null; if (exportsStmt.type === 'ExportAllDeclaration') { - exports.push({ + return [{ local: null, exported: '*', imported: '*', fromModule: from, node: node, type: 'all' - }); - continue; + }]; } if (exportsStmt.type === 'ExportDefaultDeclaration') { if (helpers.isDeclaration(exportsStmt.declaration)) { - exports.push({ + return [{ local: exportsStmt.declaration.id ? exportsStmt.declaration.id.name : null, exported: 'default', type: exportsStmt.declaration.type === 'FunctionDeclaration' @@ -531,13 +523,12 @@ function exports (scope, resolve = false) { node: node, decl: exportsStmt.declaration, declId: exportsStmt.declaration.id - }); - continue; + }]; } if (exportsStmt.declaration.type === 'Identifier') { const { decl, declId } = scope.resolvedRefMap.get(exportsStmt.declaration) || {}; - exports.push({ + return [{ local: exportsStmt.declaration.name, exported: 'default', fromModule: null, @@ -545,12 +536,11 @@ function exports (scope, resolve = false) { type: 'id', decl, declId - }); - continue; + }] } // exportsStmt.declaration is an expression - exports.push({ + return [{ local: null, exported: 'default', fromModule: null, @@ -558,12 +548,11 @@ function exports (scope, resolve = false) { type: 'expr', decl: exportsStmt.declaration, declId: exportsStmt.declaration - }); - continue; + }]; } if (exportsStmt.specifiers && exportsStmt.specifiers.length) { - exports.push(...exportsStmt.specifiers.map(exportSpec => { + return exportsStmt.specifiers.map(exportSpec => { let decl, declId; if (from) { // "export { x as y } from 'foo'" is the only case where export @@ -585,12 +574,11 @@ function exports (scope, resolve = false) { decl, declId }; - })); - continue; + }) } if (exportsStmt.declaration && exportsStmt.declaration.declarations) { - exports.push(...exportsStmt.declaration.declarations.map(decl => { + return exportsStmt.declaration.declarations.map(decl => { return { local: decl.id ? decl.id.name : 'default', exported: decl.id ? decl.id.name : 'default', @@ -600,12 +588,11 @@ function exports (scope, resolve = false) { decl: decl, declId: decl.id }; - })); - continue; + }) } if (exportsStmt.declaration) { - exports.push({ + return [{ local: exportsStmt.declaration.id ? exportsStmt.declaration.id.name : 'default', exported: exportsStmt.declaration.id ? exportsStmt.declaration.id.name : 'default', type: exportsStmt.declaration.type === 'FunctionDeclaration' @@ -617,9 +604,21 @@ function exports (scope, resolve = false) { node: node, decl: exportsStmt.declaration, declId: exportsStmt.declaration.id - }); - continue; + }] } + + return []; +} + +function exports (scope, resolve = false) { + if (resolve) resolveReferences(scope); + + const exports = []; + for (const node of scope.exportDecls) { + var exportsStmt = statementOf(scope.node, node); + if (!exportsStmt) continue; + + exports.push(...handleExportStmt(exportsStmt, scope, node)); } return arr.uniqBy(exports, (a, b) => @@ -665,5 +664,6 @@ export { refWithDeclAt, imports, exports, + handleExportStmt, queryNodes }; diff --git a/lively.classes/class-to-function-transform.js b/lively.classes/class-to-function-transform.js index 88b9462d99..0e328c9bce 100644 --- a/lively.classes/class-to-function-transform.js +++ b/lively.classes/class-to-function-transform.js @@ -344,7 +344,12 @@ function replaceClass (node, state, options) { : classId ? [n.varDecl(classId, constructorTemplate(classId.name, fields, options)), n.varDecl(n.id(tempLivelyClassVar), classId)] : [n.varDecl(n.id(tempLivelyClassVar), constructorTemplate(null, fields, options))], - n.ifStmt(n.funcCall(n.member(n.id('Object'), n.id('isFrozen')), [n.id(tempLivelyClassHolderVar)]), n.block([n.returnStmt(n.id(tempLivelyClassVar))]), null), + n.ifStmt( + n.logicalExpr('||', + n.funcCall(n.member(n.id('Object'), n.id('isFrozen')), [n.id(tempLivelyClassHolderVar)]), + n.funcCall(n.member(n.id('Object'), n.id('isFrozen')), [n.member(n.id(tempLivelyClassVar), n.id('prototype'))])), + n.block([n.returnStmt(n.id(tempLivelyClassVar))]), + null), n.returnStmt( n.funcCall( options.functionNode, diff --git a/lively.classes/index.js b/lively.classes/index.js index 160e015c9a..6a53423efd 100644 --- a/lively.classes/index.js +++ b/lively.classes/index.js @@ -1,4 +1,4 @@ import * as runtime from './runtime.js'; -import { classToFunctionTransform } from './class-to-function-transform.js'; +import { classToFunctionTransform, classToFunctionTransformBabel } from './class-to-function-transform.js'; -export { runtime, classToFunctionTransform }; +export { runtime, classToFunctionTransform, classToFunctionTransformBabel }; diff --git a/lively.classes/tests/class-to-function-transform-test.js b/lively.classes/tests/class-to-function-transform-test.js index e5481589c3..fb535e39be 100644 --- a/lively.classes/tests/class-to-function-transform-test.js +++ b/lively.classes/tests/class-to-function-transform-test.js @@ -32,7 +32,7 @@ function classTemplate (className, superClassName, methodString, classMethodStri return this[Symbol.for("lively-instance-initialize")].apply(this, arguments); } };${(useClassHolder || !className) ? '' : '\n var __lively_class__ = Foo;'} - if (Object.isFrozen(__lively_classholder__)) { + if (Object.isFrozen(__lively_classholder__) || Object.isFrozen(__lively_class__.prototype)) { return __lively_class__; } return initializeClass(__lively_class__, superclass, ${methodString}, ${classMethodString}, ${ useClassHolder ? '__lively_classholder__' : 'null'}, ${moduleMeta}${pos}); diff --git a/lively.freezer/package.json b/lively.freezer/package.json index 832c6f1b8d..23f9b8c4ce 100644 --- a/lively.freezer/package.json +++ b/lively.freezer/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "type": "module", "dependencies": { - "@babel/core": "^7.12.3", + "@babel/core": "7.26.0", "@babel/cli": "^7.12.1", "@babel/preset-env": "^7.12.1", "@babel/plugin-transform-runtime": "^7.12.1", @@ -49,6 +49,61 @@ "zlib": { "~node": "@empty" } + }, + "importMap": { + "imports": { + "@babel/core": "esm://ga.jspm.io/npm:@babel/core@7.26.0/lib/dev.index.js", + "@babel/types": "esm://ga.jspm.io/npm:@babel/types@7.26.9/lib/index.js" + }, + "scopes": { + "esm://ga.jspm.io/": { + "#lib/config/files/index.js": "esm://ga.jspm.io/npm:@babel/core@7.26.0/lib/config/files/index-browser.js", + "#lib/config/resolve-targets.js": "esm://ga.jspm.io/npm:@babel/core@7.26.0/lib/config/resolve-targets-browser.js", + "#lib/transform-file.js": "esm://ga.jspm.io/npm:@babel/core@7.26.0/lib/transform-file-browser.js", + "#node.js": "esm://ga.jspm.io/npm:browserslist@4.24.4/browser.js", + "@ampproject/remapping": "esm://ga.jspm.io/npm:@ampproject/remapping@2.3.0/dist/remapping.umd.js", + "@babel/code-frame": "esm://ga.jspm.io/npm:@babel/code-frame@7.26.2/lib/index.js", + "@babel/compat-data/native-modules": "esm://ga.jspm.io/npm:@babel/compat-data@7.26.8/native-modules.js", + "@babel/compat-data/plugins": "esm://ga.jspm.io/npm:@babel/compat-data@7.26.8/plugins.js", + "@babel/generator": "esm://ga.jspm.io/npm:@babel/generator@7.26.9/lib/index.js", + "@babel/helper-compilation-targets": "esm://ga.jspm.io/npm:@babel/helper-compilation-targets@7.26.5/lib/index.js", + "@babel/helper-module-imports": "esm://ga.jspm.io/npm:@babel/helper-module-imports@7.25.9/lib/index.js", + "@babel/helper-module-transforms": "esm://ga.jspm.io/npm:@babel/helper-module-transforms@7.26.0/lib/index.js", + "@babel/helper-string-parser": "esm://ga.jspm.io/npm:@babel/helper-string-parser@7.25.9/lib/index.js", + "@babel/helper-validator-identifier": "esm://ga.jspm.io/npm:@babel/helper-validator-identifier@7.25.9/lib/index.js", + "@babel/helper-validator-option": "esm://ga.jspm.io/npm:@babel/helper-validator-option@7.25.9/lib/index.js", + "@babel/helpers": "esm://ga.jspm.io/npm:@babel/helpers@7.26.9/lib/index.js", + "@babel/parser": "esm://ga.jspm.io/npm:@babel/parser@7.26.9/lib/index.js", + "@babel/template": "esm://ga.jspm.io/npm:@babel/template@7.26.9/lib/index.js", + "@babel/traverse": "esm://ga.jspm.io/npm:@babel/traverse@7.26.9/lib/index.js", + "@jridgewell/gen-mapping": "esm://ga.jspm.io/npm:@jridgewell/gen-mapping@0.3.8/dist/gen-mapping.umd.js", + "@jridgewell/resolve-uri": "esm://ga.jspm.io/npm:@jridgewell/resolve-uri@3.1.2/dist/resolve-uri.umd.js", + "@jridgewell/set-array": "esm://ga.jspm.io/npm:@jridgewell/set-array@1.2.1/dist/set-array.umd.js", + "@jridgewell/sourcemap-codec": "esm://ga.jspm.io/npm:@jridgewell/sourcemap-codec@1.5.0/dist/sourcemap-codec.umd.js", + "@jridgewell/trace-mapping": "esm://ga.jspm.io/npm:@jridgewell/trace-mapping@0.3.25/dist/trace-mapping.umd.js", + "assert": "esm://ga.jspm.io/npm:@jspm/core@2.1.0/nodelibs/browser/assert.js", + "browserslist": "esm://ga.jspm.io/npm:browserslist@4.24.4/index.js", + "buffer": "esm://ga.jspm.io/npm:@jspm/core@2.1.0/nodelibs/browser/buffer.js", + "caniuse-lite/dist/unpacker/agents": "esm://ga.jspm.io/npm:caniuse-lite@1.0.30001701/dist/unpacker/agents.js", + "convert-source-map": "esm://ga.jspm.io/npm:convert-source-map@2.0.0/index.js", + "debug": "esm://ga.jspm.io/npm:debug@4.4.0/src/browser.js", + "electron-to-chromium/versions": "esm://ga.jspm.io/npm:electron-to-chromium@1.5.107/versions.js", + "fs": "esm://ga.jspm.io/npm:@jspm/core@2.1.0/nodelibs/browser/fs.js", + "gensync": "esm://ga.jspm.io/npm:gensync@1.0.0-beta.2/index.js", + "globals": "esm://ga.jspm.io/npm:globals@11.12.0/index.js", + "js-tokens": "esm://ga.jspm.io/npm:js-tokens@4.0.0/index.js", + "jsesc": "esm://ga.jspm.io/npm:jsesc@3.1.0/jsesc.js", + "lru-cache": "esm://ga.jspm.io/npm:lru-cache@5.1.1/index.js", + "ms": "esm://ga.jspm.io/npm:ms@2.1.3/index.js", + "node-releases/data/processed/envs.json": "esm://ga.jspm.io/npm:node-releases@2.0.19/data/processed/envs.json.js", + "node-releases/data/release-schedule/release-schedule.json": "esm://ga.jspm.io/npm:node-releases@2.0.19/data/release-schedule/release-schedule.json.js", + "path": "esm://ga.jspm.io/npm:@jspm/core@2.1.0/nodelibs/browser/path.js", + "picocolors": "esm://ga.jspm.io/npm:picocolors@1.1.1/picocolors.browser.js", + "process": "esm://ga.jspm.io/npm:@jspm/core@2.1.0/nodelibs/browser/process.js", + "semver": "esm://ga.jspm.io/npm:semver@6.3.1/semver.js", + "yallist": "esm://ga.jspm.io/npm:yallist@3.1.1/yallist.js" + } + } } }, "lively": { diff --git a/lively.freezer/src/bundler.js b/lively.freezer/src/bundler.js index 8bdbf3ab32..f3b99aea6c 100644 --- a/lively.freezer/src/bundler.js +++ b/lively.freezer/src/bundler.js @@ -1,4 +1,6 @@ /* global process */ +import babel from '@babel/core'; +import t from '@babel/types'; import { resource } from 'lively.resources'; import * as ast from 'lively.ast'; import * as classes from 'lively.classes'; @@ -10,6 +12,15 @@ import { ensureComponentDescriptors, replaceExportedNamespaces } from 'lively.source-transform'; +import { + ensureComponentDescriptors as babel_ensureComponentDescriptors, + replaceExportedVarDeclarations as babel_replaceExportedVarDeclarations, + replaceExportedNamespaces as babel_replaceExportedNamespaces, + replaceImportedNamespaces as babel_replaceImportedNamespaces, + rewriteToCaptureTopLevelVariables as babel_rewriteToCaptureTopLevelVariables, + getScopeFromPath, + babelNodes +} from 'lively.source-transform/babel/plugin.js'; import { rewriteToCaptureTopLevelVariables, insertCapturesForFunctionDeclarations, @@ -71,30 +82,86 @@ const ADVANCED_EXCLUDED_MODULES = [ const baseURL = typeof System !== 'undefined' ? System.baseURL : ensureFolder(process.env.lv_next_dir || process.cwd()); -export function bulletProofNamespaces (code) { +export function bulletProofNamespaces (code, chunkFileName, isResurrectionBuild, sourceMap = false) { + if (sourceMap) { + let { code: transformedCode, map } = babel.transform(code, { + sourceMaps: true, + comments: true, + compact: true, + plugins: [() => ({ + visitor: { + Program (path) { + const functionBody = path.get('body.0.expression.arguments.1.body') + const [useStrictDirective] = functionBody.get('directives'); + if (useStrictDirective) { + useStrictDirective.remove(); + functionBody.unshiftContainer('body', babel.parse("var __contextModule__ = typeof module !== 'undefined' ? module : arguments[1];").program.body[0]); + } + if (isResurrectionBuild) { + path.get('body.0.expression.callee.object').replaceWith(t.Identifier('BootstrapSystem')); + path.unshiftContainer('body', babel.parse(`BootstrapSystem._currentFile = "${chunkFileName}";`).program.body[0]); + } + }, + VariableDeclaration (path) { + let hasPureComment = false; + for (const declarator of path.node.declarations) { + const init = declarator.init; + if (!init) continue; + const leading = init.leadingComments; + if (Array.isArray(leading)) { + hasPureComment = leading.some(comment => { + return comment.value.trim() === "#__PURE__"; + }); + + if (hasPureComment) { + break; + } + } + } + if (!hasPureComment) return; + try { + const matchingGetter = path.get('declarations.0.init.arguments.0.properties').find(({ node: prop }) => prop?.key?.name === 'default' && prop.kind === 'get'); + const [returnStmt] = matchingGetter.get('body.body'); + if (!returnStmt?.isReturnStatement()) return; + matchingGetter.get('body').unshiftContainer('body', babel.parse(`if (typeof ${returnStmt.node.argument.name} === 'undefined') throw new Error('Module not yet initialized!');`).program.body[0]) + } catch (err) { + + } + } + } + })] + }); + return { code: transformedCode, map } + } + let rewrites = []; let parsed = ast.parse(code, { withComments: true }); const pureComments = parsed.allComments.filter(c => c.text === '#__PURE__'); - if (pureComments.length === 0) return null; - parsed = ast.ReplaceVisitor.run(parsed, (node) => { - if (node.type === 'VariableDeclaration') { - const matchingComment = pureComments.find(c => node.start < c.start && c.end > node.end); - if (!matchingComment) return node; - const matchingGetter = node.declarations[0]?.init?.arguments?.[0]?.properties?.find(prop => prop.key.name === 'default' && prop.kind === 'get'); - if (!matchingGetter) return node; - const getterBody = matchingGetter.value.body; - const [returnStmt] = getterBody.body; - rewrites.push([getterBody, `\nif (typeof ${returnStmt.argument.name} === 'undefined') throw new Error('Module not yet initialized!');\n`]) - } - return node; - }); + if (pureComments.length > 0) { + ast.ReplaceVisitor.run(parsed, (node) => { + if (node.type === 'VariableDeclaration') { + const matchingComment = pureComments.find(c => node.start < c.start && c.end > node.end); + if (!matchingComment) return node; + const matchingGetter = node.declarations[0]?.init?.arguments?.[0]?.properties?.find(prop => prop.key.name === 'default' && prop.kind === 'get'); + if (!matchingGetter) return node; + const getterBody = matchingGetter.value.body; + const [returnStmt] = getterBody.body; + rewrites.push([getterBody, `\nif (typeof ${returnStmt.argument.name} === 'undefined') throw new Error('Module not yet initialized!');\n`]); + } + return node; + }); + } if (rewrites.length > 0) { arr.sortBy(rewrites, ([node]) => node.start).forEach(([node, snippet]) => { code = code.slice(0, node.start + 1) + snippet + code.slice(node.start + 1); }); - return code; } - return null; + if (isResurrectionBuild) { + // this messes up the source map + code = code.replace('System.register', `BootstrapSystem._currentFile = "${chunkFileName}";\nBootstrapSystem.register`); + } + code = code.replace("'use strict';", "var __contextModule__ = typeof module !== 'undefined' ? module : arguments[1];\n"); + return { code }; } /** @@ -151,7 +218,6 @@ function resolutionId (id, importer) { * @returns { boolean } Wether or not the module was served from an ESM CDN. */ function isCdnImport (id, importer, resolver) { - if (ESM_CDNS.find(cdn => id.match(cdn) || importer.match(cdn)) && importer && importer !== ROOT_ID) { const { url } = resource(resolver.ensureFileFormat(importer)).root(); // get the cdn host root return ESM_CDNS.find(cdn => url.match(cdn)); @@ -178,7 +244,8 @@ export default class LivelyRollup { compress = true, minify = true, captureModuleScope = true, - verbose = false + verbose = false, + sourceMap = false }) { this.verbose = verbose; // wether or not to log the warnings to the console that happen during build this.resolver = resolver; // resolves the modules to the respective urls, for either client or browser @@ -195,6 +262,7 @@ export default class LivelyRollup { this.includeLivelyAssets = includeLivelyAssets; // If set to true, will include the default fonts and css from lively.next into the bundle. Disabling this is probably a bad idea. this.compress = compress; // If true, this will perform custom compression of the files to brotli and gzip. this.minify = minify; // If true, will invoke the google closure minification to further reduce source code size. + this.sourceMap = sourceMap; this.globalMap = {}; // accumulates the package -> url mappings that are provided by each of the packages this.modulesWithDynamicLoads = new Set(); // collection of all modules that include System.import() @@ -205,6 +273,7 @@ export default class LivelyRollup { this.customFontFiles = []; this.projectsInBundle = new Set(); this.moduleToPkg = new Map(); + this.moduleSources = {}; this.resolver.setStatus({ label: 'Freezing in Progress' }); } @@ -233,11 +302,11 @@ export default class LivelyRollup { * @param { string } path - The relative path to be imported. */ // FIXME: the reason this is async is because we still keep the browser resolver around... - async resolveRelativeImport (moduleId, path) { + resolveRelativeImport (moduleId, path) { if (!path.startsWith('.')) return this.resolver.normalizeFileName(path); // how to achieve that without the nasty file handle - return await this.resolver.normalizeFileName( - string.joinPath(await this.resolver.normalizeFileName(moduleId), '..', path)); + return this.resolver.normalizeFileName( + string.joinPath(this.resolver.normalizeFileName(moduleId), '..', path)); } /** @@ -257,40 +326,64 @@ export default class LivelyRollup { */ getTransformOptions (modId, parsedSource) { if (modId === '@empty.js') return {}; + const parsedGlobals = parsedSource.scope?.globals && Object.keys(parsedSource.scope?.globals) || GlobalInjector.getGlobals(null, parsedSource); let version, name; const pkg = this.resolver.resolvePackage(modId); if (pkg) { name = pkg.name; version = pkg.version; + } else if (modId.startsWith('esm://')) { + [name, version] = resource(modId).path().slice(1).split('@'); + if (version) version = version.split('/')[0]; } else { // assuming the module comes from jspm version = modId.split('@')[1]; name = modId.split('npm:')[1].split('@')[0]; } - const classToFunction = { - classHolder: ast.parse(`((lively.FreezerRuntime || lively.frozenModules).recorderFor("${this.normalizedId(modId)}", __contextModule__))`), - functionNode: { type: 'Identifier', name: 'initializeES6ClassForLively' }, - transform: classes.classToFunctionTransform, - currentModuleAccessor: ast.parse(`({ - pathInPackage: () => { - return "${this.resolver.pathInPackageFor(modId)}" + const classToFunction = this.sourceMap ? { + classHolder: babel.parse(`((lively.FreezerRuntime || lively.frozenModules).recorderFor("${this.normalizedId(modId)}", __contextModule__))`).program.body[0].expression, + functionNode: t.Identifier('initializeES6ClassForLively'), + transform: (path, options) => { + classes.classToFunctionTransformBabel(path, {}, options); }, - unsubscribeFromToplevelDefinitionChanges: () => () => {}, - subscribeToToplevelDefinitionChanges: () => () => {}, - package: () => { - return { - name: "${name}", - version: "${version}" + nodes: babelNodes, + currentModuleAccessor: babel.parse(`({ + pathInPackage: () => { + return "${this.resolver.pathInPackageFor(modId)}" + }, + unsubscribeFromToplevelDefinitionChanges: () => () => {}, + subscribeToToplevelDefinitionChanges: () => () => {}, + package: () => { + return { + name: "${name}", + version: "${version}" + } } - } - })`).body[0].expression - }; + })`).program.body[0].expression + } : { + classHolder: ast.parse(`((lively.FreezerRuntime || lively.frozenModules).recorderFor("${this.normalizedId(modId)}", __contextModule__))`).body[0].expression, + functionNode: { type: 'Identifier', name: 'initializeES6ClassForLively' }, + transform: classes.classToFunctionTransform, + currentModuleAccessor: ast.parse(`({ + pathInPackage: () => { + return "${this.resolver.pathInPackageFor(modId)}" + }, + unsubscribeFromToplevelDefinitionChanges: () => () => {}, + subscribeToToplevelDefinitionChanges: () => () => {}, + package: () => { + return { + name: "${name}", + version: "${version}" + } + } + })`).body[0].expression + }; return { - captureImports: false, // we do not need to support inline evals within bundled modules, + captureImports: this.sourceMap, // for the babel transform, we need to capture imports as well exclude: [ 'System', '__contextModule__', - ...this.resolver.dontTransform(modId, [...ast.query.knownGlobals, ...GlobalInjector.getGlobals(null, parsedSource)]), + ...this.resolver.dontTransform(modId, [...ast.query.knownGlobals, ...parsedGlobals]), ...arr.range(0, 50).map(i => `__captured${i}__`) ], classToFunction @@ -317,7 +410,7 @@ export default class LivelyRollup { * world as the argument. */ async synthesizeMainModule () { - let mainModuleSource = await resource(this.resolver.ensureFileFormat(await this.resolver.normalizeFileName('lively.freezer/src/util/main-module.js'))).read(); + let mainModuleSource = await resource(this.resolver.ensureFileFormat(this.resolver.normalizeFileName('lively.freezer/src/util/main-module.js'))).read(); mainModuleSource = mainModuleSource.replaceAll('TRACE', this.isResurrectionBuild ? 'true' : 'false'); return mainModuleSource.replace('prepare()', `const { main, WORLD_CLASS = World, TITLE } = await System.import('${this.rootModuleId}')`); } @@ -422,13 +515,31 @@ export default class LivelyRollup { }); } + babel_instrumentDynamicLoads (path) { + const self = this; + path.traverse({ + CallExpression (path) { + if (path.get('callee').type === 'MemberExpression' && path.get('arguments').length === 1) { + const { property, object } = path.get('callee').node; + if (property.name === 'import' && object.name === 'System') { + try { + const resolvedImport = eval(path.get('arguments')[0].getSource()); + if (resolvedImport) self.hasDynamicImports = true; + path.replaceWith(babel.parse(`import("${resolvedImport}")`).program.body[0].expression); + } catch (err) { + } + } + } + } + }); + } + /** * A custom transform() callback for RollupJS. * @param { string } source - The source code of a module. * @param { string } id - The id of the module to be transformed. */ async transform (source, id) { - const originalSource = source; if (id.startsWith('\0') || id.endsWith('.json') || this.excludedModules.find(m => id.startsWith(m))) { return source; } @@ -450,12 +561,53 @@ export default class LivelyRollup { return `projectAsset(\'${newName}\')`; }; - source = source.replaceAll(projectAssetRegex, assetNameRewriter); + source = source.replaceAll(projectAssetRegex, assetNameRewriter); // needs to be performed in a way to preserve sourcemap + } + + // FIXME: here we need to move over to a babel transform and also pass over the sourcemap + // The easiest way to achieve thatis via an inline visitor that basically performs the same steps as below within babel. + + const needsLoadInstrumentation = this.needsDynamicLoadTransform(source); + const self = this; + + if (this.sourceMap) { + + function inlinePlugin () { + return { + visitor: { + Program (path, state) { + const source = self.moduleSources[id]; + if (!source) return; + if (id === ROOT_ID && !needsLoadInstrumentation) return; + if (needsLoadInstrumentation) { + self.babel_instrumentDynamicLoads(path, state, id); + } + if (id === ROOT_ID) return; + // this capturing stuff needs to behave differently when we have dynamic imports. Why?? + const instrumentClasses = self.needsClassInstrumentation(id, source); + if (instrumentClasses || self.needsScopeToBeCaptured(id, null, source)) { + const sourceHash = string.hashCode(source); // why cant we use the original source here? because other plugins already scrambled the code... + self.babel_captureScope(path, id, sourceHash, instrumentClasses); + } + } + } + }; + } + + const { code, map } = babel.transform(source, { + sourceMaps: true, + compact: true, + comments: false, + plugins: [inlinePlugin] + }); + + return { code, map }; + } let parsed = ast.parse(source); - if (this.needsDynamicLoadTransform(source)) { + if (needsLoadInstrumentation) { parsed = this.instrumentDynamicLoads(parsed, id); } @@ -475,7 +627,7 @@ export default class LivelyRollup { * @param { string } id - The module id to be resolved. * @param { string } importer - The module id that is importing said module. */ - async resolveId (id, importer) { + resolveId (id, importer) { if (this.resolved[resolutionId(id, importer)]) return this.resolved[resolutionId(id, importer)]; if (id === ROOT_ID) return id; // handle standalone @@ -529,7 +681,7 @@ export default class LivelyRollup { if (id.startsWith('.')) { // handle some kind of relative import try { - absolutePath = await this.resolveRelativeImport(importer, id); + absolutePath = this.resolveRelativeImport(importer, id); if (this.belongsToExcludedPackage(absolutePath)) return null; return this.resolved[resolutionId(id, importer)] = absolutePath; } catch (err) { @@ -579,7 +731,12 @@ export default class LivelyRollup { * @param { string } id - The module id to getch the source code for. * @returns { string } The source code. */ - async load (id) { + + async load(id) { + return this.moduleSources[id] = await this.perform_load(id); + } + + async perform_load (id) { if (this.excludedModules.find(m => id.startsWith(m))) { if (id === 'lively.ast') { return ` @@ -749,6 +906,139 @@ export default class LivelyRollup { return instrumented; } + babel_captureScope (path, id, hashCode, instrumentClasses) { + let classRuntimeImport = ''; + const recorderName = '__varRecorder__'; + + const exports = []; + const self = this; + + const scope = { resolvedRefMap: new Map(), decls: [] }; + + Object.values(path.scope.bindings).map(binding => { + let decl = binding.path.node; + if (decl.type === 'ImportSpecifier' || decl.type === 'ImportDefaultSpecifier') decl = binding.path.parent; + scope.decls.push([decl, binding.identifier]); // this data format is just weird af? + binding.referencePaths.forEach(ref => { + scope.resolvedRefMap.set(ref.node, { decl, declId: binding.identifier, ref }); + }); + }); + + path.traverse({ + ExportDeclaration (path) { + for (let exp of ast.query.handleExportStmt(path.node, scope)) { + if (exp.local && exp.exported !== 'default' && exp.exported !== exp.local) { + // retrieve all the exports of the module + exports.push(JSON.stringify('__rename__' + exp.local + '->' + exp.exported)); + continue; + } + if (exp.exported === '*') { + // retrieve all the exports of the module + exports.push(JSON.stringify('__reexport__' + self.normalizedId(self.resolveId(exp.fromModule, id)))); + continue; + } + if (exp.exported === 'default' && exp.local) { + exports.push(JSON.stringify('__default__' + exp.local)); // in order to capture this + } + exports.push(JSON.stringify(exp.exported)); + } + } + }) + + const localLivelyVar = Object.keys(path.scope.references).includes('lively'); + const recorderString = this.captureModuleScope + ? `${localLivelyVar ? GLOBAL_FETCH : ''} const ${recorderName} = (${localLivelyVar ? 'G.' : ''}lively.FreezerRuntime || ${localLivelyVar ? 'G.' : ''}lively.frozenModules).recorderFor("${this.normalizedId(id)}", __contextModule__);\n` + : ''; + const moduleHash = `${recorderName}.__module_hash__ = ${hashCode};\n`; + const moduleExports = `${recorderName}.__module_exports__ = ${recorderName}.__module_exports__ || [${exports.join(',')}];\n`; + const captureObj = t.Identifier(recorderName); + const opts = this.getTransformOptions(this.resolver.resolveModuleId(id), path); + const currentModuleAccessor = opts.classToFunction.currentModuleAccessor; + + if (instrumentClasses) { + classRuntimeImport = `import { initializeClass as initializeES6ClassForLively } from "${this.isResurrectionBuild ? 'livelyClassesRuntime.js' : 'lively.classes/runtime.js'}";\n`; + } else { + opts.classToFunction = false; + } + + const normalizedId = this.normalizedId(id); + if (this.isComponentModule(id)) { + babel_ensureComponentDescriptors(path, normalizedId, { recorderName }); + } + + let defaultExport = ''; + if (this.captureModuleScope) { + babel_replaceExportedVarDeclarations(path, normalizedId, { recorderName }); + if (this.isResurrectionBuild) { + babel_replaceImportedNamespaces(path, id, this, opts); + babel_replaceExportedNamespaces(path, id, this, opts); + } + path.scope.crawl(); + Object.assign(scope, getScopeFromPath(path)); + babel_rewriteToCaptureTopLevelVariables(path, { + ...opts, + scope, + captureObj, + // declarationWrapper: t.MemberExpression(captureObj, t.StringLiteral(normalizedId + '__define__'), true), + currentModuleAccessor + }); + + const imports = []; + const toBeReplaced = []; + + path.traverse({ + ImportDeclaration (path) { + arr.pushIfNotIncluded(imports, path); + }, + ExportDefaultDeclaration (path) { + let exp; + switch (path.get('declaration').type) { + case 'Literal': + exp = path.get('declaration.raw').node; + break; + case 'Identifier': + exp = path.get('declaration.name').node; + break; + case 'ClassDeclaration': + case 'FunctionDeclaration': + exp = path.get('declaration.id.name').node; + break; + } + if (exp) defaultExport = `${captureObj.name}.default = ${exp};\n`; + } + }); + + for (const stmts of Object.values(arr.groupBy(imports, imp => imp.node.source.value))) { + const toBeMerged = stmts.filter(stmt => stmt.get('specifiers').every(spec => spec.type === 'ImportSpecifier')); + if (toBeMerged.length > 1) { + // merge statements + // fixme: if specifiers are not named, these can not be merged + // fixme: properly handle default export + const mergedSpecifiers = arr.uniqBy( + toBeMerged.map(stmt => stmt.node.specifiers).flat(), + (spec1, spec2) => + spec1.type === 'ImportSpecifier' && + spec2.type === 'ImportSpecifier' && + spec1.imported.name === spec2.imported.name && + spec1.local.name === spec2.local.name + ); + toBeMerged[0].set('specifiers', mergedSpecifiers); + toBeMerged.slice(1).map(stmt => { + stmt.remove(); + }); + } + } + } + + path.unshiftContainer('body', [ + ...babel.parse(recorderString).program.body, + ...babel.parse(this.isResurrectionBuild ? moduleHash + moduleExports : '').program.body, + ...babel.parse(classRuntimeImport).program.body + ]); + + path.pushContainer('body', babel.parse(defaultExport).program.body); + } + /** * Automatically generates a variable name from a module id. * This variable name can be used for storing the module scope @@ -819,9 +1109,9 @@ export default class LivelyRollup { // however that does not allow us to transition to the dynamic lively.modules system // so we can only utilize s.js in case we do not want to resurrect if (this.needsOldSystem) { - code += await resource(this.resolver.ensureFileFormat(await this.resolver.normalizeFileName('lively.freezer/src/util/system.0.21.js'))).read(); + code += await resource(this.resolver.ensureFileFormat(this.resolver.normalizeFileName('lively.freezer/src/util/system.0.21.js'))).read(); } else { - code += await resource(this.resolver.ensureFileFormat(await this.resolver.normalizeFileName('lively.freezer/src/util/system.6.js'))).read(); + code += await resource(this.resolver.ensureFileFormat(this.resolver.normalizeFileName('lively.freezer/src/util/system.6.js'))).read(); } // stub the globals code += `(${instrumentStaticSystemJS.toString()})(System);\n`; @@ -852,9 +1142,9 @@ export default class LivelyRollup { async getRuntimeCode () { const includePolyfills = this.includePolyfills && this.asBrowserModule; - let runtimeCode = await resource(this.resolver.ensureFileFormat(await this.resolver.normalizeFileName('lively.freezer/src/util/runtime.js'))).read(); - const regeneratorSource = await resource(this.resolver.ensureFileFormat(await this.resolver.normalizeFileName('lively.freezer/src/util/regenerator-runtime.js'))).read(); - const polyfills = includePolyfills ? await resource(this.resolver.ensureFileFormat(await this.resolver.normalizeFileName('lively.freezer/deps/fetch.umd.js'))).read() : ''; + let runtimeCode = await resource(this.resolver.ensureFileFormat(this.resolver.normalizeFileName('lively.freezer/src/util/runtime.js'))).read(); + const regeneratorSource = await resource(this.resolver.ensureFileFormat(this.resolver.normalizeFileName('lively.freezer/src/util/regenerator-runtime.js'))).read(); + const polyfills = includePolyfills ? await resource(this.resolver.ensureFileFormat(this.resolver.normalizeFileName('lively.freezer/deps/fetch.umd.js'))).read() : ''; runtimeCode = `(${runtimeCode.slice(0, -1).replace('export ', '')})();\n`; if (!this.hasDynamicImports) { // If there are no dynamic imports, we compile without systemjs and @@ -884,15 +1174,7 @@ export default class LivelyRollup { async generateBundle (plugin, bundle, depsCode, importMap, opts) { const modules = Object.values(bundle); - modules.forEach(chunk => { - if (chunk.code) { - if (this.isResurrectionBuild) { - chunk.code = chunk.code.replace('System.register', `BootstrapSystem._currentFile = "${chunk.fileName}";\nBootstrapSystem.register`); - } - chunk.code = chunk.code.replace("'use strict'", "var __contextModule__ = typeof module !== 'undefined' ? module : arguments[1];\n"); - } - }); - if (this.minify && opts.format !== 'esm') { + if (this.minify && opts.format !== 'esm' && !this.sourceMap) { modules.forEach((chunk, i) => { chunk.instrumentedCode = `"${separator}",${i};\n` + chunk.code; }); @@ -921,7 +1203,7 @@ export default class LivelyRollup { plugin.emitFile({ type: 'asset', fileName: 'livelyClassesRuntime.js', - source: await this.resolver.load(await this.resolver.normalizeFileName('lively.classes/build/runtime.js')) + source: await this.resolver.load(this.resolver.normalizeFileName('lively.classes/build/runtime.js')) }); } diff --git a/lively.freezer/src/plugins/rollup.js b/lively.freezer/src/plugins/rollup.js index b82a069c06..08648efbe1 100644 --- a/lively.freezer/src/plugins/rollup.js +++ b/lively.freezer/src/plugins/rollup.js @@ -1,3 +1,4 @@ +/* global process */ import LivelyRollup, { customWarn, bulletProofNamespaces } from '../bundler.js'; import { ROOT_ID } from '../util/helpers.js'; import { obj, arr } from 'lively.lang'; @@ -104,13 +105,13 @@ export function lively (args) { opts.globals = { ...opts.globals, ...globals }; } } + opts.chunkFileNames = (chunk) => { + return `${chunk.name.replace('!cjs', '_CJS_')}-[hash].js`; + } return opts; }, - renderChunk(code) { - if (code.includes('get default ()')) { - return bulletProofNamespaces(code); - } - return null; + renderChunk(code, chunk) { + return bulletProofNamespaces(code, chunk.fileName, bundler.isResurrectionBuild, bundler.sourceMap); // this completely messes up the source mapping }, renderDynamicImport: () => { bundler.hasDynamicImports = true; // set flag to handle dynamic imports diff --git a/lively.freezer/src/resolvers/node.cjs b/lively.freezer/src/resolvers/node.cjs index 7386759be8..b5884539f2 100644 --- a/lively.freezer/src/resolvers/node.cjs +++ b/lively.freezer/src/resolvers/node.cjs @@ -72,7 +72,7 @@ function detectFormatFromSource (source) { } -async function normalizeFileName (fileName) { +function normalizeFileName (fileName) { if (isAlreadyResolved(fileName)) return fileName; return require.resolve(fileName); } diff --git a/lively.freezer/src/util/runtime.js b/lively.freezer/src/util/runtime.js index 25dbf13a02..bea409cd8a 100644 --- a/lively.freezer/src/util/runtime.js +++ b/lively.freezer/src/util/runtime.js @@ -590,7 +590,8 @@ export function runtimeDefinition () { const [local, exported] = exp.replace('__rename__', '').split('->'); exports[exported] = rec[local]; } else if (exp.startsWith('__reexport__')) Object.assign(exports, this.exportsOf(exp.replace('__reexport__', ''))); - else exports[exp] = rec[exp]; + else if (exp.startsWith('__default__')) exports.default = rec[exp.replace('__default__', '')] ; + else if (exp in rec) exports[exp] = rec[exp]; } return exports; }, diff --git a/lively.freezer/tools/build.landing-page.mjs b/lively.freezer/tools/build.landing-page.mjs index 69650b1934..e9501a203d 100644 --- a/lively.freezer/tools/build.landing-page.mjs +++ b/lively.freezer/tools/build.landing-page.mjs @@ -8,7 +8,7 @@ import PresetEnv from '@babel/preset-env'; const verbose = process.argv[2] === '--verbose'; const minify = !process.env.CI; - +const sourceMap = !!process.env.DEBUG; try { const build = await rollup({ input: './src/landing-page.cp.js', @@ -25,6 +25,7 @@ try { }, minify, verbose, + sourceMap, isResurrectionBuild: true, asBrowserModule: true, excludedModules: [ @@ -51,6 +52,7 @@ try { await build.write({ format: 'system', dir: 'landing-page', + sourcemap: sourceMap ? 'inline' : false, globals: { chai: 'chai', mocha: 'mocha', diff --git a/lively.freezer/tools/build.loading-screen.mjs b/lively.freezer/tools/build.loading-screen.mjs index 3a181b986f..f20611741b 100644 --- a/lively.freezer/tools/build.loading-screen.mjs +++ b/lively.freezer/tools/build.loading-screen.mjs @@ -8,6 +8,7 @@ import PresetEnv from '@babel/preset-env'; const verbose = process.argv[2] === '--verbose'; const minify = !process.env.CI; +const sourceMap = !!process.env.DEBUG; try { const build = await rollup({ input: './src/loading-screen.cp.js', @@ -22,6 +23,7 @@ try { ` }, + sourceMap, minify, verbose, isResurrectionBuild: true, @@ -49,6 +51,7 @@ try { await build.write({ format: 'system', dir: 'loading-screen', + sourcemap: sourceMap ? 'inline' : false, globals: { chai: 'chai', mocha: 'mocha', diff --git a/lively.lang/interval.js b/lively.lang/interval.js index 1e8c764459..d775f970cb 100644 --- a/lively.lang/interval.js +++ b/lively.lang/interval.js @@ -13,7 +13,7 @@ import { timeToRunN } from './function.js'; /** * An interval defining an upper and a lower bound. * @typedef { number[] } Interval - * @property {number} 0 - The lower bound of the interval. + * @property {number} 0 - The lower bound of the interval. * @property {number} 1 - The upper bound of the interval. */ @@ -91,7 +91,7 @@ function compare (a, b) { * interval.coalesce([3,6], [4,5]) // => [3,6] */ function coalesce (interval1, interval2, optMergeCallback) { - const cmpResult = this.compare(interval1, interval2); + const cmpResult = compare(interval1, interval2); let temp; switch (cmpResult) { case -3: @@ -134,7 +134,7 @@ function coalesceOverlapping (intervals, optMergeCallback) { } condensed.push(ival); } - return this.sort(condensed); + return sort(condensed); } /** @@ -181,7 +181,7 @@ function mergeOverlapping (intervalsA, intervalsB, mergeFunc) { function intervalsInRangeDo (start, end, intervals, iterator, mergeFunc, context) { context = context || GLOB; // need to be sorted for the algorithm below - intervals = this.sort(intervals); + intervals = sort(intervals); let nextInterval; const collected = []; // merged intervals are already sorted, simply "negate" the interval array; while ((nextInterval = intervals.shift())) { diff --git a/lively.modules/src/system.js b/lively.modules/src/system.js index 4cb11fe659..bcc397a716 100644 --- a/lively.modules/src/system.js +++ b/lively.modules/src/system.js @@ -217,6 +217,8 @@ function prepareSystem (System, config) { ? config.useModuleTranslationCache : !urlQuery().noModuleCache; System.useModuleTranslationCache = useModuleTranslationCache; + + System.importMapCache = new Map(); if (config._nodeRequire) System._nodeRequire = config._nodeRequire; @@ -395,9 +397,9 @@ function preNormalize (System, name, parent) { mappedObject = map?.[name] || System.map[name]; } - if (importMap) { + if (importMap || (importMap = System.importMapCache.get(parent))) { let remapped = importMap.imports?.[name]; - let scope, prefix; + let scope; if (scope = Object.entries(importMap.scopes) .filter(([k, v]) => parent.startsWith(k)) .sort((a, b) => a[0].length - b[0].length) @@ -408,7 +410,11 @@ function preNormalize (System, name, parent) { if (remapped) { name = remapped; if (mappedObject) mappedObject = name; - packageRegistry.moduleUrlToPkg.set(name, pkg); + const cachedImportMap = System.importMapCache.get(name); + if (cachedImportMap) { + if (cachedImportMap !== importMap) + System.importMapCache.set(name, obj.deepMerge(cachedImportMap, importMap)); + } else System.importMapCache.set(name, importMap) } } @@ -430,8 +436,8 @@ function preNormalize (System, name, parent) { name = resolved; } - if (pkg && importMap && !packageRegistry.moduleUrlToPkg.get(name)) { - packageRegistry.moduleUrlToPkg.set(name, pkg); + if (importMap && !System.importMapCache.get(name)) { + System.importMapCache.set(name, importMap); } } diff --git a/lively.source-transform/babel/helpers.js b/lively.source-transform/babel/helpers.js index 290c8b8c38..0cb1279e76 100644 --- a/lively.source-transform/babel/helpers.js +++ b/lively.source-transform/babel/helpers.js @@ -1,6 +1,5 @@ import t from '@babel/types'; import { helpers } from 'lively.ast/lib/query.js'; -import { Path } from 'lively.lang'; export function getAncestryPath (path) { return path.getAncestry().map(m => m.inList ? [m.key, m.listKey] : m.key).flat().slice(0, -1).reverse(); @@ -259,7 +258,7 @@ export function additionalIgnoredDecls ({ varDecls, catches }) { }).flat()); } -export function additionalIgnoredRefs ({ varDecls, catches, importSpecifiers }, options) { +export function additionalIgnoredRefs ({ varDecls, catches, importSpecifiers = [] }, options) { const ignoreDecls = []; varDecls.forEach(pathToNode => { const decl = pathToNode.node; diff --git a/lively.source-transform/babel/plugin.js b/lively.source-transform/babel/plugin.js index b81b4cbbd8..f6aba629e7 100644 --- a/lively.source-transform/babel/plugin.js +++ b/lively.source-transform/babel/plugin.js @@ -4,11 +4,15 @@ import babel from '@babel/core'; import systemjsTransform from '@babel/plugin-transform-modules-systemjs'; import dynamicImport from '@babel/plugin-proposal-dynamic-import'; import { arr, Path } from 'lively.lang'; -import { topLevelFuncDecls } from 'lively.ast/lib/visitors.js'; import { query } from 'lively.ast'; +import { topLevelFuncDecls } from 'lively.ast/lib/visitors.js'; +import { classToFunctionTransformBabel } from 'lively.classes'; import { getGlobal } from 'lively.vm/lib/util.js'; import { declarationWrapperCall, annotationSym, assignExpr, varDeclOrAssignment, transformPattern, generateUniqueName, varDeclAndImportCall, importCallStmt, shouldDeclBeCaptured, importCall, exportCallStmt, exportFromImport, additionalIgnoredDecls, additionalIgnoredRefs } from './helpers.js'; -import { classToFunctionTransformBabel } from 'lively.classes/class-to-function-transform.js'; + +export function babel_parse (source) { + return babel.parse(source).program.body; +} export const defaultDeclarationWrapperName = 'lively.capturing-declaration-wrapper'; export const defaultClassToFunctionConverter = t.Identifier('initializeES6ClassForLively'); @@ -22,7 +26,7 @@ function getVarDecls (scope) { return new Set(Object.values(scope.bindings).filter(decl => decl.kind !== 'module' && decl.kind !== 'hoisted').map(m => m.path.parentPath).filter(node => node.type === 'VariableDeclaration')); } -const babelNodes = { +export const babelNodes = { member: t.MemberExpression, property: t.ObjectProperty, property: (kind, key, val) => t.ObjectProperty(key, val), @@ -346,7 +350,7 @@ function ensureGlobalBinding (ref, options) { function replaceVarDeclsAndRefs (path, options) { const globalInitStmt = '_global = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : global'; - const refsToReplace = new Set(options.scope.refs.filter(ref => !options?.excludeRefs.includes(ref.name))); + const refsToReplace = new Set(options.scope.refs.filter(ref => !options?.excludeRefs.includes(ref.name) && path.scope.bindings[ref.name]?.kind !== 'module')); const varDeclsToReplace = getVarDecls(path.scope); const declaredNames = Object.keys(path.scope.bindings); const currentModuleAccessor = options.classToFunction?.currentModuleAccessor; @@ -580,6 +584,7 @@ function splitExportDeclarations (path) { } function insertCapturesForImportAndExportDeclarations (path, options) { + let i = 0; const declaredNames = new Set(Object.keys(path.scope.bindings)); function handleDeclarations (path) { const stmt = path.node; @@ -618,6 +623,44 @@ function insertCapturesForImportAndExportDeclarations (path, options) { ? null : varDeclOrAssignment(declaredNames, specifier.local, t.MemberExpression(options.captureObj, specifier.local))))); } + + if (stmt.specifiers.length && stmt.source) { + const specifiers = stmt.specifiers; + let paths = path.replaceWithMultiple([ + t.ImportDeclaration(specifiers.map(spec => { + if (spec.local.name === 'default' && spec.exported.name === 'default') { + return t.ImportSpecifier(t.Identifier(`__default${++i}__`), t.Identifier('default')); + } else if (spec.local.name !== spec.exported.name && spec.exported.name === 'default') { + spec.shadow = `__default${++i}__`; + return t.ImportSpecifier(t.Identifier(spec.shadow), t.Identifier(spec.local.name)); + } else if (spec.local.name !== spec.exported.name && spec.local.name === 'default') { + spec.shadow = spec.exported.name; + if (declaredNames.has(spec.exported.name)) return false; + return t.ImportSpecifier(t.Identifier(spec.exported.name), t.Identifier(spec.local.name)); + } else if (spec.local.name !== spec.exported.name) { + spec.shadow = spec.exported.name; + if (declaredNames.has(spec.exported.name)) return false; + return t.ImportSpecifier(t.Identifier(spec.exported.name), t.Identifier(spec.local.name)); + } else { + spec.shadow = `__${spec.local.name}__`; + return t.ImportSpecifier(t.Identifier(spec.shadow), t.Identifier(spec.local.name)); + } + }).filter(Boolean), t.StringLiteral(stmt.source.value)), + t.ExportNamedDeclaration(null, specifiers.map(spec => { + if (spec.local.name === 'default' && spec.exported.name === 'default') { + return t.ExportSpecifier(t.Identifier(`__default${i}__`), t.Identifier('default')); + } + declaredNames.add(spec.shadow); + return t.ExportSpecifier(t.Identifier(spec.shadow), t.Identifier(spec.exported.name)); + }).filter(Boolean)) + ]); + + for (let spec of specifiers) { + if (spec.local.name === 'default' && spec.exported.name === 'default') { paths[0].insertAfter(assignExpr(options.captureObj, t.Identifier('default'), t.Identifier(`__default${i}__`), false)); } else { paths[0].insertAfter(assignExpr(options.captureObj, t.Identifier(spec.exported.name), t.Identifier(spec.shadow), false)); } + } + + paths.forEach(path => path.skip()); + } }, ExportDefaultDeclaration (path) { const stmt = path.node; @@ -728,9 +771,11 @@ export function rewriteToCaptureTopLevelVariables (path, options) { path.unshiftContainer('body', header); path.pushContainer('body', footer); + + return options; } -function ensureComponentDescriptors (path, moduleId, options) { +export function ensureComponentDescriptors (path, moduleId, options) { // check first for top level decls const varDecls = getVarDecls(path.scope); let earlyReturn = false; @@ -776,10 +821,100 @@ function ensureComponentDescriptors (path, moduleId, options) { }); } +export function replaceExportedVarDeclarations (path, moduleId, options) { + path.traverse({ + ExportNamedDeclaration (path) { + const variableDeclaration = path.get('declaration'); + if (variableDeclaration.type !== 'VariableDeclaration') return; + const [exportedVariable] = variableDeclaration.get('declarations'); + if (!exportedVariable) return; + const exportExpression = babel.parse(`var ${exportedVariable.node.id.name}; export { ${exportedVariable.node.id.name} }`).program.body[1]; + path.replaceWithMultiple([variableDeclaration.node, exportExpression]); + } + }); + + if (moduleId.includes('lively.morphic/config.js')) { + for (let i = 0; i < path.node.body.length; i++) { + const stmt = path.get('body')[i]; + if (stmt.type === 'VariableDeclaration' && stmt.node.declarations?.[0].init?.type === 'ObjectExpression') { + const { id, init } = stmt.node.declarations[0]; + stmt.get('declarations.0.init').replaceWith(t.LogicalExpression('||', babel.parse(`${options.recorderName}.${id.name}`).program.body[0].expression, init)); + } + } + } +} + +export function replaceExportedNamespaces (path, moduleName, bundler, options) { + // namespace that are directly imported or getting re-exported need to be chanelled through the module recorder + const insertNodes = []; + let i = 0; + // such that the namespaces are getting correctly updated in case a module is getting revived + path.traverse({ + ExportAllDeclaration (path) { + let dep = bundler.resolveId(path.node.source.value, moduleName); + let name = path.node.exported?.name; + const isNamed = !!name; + if (isNamed) { + insertNodes.push( + babel.parse(`const ${name} = (lively.FreezerRuntime || lively.frozenModules).exportsOf("${bundler.normalizedId(dep)}") || ${name}_namespace;`).program.body[0], + babel.parse(`export { ${name} }`).program.body[0] + ); + path.replaceWith(babel.parse(`import * as ${name}_namespace from "${path.node.source.value}";`).program.body[0]); + options.exclude.push(`${name}_namespace`); + return; + } + insertNodes.push( + babel.parse(`import * as tmp_${++i} from "${path.node.source.value}";`).program.body[0], + babel.parse(`Object.assign((lively.FreezerRuntime || lively.frozenModules).recorderFor("${bundler.normalizedId(dep)}"), tmp_${i})`).program.body[0] + ); + options.exclude.push(`tmp_${i}`); + } + }); + + const insertFrom = path.get('body').find(n => n.type !== 'ImportDeclaration' && n.type !== 'ExportAllDeclaration'); + if (insertFrom) insertFrom.insertBefore(insertNodes); + else path.pushContainer('body', insertNodes); +} + +export function replaceImportedNamespaces (path, moduleName, bundler, options) { + // namespace that are directly imported or getting re-exported need to be chanelled through the module recorder + const namespaceVars = []; + // such that the namespaces are getting correctly updated in case a module is getting revived + + path.traverse({ + ImportDeclaration (path) { + if (path.node.specifiers[0]?.type !== 'ImportNamespaceSpecifier') return; + let dep = bundler.resolveId(path.node.source.value, moduleName); + let name = path.node.specifiers[0]?.local?.name; + if (name) { + namespaceVars.push([name, dep]); + path.get('specifiers.0.local').replaceWith(t.Identifier(name + '_namespace')); + options.exclude.push(`${name}_namespace`); + } + } + }); + + // now we have to insert the the assignments of the tmp namespace imports to the initial names, + // but filtered by the recorder object + const insertFrom = path.get('body').find(n => n.type !== 'ImportDeclaration' && n.type !== 'ExportAllDeclaration'); + for (let [namespaceVar, importedModule] of namespaceVars) { + insertFrom.insertBefore(babel.parse(`const ${namespaceVar} = (lively.FreezerRuntime || lively.frozenModules).exportsOf("${bundler.normalizedId(importedModule)}") || ${namespaceVar}_namespace;`).program.body[0]); + } +} + function getExportDecls (scope) { return [...new Set(Object.values(scope.bindings).map(m => m.referencePaths.filter(m => m.parentPath.parentPath?.type.match(/ExportNamedDeclaration|ExportDefaultDeclaration/))).flat().map(m => m.parent))]; } +export function getScopeFromPath (path) { + return { + classDecls: getClassDecls(path.scope), + funcDecls: getFuncDecls(path.scope), + refs: getRefs(path.scope), + varDecls: getVarDecls(path.scope) + }; +} + function evalCodeTransform (path, state, options) { // A: Rewrite the component definitions to create component descriptors. let { moduleName } = options; @@ -800,12 +935,7 @@ function evalCodeTransform (path, state, options) { // 2. Annotate definitions with code location. This is being used by the // function-wrapper-source transform. - options.scope = { - classDecls: getClassDecls(path.scope), - funcDecls: getFuncDecls(path.scope), - refs: getRefs(path.scope), - varDecls: getVarDecls(path.scope) - }; + options.scope = getScopeFromPath(path); if (options.hasOwnProperty('evalId')) annotation.evalId = options.evalId; if (options.sourceAccessorName) annotation.sourceAccessorName = options.sourceAccessorName; @@ -1240,8 +1370,8 @@ export function setupBabelTranspiler (System) { transpiler: 'lively.transpiler.babel', babelOptions: { sourceMaps: true, - compact: false, - comments: true, + compact: false, // for some reason, the compact options messes up the source maps on a per module basis + comments: false, presets: [] } }); diff --git a/lively.source-transform/index.js b/lively.source-transform/index.js index cb885b5a0a..27a3c9aa8e 100644 --- a/lively.source-transform/index.js +++ b/lively.source-transform/index.js @@ -191,7 +191,7 @@ export async function replaceExportedNamespaces (translated, moduleName, bundler } insertNodes.push( parse(`import * as tmp_${i++} from "${exportAllDecl.source.value}";`).body[0], - (async () => parse(`Object.assign((lively.FreezerRuntime || lively.frozenModules).recorderFor("${bundler.normalizedId(await dep)}"), mp_${i++})`).body[0])() + (async () => parse(`Object.assign((lively.FreezerRuntime || lively.frozenModules).recorderFor("${bundler.normalizedId(await dep)}"), tmp_${i})`).body[0])() ); return exportAllDecl; }); diff --git a/lively.source-transform/tests/babel-test.js b/lively.source-transform/tests/babel-test.js index 2d2e6fdab8..03788e6e69 100644 --- a/lively.source-transform/tests/babel-test.js +++ b/lively.source-transform/tests/babel-test.js @@ -85,7 +85,7 @@ function classTemplate (className, superClassName, methodString, classMethodStri return this[Symbol.for("lively-instance-initialize")].apply(this, arguments); } };${useClassHolder ? '' : '\nvar __lively_class__ = Foo;'} - if (Object.isFrozen(__lively_classholder__)) { + if (Object.isFrozen(__lively_classholder__) || Object.isFrozen(__lively_class__.prototype)) { return __lively_class__; } return _createOrExtendClass(__lively_class__, superclass, ${methodString}, ${classMethodString}, ${ useClassHolder ? '__lively_classholder__' : 'null'}, ${moduleMeta}${pos}); @@ -592,11 +592,17 @@ bar;`); testVarTfm('re-export named', 'export { name1, name2 } from "foo";', - 'export {\n name1,\n name2\n} from "foo";'); + `import { name1 as __name1__, name2 as __name2__ } from "foo"; +_rec.name2 = __name2__; +_rec.name1 = __name1__; +export { __name1__ as name1, __name2__ as name2 };`); testVarTfm('export from named', 'export { name1 as foo1, name2 as bar2 } from "foo";', - 'export {\n name1 as foo1,\n name2 as bar2\n} from "foo";'); + `import { name1 as foo1, name2 as bar2 } from "foo"; +_rec.bar2 = bar2; +_rec.foo1 = foo1; +export { foo1, bar2 };`); testVarTfm('export bug 1', 'foo();\nexport function a() {}\nexport function b() {}', @@ -610,7 +616,9 @@ bar;`); '_rec.b = b;\n' + 'function c() {\n}\n' + '_rec.c = c;\n' + - 'export {\n a\n} from "./package-commands.js";\n' + + 'import {\n a as __a__\n} from "./package-commands.js";\n' + + '_rec.a = __a__;\n' + + 'export {\n __a__ as a\n};\n' + 'export {\n b\n};\n' + 'export {\n c\n};'); }); diff --git a/lively.source-transform/tests/capturing-test.js b/lively.source-transform/tests/capturing-test.js index 349f245f9c..2a1d7c8c41 100644 --- a/lively.source-transform/tests/capturing-test.js +++ b/lively.source-transform/tests/capturing-test.js @@ -61,7 +61,7 @@ function classTemplate (className, superClassName, methodString, classMethodStri return this[Symbol.for("lively-instance-initialize")].apply(this, arguments); } };${useClassHolder ? '' : '\nvar __lively_class__ = Foo;'} - if (Object.isFrozen(__lively_classholder__)) { + if (Object.isFrozen(__lively_classholder__) || Object.isFrozen(__lively_class__.prototype)) { return __lively_class__; } return _createOrExtendClass(__lively_class__, superclass, ${methodString}, ${classMethodString}, ${ useClassHolder ? '__lively_classholder__' : 'null'}, ${moduleMeta}${pos});