test: add test case for plugin bootstrap (#3529)

This commit is contained in:
Alex Yang 2023-08-02 18:48:35 -07:00 committed by GitHub
parent dcd070b3e7
commit d3c719d89a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 110 additions and 44 deletions

View File

@ -1,7 +1,6 @@
import { join, resolve } from 'node:path'; import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
import HTMLPlugin from 'html-webpack-plugin';
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server'; import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
import { PerfseePlugin } from '@perfsee/webpack'; import { PerfseePlugin } from '@perfsee/webpack';
import { sentryWebpackPlugin } from '@sentry/webpack-plugin'; import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
@ -253,14 +252,6 @@ export const createConfiguration: (
ignoreOrder: true, ignoreOrder: true,
}), }),
]), ]),
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['index', 'plugin', 'polyfill-ses'],
filename: 'index.html',
}),
new VanillaExtractPlugin(), new VanillaExtractPlugin(),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': JSON.stringify({}), 'process.env': JSON.stringify({}),

View File

@ -1,8 +1,9 @@
import { createConfiguration, rootPath } from './config.js'; import { createConfiguration, rootPath } from './config.js';
import { merge } from 'webpack-merge'; import { merge } from 'webpack-merge';
import { resolve } from 'node:path'; import { join, resolve } from 'node:path';
import type { BuildFlags } from '@affine/cli/config'; import type { BuildFlags } from '@affine/cli/config';
import { getRuntimeConfig } from './runtime-config.js'; import { getRuntimeConfig } from './runtime-config.js';
import HTMLPlugin from 'html-webpack-plugin';
export default async function (cli_env: any, _: any) { export default async function (cli_env: any, _: any) {
const flags: BuildFlags = JSON.parse( const flags: BuildFlags = JSON.parse(
@ -28,6 +29,29 @@ export default async function (cli_env: any, _: any) {
dependOn: ['polyfill-ses', 'plugin'], dependOn: ['polyfill-ses', 'plugin'],
import: resolve(rootPath, 'src/index.tsx'), import: resolve(rootPath, 'src/index.tsx'),
}, },
'_plugin/index.test': {
asyncChunks: false,
dependOn: ['polyfill-ses', 'plugin'],
import: resolve(rootPath, 'src/_plugin/index.test.tsx'),
},
}, },
plugins: [
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['index', 'plugin', 'polyfill-ses'],
filename: 'index.html',
}),
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['_plugin/index.test', 'plugin', 'polyfill-ses'],
filename: '_plugin/index.html',
}),
],
}); });
} }

View File

@ -0,0 +1,42 @@
import { assertExists } from '@blocksuite/global/utils';
import {
registeredPluginAtom,
rootStore,
} from '@toeverything/plugin-infra/atom';
import { use } from 'foxact/use';
import { useAtomValue } from 'jotai';
import { Provider } from 'jotai/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { _pluginNestedImportsMap } from '../bootstrap/plugins/setup';
import { pluginRegisterPromise } from '../bootstrap/register-plugins';
const root = document.getElementById('app');
assertExists(root);
const App = () => {
use(pluginRegisterPromise);
const plugins = useAtomValue(registeredPluginAtom);
_pluginNestedImportsMap.forEach(value => {
const exports = value.get('index.js');
assertExists(exports);
assertExists(exports?.get('entry'));
});
return (
<div>
<div data-plugins-load-status="success">Successfully loaded plugins:</div>
{plugins.map(plugin => {
return <div key={plugin}>{plugin}</div>;
})}
</div>
);
};
createRoot(root).render(
<StrictMode>
<Provider store={rootStore}>
<App />
</Provider>
</StrictMode>
);

View File

@ -37,43 +37,46 @@ const permissionLogger = new DebugLogger('plugins:permission');
const importLogger = new DebugLogger('plugins:import'); const importLogger = new DebugLogger('plugins:import');
const setupRootImportsMap = () => { const setupRootImportsMap = () => {
rootImportsMap.set('react', new Map(Object.entries(React))); _rootImportsMap.set('react', new Map(Object.entries(React)));
rootImportsMap.set( _rootImportsMap.set(
'react/jsx-runtime', 'react/jsx-runtime',
new Map(Object.entries(ReactJSXRuntime)) new Map(Object.entries(ReactJSXRuntime))
); );
rootImportsMap.set('react-dom', new Map(Object.entries(ReactDom))); _rootImportsMap.set('react-dom', new Map(Object.entries(ReactDom)));
rootImportsMap.set( _rootImportsMap.set(
'react-dom/client', 'react-dom/client',
new Map(Object.entries(ReactDomClient)) new Map(Object.entries(ReactDomClient))
); );
rootImportsMap.set('@blocksuite/icons', new Map(Object.entries(Icons))); _rootImportsMap.set('@blocksuite/icons', new Map(Object.entries(Icons)));
rootImportsMap.set( _rootImportsMap.set(
'@affine/component', '@affine/component',
new Map(Object.entries(AFFiNEComponent)) new Map(Object.entries(AFFiNEComponent))
); );
rootImportsMap.set( _rootImportsMap.set(
'@blocksuite/blocks/std', '@blocksuite/blocks/std',
new Map(Object.entries(BlockSuiteBlocksStd)) new Map(Object.entries(BlockSuiteBlocksStd))
); );
rootImportsMap.set( _rootImportsMap.set(
'@blocksuite/global/utils', '@blocksuite/global/utils',
new Map(Object.entries(BlockSuiteGlobalUtils)) new Map(Object.entries(BlockSuiteGlobalUtils))
); );
rootImportsMap.set('jotai', new Map(Object.entries(Jotai))); _rootImportsMap.set('jotai', new Map(Object.entries(Jotai)));
rootImportsMap.set('jotai/utils', new Map(Object.entries(JotaiUtils))); _rootImportsMap.set('jotai/utils', new Map(Object.entries(JotaiUtils)));
rootImportsMap.set( _rootImportsMap.set(
'@toeverything/plugin-infra/atom', '@toeverything/plugin-infra/atom',
new Map(Object.entries(Atom)) new Map(Object.entries(Atom))
); );
rootImportsMap.set('swr', new Map(Object.entries(SWR))); _rootImportsMap.set('swr', new Map(Object.entries(SWR)));
}; };
// module -> importName -> updater[] // module -> importName -> updater[]
const rootImportsMap = new Map<string, Map<string, any>>(); export const _rootImportsMap = new Map<string, Map<string, any>>();
setupRootImportsMap(); setupRootImportsMap();
// pluginName -> module -> importName -> updater[] // pluginName -> module -> importName -> updater[]
const pluginNestedImportsMap = new Map<string, Map<string, Map<string, any>>>(); export const _pluginNestedImportsMap = new Map<
string,
Map<string, Map<string, any>>
>();
const pluginImportsFunctionMap = new Map<string, (imports: any) => void>(); const pluginImportsFunctionMap = new Map<string, (imports: any) => void>();
export const createImports = (pluginName: string) => { export const createImports = (pluginName: string) => {
@ -85,12 +88,12 @@ export const createImports = (pluginName: string) => {
newUpdaters: [string, [string, ((val: any) => void)[]][]][] newUpdaters: [string, [string, ((val: any) => void)[]][]][]
) => { ) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = pluginNestedImportsMap.get(pluginName)!; const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
console.log('currentImportMap', pluginName, currentImportMap); importLogger.debug('currentImportMap', pluginName, currentImportMap);
for (const [module, moduleUpdaters] of newUpdaters) { for (const [module, moduleUpdaters] of newUpdaters) {
console.log('imports module', module, moduleUpdaters); importLogger.debug('imports module', module, moduleUpdaters);
let moduleImports = rootImportsMap.get(module); let moduleImports = _rootImportsMap.get(module);
if (!moduleImports) { if (!moduleImports) {
moduleImports = currentImportMap.get(module); moduleImports = currentImportMap.get(module);
} }
@ -107,11 +110,11 @@ export const createImports = (pluginName: string) => {
} }
} }
} else { } else {
console.log( console.error(
'cannot find module in plugin import map', 'cannot find module in plugin import map',
module, module,
currentImportMap, currentImportMap,
pluginNestedImportsMap _pluginNestedImportsMap
); );
} }
} }
@ -299,13 +302,13 @@ export const setupPluginCode = async (
pluginName: string, pluginName: string,
filename: string filename: string
) => { ) => {
if (!pluginNestedImportsMap.has(pluginName)) { if (!_pluginNestedImportsMap.has(pluginName)) {
pluginNestedImportsMap.set(pluginName, new Map()); _pluginNestedImportsMap.set(pluginName, new Map());
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = pluginNestedImportsMap.get(pluginName)!; const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
const isMissingPackage = (name: string) => const isMissingPackage = (name: string) =>
rootImportsMap.has(name) && !currentImportMap.has(name); _rootImportsMap.has(name) && !currentImportMap.has(name);
const bundleAnalysis = await fetch(`${baseUrl}/${filename}.json`).then(res => const bundleAnalysis = await fetch(`${baseUrl}/${filename}.json`).then(res =>
res.json() res.json()
@ -321,7 +324,7 @@ export const setupPluginCode = async (
if (isMissingPackage(name)) { if (isMissingPackage(name)) {
return Promise.resolve(); return Promise.resolve();
} else { } else {
console.log('missing package', name); importLogger.debug('missing package', name);
return setupPluginCode(baseUrl, pluginName, name); return setupPluginCode(baseUrl, pluginName, name);
} }
}) })
@ -329,7 +332,7 @@ export const setupPluginCode = async (
const code = await fetch(`${baseUrl}/${filename.replace(/^\.\//, '')}`).then( const code = await fetch(`${baseUrl}/${filename.replace(/^\.\//, '')}`).then(
res => res.text() res => res.text()
); );
console.log('evaluating', filename); importLogger.debug('evaluating', filename);
const moduleCompartment = new Compartment( const moduleCompartment = new Compartment(
createOrGetGlobalThis( createOrGetGlobalThis(
pluginName, pluginName,
@ -358,7 +361,6 @@ export const setupPluginCode = async (
onceVar: setVarProxy, onceVar: setVarProxy,
}); });
console.log('module exports alias', moduleExports);
for (const [newExport, [originalExport]] of Object.entries(moduleExports)) { for (const [newExport, [originalExport]] of Object.entries(moduleExports)) {
if (newExport === originalExport) continue; if (newExport === originalExport) continue;
const value = moduleExportsMap.get(originalExport); const value = moduleExportsMap.get(originalExport);
@ -366,7 +368,6 @@ export const setupPluginCode = async (
moduleExportsMap.delete(originalExport); moduleExportsMap.delete(originalExport);
} }
console.log('module re-exports', moduleReexports);
for (const [name, reexports] of Object.entries(moduleReexports)) { for (const [name, reexports] of Object.entries(moduleReexports)) {
const targetExports = currentImportMap.get(filename); const targetExports = currentImportMap.get(filename);
const moduleExports = currentImportMap.get(name); const moduleExports = currentImportMap.get(name);
@ -374,7 +375,6 @@ export const setupPluginCode = async (
assertExists(moduleExports); assertExists(moduleExports);
for (const [exportedName, localName] of reexports) { for (const [exportedName, localName] of reexports) {
const exportedValue: any = moduleExports.get(exportedName); const exportedValue: any = moduleExports.get(exportedName);
console.log('re-export', name, localName, exportedName, exportedValue);
assertExists(exportedValue); assertExists(exportedValue);
targetExports.set(localName, exportedValue); targetExports.set(localName, exportedValue);
} }
@ -395,7 +395,7 @@ const entryLogger = new DebugLogger('plugin:entry');
export const evaluatePluginEntry = (pluginName: string) => { export const evaluatePluginEntry = (pluginName: string) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = pluginNestedImportsMap.get(pluginName)!; const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
const pluginExports = currentImportMap.get('index.js'); const pluginExports = currentImportMap.get('index.js');
assertExists(pluginExports); assertExists(pluginExports);
const entryFunction = pluginExports.get('entry'); const entryFunction = pluginExports.get('entry');

View File

@ -22,7 +22,7 @@ declare global {
globalThis.__pluginPackageJson__ = []; globalThis.__pluginPackageJson__ = [];
Promise.all( export const pluginRegisterPromise = Promise.all(
[...builtinPluginUrl].map(url => { [...builtinPluginUrl].map(url => {
return fetch(`${url}/package.json`) return fetch(`${url}/package.json`)
.then(async res => { .then(async res => {

View File

@ -1,8 +1,13 @@
import { test } from '@affine-test/kit/playwright'; import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page'; import { openHomePage, openPluginPage } from '@affine-test/kit/utils/load-page';
import { waitEditorLoad } from '@affine-test/kit/utils/page-logic'; import { waitEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
test('plugin map should valid', async ({ page }) => {
await openPluginPage(page);
await page.waitForSelector('[data-plugins-load-status="success"]');
});
test('plugin should exist', async ({ page }) => { test('plugin should exist', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitEditorLoad(page); await waitEditorLoad(page);

View File

@ -5,3 +5,7 @@ export const webUrl = 'http://localhost:8080';
export async function openHomePage(page: Page) { export async function openHomePage(page: Page) {
await page.goto(webUrl); await page.goto(webUrl);
} }
export async function openPluginPage(page: Page) {
await page.goto(`${webUrl}/_plugin/index.html`);
}

View File

@ -26,8 +26,8 @@ export default defineConfig({
include: [ include: [
resolve(rootDir, 'packages/**/*.spec.ts'), resolve(rootDir, 'packages/**/*.spec.ts'),
resolve(rootDir, 'packages/**/*.spec.tsx'), resolve(rootDir, 'packages/**/*.spec.tsx'),
resolve(rootDir, 'apps/web/**/*.spec.ts'), resolve(rootDir, 'apps/core/**/*.spec.ts'),
resolve(rootDir, 'apps/web/**/*.spec.tsx'), resolve(rootDir, 'apps/core/**/*.spec.tsx'),
resolve(rootDir, 'tests/unit/**/*.spec.ts'), resolve(rootDir, 'tests/unit/**/*.spec.ts'),
resolve(rootDir, 'tests/unit/**/*.spec.tsx'), resolve(rootDir, 'tests/unit/**/*.spec.tsx'),
], ],