From 0b66e911b1cccdabe6247a98d3bf0fd0e2826633 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Sat, 29 Jul 2023 12:07:32 -0700 Subject: [PATCH] feat(plugin-infra): support esm bundler (#3460) --- apps/core/.webpack/config.ts | 6 +- apps/core/src/bootstrap/plugins/setup.ts | 79 +++++------ apps/core/src/bootstrap/register-plugins.ts | 141 ++++++++++++-------- packages/cli/package.json | 1 + packages/cli/src/bin/dev-plugin.ts | 30 ++++- plugins/bookmark/package.json | 1 + plugins/copilot/package.json | 1 + plugins/hello-world/package.json | 1 + plugins/image-preview/package.json | 1 + yarn.lock | 33 ++++- 10 files changed, 191 insertions(+), 103 deletions(-) diff --git a/apps/core/.webpack/config.ts b/apps/core/.webpack/config.ts index ac2e2365c5..f2e57e8def 100644 --- a/apps/core/.webpack/config.ts +++ b/apps/core/.webpack/config.ts @@ -32,6 +32,7 @@ const OptimizeOptionOptions: ( minimizer: [ new TerserPlugin({ minify: TerserPlugin.swcMinify, + exclude: [/\.min\.js$/, /plugins\/.+\/.+\.mjs$/], parallel: true, extractComments: true, terserOptions: { @@ -269,7 +270,10 @@ export const createConfiguration: ( }), new CopyPlugin({ patterns: [ - { from: resolve(rootPath, 'public'), to: resolve(rootPath, 'dist') }, + { + from: resolve(rootPath, 'public'), + to: resolve(rootPath, 'dist'), + }, ], }), ], diff --git a/apps/core/src/bootstrap/plugins/setup.ts b/apps/core/src/bootstrap/plugins/setup.ts index 6da43b396c..381801bfdf 100644 --- a/apps/core/src/bootstrap/plugins/setup.ts +++ b/apps/core/src/bootstrap/plugins/setup.ts @@ -9,44 +9,36 @@ import * as React from 'react'; import * as ReactJSXRuntime from 'react/jsx-runtime'; import * as ReactDom from 'react-dom'; import * as ReactDomClient from 'react-dom/client'; +import * as SWR from 'swr'; -const customRequire = (id: string) => { - if (id === '@toeverything/plugin-infra/atom') { - return Atom; - } - if (id === 'react') { - return React; - } - if (id === 'react/jsx-runtime') { - return ReactJSXRuntime; - } - if (id === 'react-dom') { - return ReactDom; - } - if (id === 'react-dom/client') { - return ReactDomClient; - } - if (id === '@blocksuite/icons') { - return Icons; - } - if (id === '@affine/component') { - return AFFiNEComponent; - } - if (id === '@blocksuite/blocks/std') { - return BlockSuiteBlocksStd; - } - if (id === '@blocksuite/global/utils') { - return BlockSuiteGlobalUtils; - } - if (id === 'jotai') { - return Jotai; - } - if (id === 'jotai/utils') { - return JotaiUtils; - } - throw new Error(`Cannot find module '${id}'`); +const setupImportsMap = () => { + importsMap.set('react', new Map(Object.entries(React))); + importsMap.set('react/jsx-runtime', new Map(Object.entries(ReactJSXRuntime))); + importsMap.set('react-dom', new Map(Object.entries(ReactDom))); + importsMap.set('react-dom/client', new Map(Object.entries(ReactDomClient))); + importsMap.set('@blocksuite/icons', new Map(Object.entries(Icons))); + importsMap.set('@affine/component', new Map(Object.entries(AFFiNEComponent))); + importsMap.set( + '@blocksuite/blocks/std', + new Map(Object.entries(BlockSuiteBlocksStd)) + ); + importsMap.set( + '@blocksuite/global/utils', + new Map(Object.entries(BlockSuiteGlobalUtils)) + ); + importsMap.set('jotai', new Map(Object.entries(Jotai))); + importsMap.set('jotai/utils', new Map(Object.entries(JotaiUtils))); + importsMap.set( + '@toeverything/plugin-infra/atom', + new Map(Object.entries(Atom)) + ); + importsMap.set('swr', new Map(Object.entries(SWR))); }; +const importsMap = new Map>(); +setupImportsMap(); +export { importsMap }; + export const createGlobalThis = () => { return { process: Object.freeze({ @@ -69,8 +61,15 @@ export const createGlobalThis = () => { clearTimeout: function (id: number) { return globalThis.clearTimeout(id); }, - // copilot uses these + + // safe to use for all plugins + Error: globalThis.Error, + TypeError: globalThis.TypeError, + RangeError: globalThis.RangeError, + console: globalThis.console, crypto: globalThis.crypto, + + // copilot uses these CustomEvent: globalThis.CustomEvent, Date: globalThis.Date, Math: globalThis.Math, @@ -80,8 +79,8 @@ export const createGlobalThis = () => { TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder, Request: globalThis.Request, - Error: globalThis.Error, - // bookmark uses these + + // image-preview uses these Blob: globalThis.Blob, ClipboardItem: globalThis.ClipboardItem, @@ -98,9 +97,5 @@ export const createGlobalThis = () => { IDBIndex: globalThis.IDBIndex, IDBCursor: globalThis.IDBCursor, IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent, - - exports: {}, - console: globalThis.console, - require: customRequire, }; }; diff --git a/apps/core/src/bootstrap/register-plugins.ts b/apps/core/src/bootstrap/register-plugins.ts index 0bacbe9f7a..da04b28d64 100644 --- a/apps/core/src/bootstrap/register-plugins.ts +++ b/apps/core/src/bootstrap/register-plugins.ts @@ -20,7 +20,7 @@ import { Provider } from 'jotai/react'; import type { PropsWithChildren } from 'react'; import { createElement } from 'react'; -import { createGlobalThis } from './plugins/setup'; +import { createGlobalThis, importsMap } from './plugins/setup'; if (!process.env.COVERAGE) { lockdown({ @@ -33,6 +33,29 @@ if (!process.env.COVERAGE) { }); } +const imports = ( + newUpdaters: [string, [string, ((val: any) => void)[]][]][] +) => { + for (const [module, moduleUpdaters] of newUpdaters) { + const moduleImports = importsMap.get(module); + if (moduleImports) { + for (const [importName, importUpdaters] of moduleUpdaters) { + const updateImport = (value: any) => { + for (const importUpdater of importUpdaters) { + importUpdater(value); + } + }; + if (moduleImports.has(importName)) { + const val = moduleImports.get(importName); + updateImport(val); + } else { + console.log('import not found', importName, module); + } + } + } + } +}; + const builtinPluginUrl = new Set([ '/plugins/bookmark', '/plugins/copilot', @@ -79,8 +102,7 @@ await Promise.all( if (!release && process.env.NODE_ENV === 'production') { return Promise.resolve(); } - const pluginCompartment = new Compartment(createGlobalThis(), {}); - const pluginGlobalThis = pluginCompartment.globalThis; + const pluginCompartment = new Compartment(createGlobalThis()); const baseURL = url; const entryURL = `${baseURL}/${core}`; rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]); @@ -107,58 +129,69 @@ await Promise.all( ); } const codeText = await res.text(); - pluginCompartment.evaluate(codeText, { - __evadeHtmlCommentTest__: true, - }); - pluginGlobalThis.__INTERNAL__ENTRY = { - register: (part, callback) => { - logger.info(`Registering ${pluginName} to ${part}`); - if (part === 'headerItem') { - rootStore.set(headerItemsAtom, items => ({ - ...items, - [pluginName]: callback as CallbackMap['headerItem'], - })); - } else if (part === 'editor') { - rootStore.set(editorItemsAtom, items => ({ - ...items, - [pluginName]: callback as CallbackMap['editor'], - })); - } else if (part === 'window') { - rootStore.set(windowItemsAtom, items => ({ - ...items, - [pluginName]: callback as CallbackMap['window'], - })); - } else if (part === 'setting') { - rootStore.set(settingItemsAtom, items => ({ - ...items, - [pluginName]: callback as CallbackMap['setting'], - })); - } else if (part === 'formatBar') { - FormatQuickBar.customElements.push((page, getBlockRange) => { - const div = document.createElement('div'); - (callback as CallbackMap['formatBar'])( - div, - page, - getBlockRange - ); - return div; - }); - } else { - throw new Error(`Unknown part: ${part}`); - } - }, - utils: { - PluginProvider, - }, - } satisfies PluginContext; - const dispose = pluginCompartment.evaluate( - 'exports.entry(__INTERNAL__ENTRY)' - ); - if (typeof dispose !== 'function') { - throw new Error('Plugin entry must return a function'); + try { + const entryPoint = pluginCompartment.evaluate(codeText, { + __evadeHtmlCommentTest__: true, + }); + entryPoint({ + imports, + onceVar: { + entry: ( + entryFunction: (context: PluginContext) => () => void + ) => { + const cleanup = entryFunction({ + register: (part, callback) => { + logger.info(`Registering ${pluginName} to ${part}`); + if (part === 'headerItem') { + rootStore.set(headerItemsAtom, items => ({ + ...items, + [pluginName]: callback as CallbackMap['headerItem'], + })); + } else if (part === 'editor') { + rootStore.set(editorItemsAtom, items => ({ + ...items, + [pluginName]: callback as CallbackMap['editor'], + })); + } else if (part === 'window') { + rootStore.set(windowItemsAtom, items => ({ + ...items, + [pluginName]: callback as CallbackMap['window'], + })); + } else if (part === 'setting') { + rootStore.set(settingItemsAtom, items => ({ + ...items, + [pluginName]: callback as CallbackMap['setting'], + })); + } else if (part === 'formatBar') { + FormatQuickBar.customElements.push( + (page, getBlockRange) => { + const div = document.createElement('div'); + (callback as CallbackMap['formatBar'])( + div, + page, + getBlockRange + ); + return div; + } + ); + } else { + throw new Error(`Unknown part: ${part}`); + } + }, + utils: { + PluginProvider, + }, + }); + if (typeof cleanup !== 'function') { + throw new Error('Plugin entry must return a function'); + } + group.add(cleanup); + }, + }, + }); + } catch (e) { + console.error(pluginName, e); } - pluginGlobalThis.__INTERNAL__ENTRY = undefined; - group.add(dispose); }); }) .catch(e => { diff --git a/packages/cli/package.json b/packages/cli/package.json index 890d46fefc..95dc33f965 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@clack/core": "^0.3.2", "@clack/prompts": "^0.6.3", + "@endo/static-module-record": "^0.7.20", "ts-node": "^10.9.1" }, "dependencies": { diff --git a/packages/cli/src/bin/dev-plugin.ts b/packages/cli/src/bin/dev-plugin.ts index bcb4ccc3c9..702dc04435 100644 --- a/packages/cli/src/bin/dev-plugin.ts +++ b/packages/cli/src/bin/dev-plugin.ts @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { parseArgs } from 'node:util'; +import { StaticModuleRecord } from '@endo/static-module-record'; import { packageJsonInputSchema, packageJsonOutputSchema, @@ -52,6 +53,9 @@ const external = [ // store /^jotai/, + // utils + 'swr', + // css /^@vanilla-extract/, @@ -132,7 +136,7 @@ await build({ lib: { entry: coreEntry, fileName: 'index', - formats: ['cjs'], + formats: ['es'], }, rollupOptions: { output: { @@ -152,6 +156,28 @@ await build({ plugins: [ vanillaExtractPlugin(), react(), + { + name: 'parse-bundle', + renderChunk(code, chunk) { + if (chunk.fileName.endsWith('.mjs')) { + const record = new StaticModuleRecord(code, chunk.fileName); + this.emitFile({ + type: 'asset', + fileName: 'analysis.json', + source: JSON.stringify( + { + exports: record.exports, + imports: record.imports, + }, + null, + 2 + ), + }); + return record.__syncModuleProgram__; + } + return code; + }, + }, { name: 'generate-package.json', async generateBundle() { @@ -162,7 +188,7 @@ await build({ affinePlugin: { release: json.affinePlugin.release, entry: { - core: 'index.js', + core: 'index.mjs', }, assets: [...metadata.assets], }, diff --git a/plugins/bookmark/package.json b/plugins/bookmark/package.json index 06c7617cb6..2a3ed99fb1 100644 --- a/plugins/bookmark/package.json +++ b/plugins/bookmark/package.json @@ -1,5 +1,6 @@ { "name": "@affine/bookmark-plugin", + "type": "module", "version": "0.8.0-canary.1", "description": "Bookmark Plugin", "affinePlugin": { diff --git a/plugins/copilot/package.json b/plugins/copilot/package.json index 09cfa4bea8..a1d823d988 100644 --- a/plugins/copilot/package.json +++ b/plugins/copilot/package.json @@ -1,5 +1,6 @@ { "name": "@affine/copilot-plugin", + "type": "module", "private": true, "description": "Copilot plugin", "affinePlugin": { diff --git a/plugins/hello-world/package.json b/plugins/hello-world/package.json index 0ac0a84a5c..32b92c40a6 100644 --- a/plugins/hello-world/package.json +++ b/plugins/hello-world/package.json @@ -1,5 +1,6 @@ { "name": "@affine/hello-world-plugin", + "type": "module", "private": true, "description": "Hello world plugin", "version": "0.8.0-canary.1", diff --git a/plugins/image-preview/package.json b/plugins/image-preview/package.json index 2320a88dda..8e29e57d39 100644 --- a/plugins/image-preview/package.json +++ b/plugins/image-preview/package.json @@ -1,5 +1,6 @@ { "name": "@affine/image-preview-plugin", + "type": "module", "version": "0.8.0-canary.1", "description": "Image preview plugin", "affinePlugin": { diff --git a/yarn.lock b/yarn.lock index 47259dc262..a7fe7ca342 100644 --- a/yarn.lock +++ b/yarn.lock @@ -92,6 +92,7 @@ __metadata: dependencies: "@clack/core": ^0.3.2 "@clack/prompts": ^0.6.3 + "@endo/static-module-record": ^0.7.20 dotenv: ^16.3.1 ts-node: ^10.9.1 peerDependencies: @@ -664,6 +665,17 @@ __metadata: languageName: unknown linkType: soft +"@agoric/babel-generator@npm:^7.17.6": + version: 7.17.6 + resolution: "@agoric/babel-generator@npm:7.17.6" + dependencies: + "@babel/types": ^7.17.0 + jsesc: ^2.5.1 + source-map: ^0.5.0 + checksum: ffc35c0df5089a7a8fbe57526049931e59c67f658ca37f8c0cca9a57fc61855f582f0223494fc270fe24c012be951f3d952eb983238b74185e144e7310f53d3a + languageName: node + linkType: hard + "@alloc/quick-lru@npm:^5.2.0": version: 5.2.0 resolution: "@alloc/quick-lru@npm:5.2.0" @@ -1951,7 +1963,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.0.0, @babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.3, @babel/parser@npm:^7.21.4, @babel/parser@npm:^7.22.5, @babel/parser@npm:^7.22.7": +"@babel/parser@npm:^7.0.0, @babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.17.3, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.3, @babel/parser@npm:^7.21.4, @babel/parser@npm:^7.22.5, @babel/parser@npm:^7.22.7": version: 7.22.7 resolution: "@babel/parser@npm:7.22.7" bin: @@ -3221,7 +3233,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.22.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.7.2": +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.17.3, @babel/traverse@npm:^7.22.6, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.7.2": version: 7.22.8 resolution: "@babel/traverse@npm:7.22.8" dependencies: @@ -3239,7 +3251,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.17.0, @babel/types@npm:^7.18.13, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.22.5 resolution: "@babel/types@npm:7.22.5" dependencies: @@ -4211,6 +4223,19 @@ __metadata: languageName: node linkType: hard +"@endo/static-module-record@npm:^0.7.20": + version: 0.7.20 + resolution: "@endo/static-module-record@npm:0.7.20" + dependencies: + "@agoric/babel-generator": ^7.17.6 + "@babel/parser": ^7.17.3 + "@babel/traverse": ^7.17.3 + "@babel/types": ^7.17.0 + ses: ^0.18.5 + checksum: a60e39aea043f774aef34e14348f972c462e9c8e75688b054abc14b3ea26968bcbbf501c07178bcbdf1c27c44955c6972f53df6a8e59fd75f06c3b5d2564bf8e + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.17.6": version: 0.17.6 resolution: "@esbuild/android-arm64@npm:0.17.6" @@ -28498,7 +28523,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.5.7": +"source-map@npm:^0.5.0, source-map@npm:^0.5.7": version: 0.5.7 resolution: "source-map@npm:0.5.7" checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d