From 8beefd3a1c048c383b9ac8a999a431e2dff4b45a Mon Sep 17 00:00:00 2001 From: 1ncounter <1ncounter.100@gmail.com> Date: Thu, 11 Apr 2024 15:35:40 +0800 Subject: [PATCH] chore: setup playground --- eslint.config.js | 6 +- packages/designer/package.json | 9 ++- .../designer/src/document/document-view.tsx | 2 +- .../designer/src/plugin/plugin-manager.ts | 1 - packages/designer/src/plugin/plugin-types.ts | 4 +- packages/designer/src/project/project.ts | 4 +- packages/designer/src/simulator.ts | 12 ++- packages/editor-core/package.json | 2 + packages/editor-skeleton/package.json | 4 + .../editor-skeleton/src/layouts/top-area.tsx | 7 +- .../editor-skeleton/src/layouts/workbench.tsx | 3 +- packages/engine/src/engine-core.ts | 9 ++- .../inner-plugins/default-panel-registry.tsx | 2 + packages/engine/src/shell/api/common.tsx | 14 ++-- .../src/workspace/layouts/workbench.tsx | 5 +- packages/plugin-command/package.json | 2 + packages/plugin-designer/package.json | 10 ++- packages/plugin-outline-pane/package.json | 7 ++ playground/engine/src/index.ts | 11 ++- .../src/plugins/plugin-logo-sample/index.css | 25 ++++++ .../src/plugins/plugin-logo-sample/index.tsx | 79 +++++++++++++++++++ scripts/build.js | 7 +- scripts/rollup-dts.js | 8 +- vite.base.config.ts | 4 +- 24 files changed, 199 insertions(+), 38 deletions(-) create mode 100644 playground/engine/src/plugins/plugin-logo-sample/index.css create mode 100644 playground/engine/src/plugins/plugin-logo-sample/index.tsx diff --git a/eslint.config.js b/eslint.config.js index 3cfaa3db8..e91d9ff0c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ import reactHooks from 'eslint-plugin-react-hooks'; import globals from 'globals'; export default tseslint.config({ - files: ['packages/*/{src,__tests__}/**/*.{ts?(x),js?(x)}'], + files: ['packages/*/{src,__tests__}/**/*.{ts?(x),js?(x)}', 'scripts/*.js'], ignores: ['**/*.test.ts'], extends: [js.configs.recommended, ...tseslint.configs.recommended], plugins: { @@ -32,13 +32,15 @@ export default tseslint.config({ '@stylistic/max-len': ['error', { code: 100, tabWidth: 2, ignoreStrings: true, ignoreComments: true }], '@stylistic/no-tabs': 'error', '@stylistic/quotes': ['error', 'single'], + '@stylistic/quote-props': ['error', 'as-needed'], '@stylistic/jsx-pascal-case': [2], '@stylistic/jsx-indent': [2, 2, { checkAttributes: true, indentLogicalExpressions: true }], '@stylistic/semi': ['error', 'always'], '@stylistic/eol-last': ['error', 'always'], '@stylistic/jsx-quotes': ['error', 'prefer-double'], - "@typescript-eslint/ban-ts-comment": ["error", { 'ts-expect-error': 'allow-with-description' }], + '@typescript-eslint/ban-ts-comment': ["error", { 'ts-expect-error': 'allow-with-description' }], + '@typescript-eslint/no-explicit-any': 'warn', 'react/jsx-no-undef': 'error', 'react/jsx-uses-vars': 'error', diff --git a/packages/designer/package.json b/packages/designer/package.json index 3bcbde815..0884b46b5 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -12,9 +12,9 @@ "require": "./dist/low-code-designer.cjs", "types": "./dist/index.d.ts" }, - "./dist/": { - "import": "./dist/", - "require": "./dist/" + "./dist/style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" } }, "files": [ @@ -54,6 +54,9 @@ }, "peerDependencies": { "@alifd/next": "^1.27.8", + "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-types": "workspace:*", + "@alilc/lowcode-utils": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/packages/designer/src/document/document-view.tsx b/packages/designer/src/document/document-view.tsx index 184ed80b5..73d8157f5 100644 --- a/packages/designer/src/document/document-view.tsx +++ b/packages/designer/src/document/document-view.tsx @@ -1,7 +1,7 @@ import { Component } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import { DocumentModel, IDocumentModel } from './document-model'; +import { IDocumentModel } from './document-model'; import { BuiltinSimulatorHostView } from '../builtin-simulator'; @observer diff --git a/packages/designer/src/plugin/plugin-manager.ts b/packages/designer/src/plugin/plugin-manager.ts index cd43cf28f..b6f6b690f 100644 --- a/packages/designer/src/plugin/plugin-manager.ts +++ b/packages/designer/src/plugin/plugin-manager.ts @@ -118,7 +118,6 @@ export class LowCodePluginManager implements ILowCodePluginManager { ); const config = newPluginModel(ctx, newOptions); // compat the legacy way to declare pluginName - // @ts-ignore pluginName = pluginName || config.name; invariant(pluginName, 'pluginConfigCreator.pluginName required', config); diff --git a/packages/designer/src/plugin/plugin-types.ts b/packages/designer/src/plugin/plugin-types.ts index cfc38866f..d094d2bff 100644 --- a/packages/designer/src/plugin/plugin-types.ts +++ b/packages/designer/src/plugin/plugin-types.ts @@ -84,7 +84,9 @@ export interface ILowCodePluginManagerCore { pluginOptions?: any, options?: IPublicTypePluginRegisterOptions, ): Promise; - init(pluginPreference?: Map>): Promise; + init( + pluginPreference?: Map> + ): Promise; get(pluginName: string): ILowCodePluginRuntime | undefined; getAll(): ILowCodePluginRuntime[]; has(pluginName: string): boolean; diff --git a/packages/designer/src/project/project.ts b/packages/designer/src/project/project.ts index b70587fd7..be605bf95 100644 --- a/packages/designer/src/project/project.ts +++ b/packages/designer/src/project/project.ts @@ -319,7 +319,9 @@ export class Project implements IProject { return doc.open(); } if (typeof doc === 'string' || typeof doc === 'number') { - const got = this.documents.find((item) => item.fileName === String(doc) || String(item.id) === String(doc)); + const got = this.documents.find( + (item) => item.fileName === String(doc) || String(item.id) === String(doc) + ); if (got) { return got.open(); } diff --git a/packages/designer/src/simulator.ts b/packages/designer/src/simulator.ts index 3a63a685b..d11f9722b 100644 --- a/packages/designer/src/simulator.ts +++ b/packages/designer/src/simulator.ts @@ -164,13 +164,19 @@ export interface ISimulatorHost

extends IPublicModelSensor { */ getComponentContext(node: INode): object | null; - getClosestNodeInstance(from: IPublicTypeComponentInstance, specId?: string): IPublicTypeNodeInstance | null; + getClosestNodeInstance( + from: IPublicTypeComponentInstance, specId?: string + ): IPublicTypeNodeInstance | null; computeRect(node: INode): DOMRect | null; - computeComponentInstanceRect(instance: IPublicTypeComponentInstance, selector?: string): DOMRect | null; + computeComponentInstanceRect( + instance: IPublicTypeComponentInstance, selector?: string + ): DOMRect | null; - findDOMNodes(instance: IPublicTypeComponentInstance, selector?: string): Array | null; + findDOMNodes( + instance: IPublicTypeComponentInstance, selector?: string + ): Array | null; getDropContainer(e: ILocateEvent): DropContainer | null; diff --git a/packages/editor-core/package.json b/packages/editor-core/package.json index 3020b77e2..dc979e387 100644 --- a/packages/editor-core/package.json +++ b/packages/editor-core/package.json @@ -57,6 +57,8 @@ }, "peerDependencies": { "@alifd/next": "^1.27.8", + "@alilc/lowcode-types": "workspace:*", + "@alilc/lowcode-utils": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/packages/editor-skeleton/package.json b/packages/editor-skeleton/package.json index aecb60e2d..b9f165ade 100644 --- a/packages/editor-skeleton/package.json +++ b/packages/editor-skeleton/package.json @@ -53,6 +53,10 @@ }, "peerDependencies": { "@alifd/next": "^1.27.8", + "@alilc/lowcode-designer": "workspace:*", + "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-types": "workspace:*", + "@alilc/lowcode-utils": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/packages/editor-skeleton/src/layouts/top-area.tsx b/packages/editor-skeleton/src/layouts/top-area.tsx index f6b84b3e6..5204899b4 100644 --- a/packages/editor-skeleton/src/layouts/top-area.tsx +++ b/packages/editor-skeleton/src/layouts/top-area.tsx @@ -4,7 +4,9 @@ import { observer } from '@alilc/lowcode-editor-core'; import { Area } from '../area'; @observer -export default class TopArea extends Component<{ area: Area; itemClassName?: string; className?: string }> { +export default class TopArea extends Component< + { area: Area; itemClassName?: string; className?: string } +> { render() { const { area, itemClassName, className } = this.props; if (area.isEmpty()) { @@ -13,8 +15,7 @@ export default class TopArea extends Component<{ area: Area; itemClassName?: str return (

+ })}>
); diff --git a/packages/editor-skeleton/src/layouts/workbench.tsx b/packages/editor-skeleton/src/layouts/workbench.tsx index 1e412ed67..c3a932293 100644 --- a/packages/editor-skeleton/src/layouts/workbench.tsx +++ b/packages/editor-skeleton/src/layouts/workbench.tsx @@ -10,10 +10,11 @@ import Toolbar from './toolbar'; import MainArea from './main-area'; import BottomArea from './bottom-area'; import RightArea from './right-area'; -import './workbench.less'; import { SkeletonContext } from '../context'; import { EditorConfig, PluginClassSet } from '@alilc/lowcode-types'; +import './workbench.less'; + @observer export class Workbench extends Component<{ skeleton: ISkeleton; diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts index da74b628f..159d49730 100644 --- a/packages/engine/src/engine-core.ts +++ b/packages/engine/src/engine-core.ts @@ -66,7 +66,9 @@ import { CommandPlugin } from '@alilc/lowcode-plugin-command'; import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane'; import { version } from '../package.json'; +import '@alilc/lowcode-plugin-outline-pane/dist/style.css'; import '@alilc/lowcode-editor-skeleton/dist/style.css'; +import '@alilc/lowcode-designer/dist/style.css'; export * from './modules/skeleton-types'; export * from './modules/designer-types'; @@ -115,10 +117,10 @@ globalContext.register(innerWorkspace, 'workspace'); const engineContext: Partial = {}; const innerSkeleton = new InnerSkeleton(editor); -editor.set('skeleton' as any, innerSkeleton); +editor.set('skeleton', innerSkeleton); const designer = new Designer({ editor, shellModelFactory }); -editor.set('designer' as any, designer); +editor.set('designer', designer); const { project: innerProject } = designer; @@ -137,7 +139,7 @@ const material = new Material(editor); const commonUI = new CommonUI(editor); editor.set('project', project); -editor.set('setters' as any, setters); +editor.set('setters', setters); editor.set('material', material); editor.set('innerHotkey', innerHotkey); @@ -290,6 +292,7 @@ export async function init( return; } + await pluginPromise; await plugins.init(pluginPreference as any); if (!root) { diff --git a/packages/engine/src/inner-plugins/default-panel-registry.tsx b/packages/engine/src/inner-plugins/default-panel-registry.tsx index b5f538d44..f064526c0 100644 --- a/packages/engine/src/inner-plugins/default-panel-registry.tsx +++ b/packages/engine/src/inner-plugins/default-panel-registry.tsx @@ -2,6 +2,8 @@ import { IPublicModelPluginContext } from '@alilc/lowcode-types'; import { SettingsPrimaryPane } from '@alilc/lowcode-editor-skeleton'; import DesignerPlugin from '@alilc/lowcode-plugin-designer'; +import '@alilc/lowcode-plugin-designer/dist/style.css'; + // 注册默认的面板 export const defaultPanelRegistry = (editor: any) => { const fun = (ctx: IPublicModelPluginContext) => { diff --git a/packages/engine/src/shell/api/common.tsx b/packages/engine/src/shell/api/common.tsx index 729ef0a63..46e546a4b 100644 --- a/packages/engine/src/shell/api/common.tsx +++ b/packages/engine/src/shell/api/common.tsx @@ -417,9 +417,9 @@ class EditorCabin implements IPublicApiCommonEditorCabin { } export class Common implements IPublicApiCommon { - private readonly __designerCabin: any; - private readonly __skeletonCabin: any; - private readonly __editorCabin: any; + private readonly __designerCabin: DesignerCabin; + private readonly __skeletonCabin: SkeletonCabin; + private readonly __editorCabin: EditorCabin; private readonly __utils: Utils; constructor(editor: Editor, skeleton: InnerSkeleton) { @@ -438,7 +438,7 @@ export class Common implements IPublicApiCommon { * this load of crap will be removed in some future versions, don`t use it. * @deprecated */ - get editorCabin(): any { + get editorCabin(): EditorCabin { return this.__editorCabin; } @@ -447,11 +447,11 @@ export class Common implements IPublicApiCommon { * this load of crap will be removed in some future versions, don`t use it. * @deprecated use canvas api instead */ - get designerCabin(): any { + get designerCabin(): DesignerCabin { return this.__designerCabin; } - get skeletonCabin(): any { + get skeletonCabin(): SkeletonCabin { return this.__skeletonCabin; } @@ -460,7 +460,7 @@ export class Common implements IPublicApiCommon { * this load of crap will be removed in some future versions, don`t use it. * @deprecated use { TransformStage } from '@alilc/lowcode-types' instead */ - get objects(): any { + get objects() { return { TransformStage: InnerTransitionStage, }; diff --git a/packages/engine/src/workspace/layouts/workbench.tsx b/packages/engine/src/workspace/layouts/workbench.tsx index 69583b524..4349a86d1 100644 --- a/packages/engine/src/workspace/layouts/workbench.tsx +++ b/packages/engine/src/workspace/layouts/workbench.tsx @@ -67,14 +67,15 @@ export class Workbench extends Component<{ } { - !workspace.windows.length && WorkspaceEmptyComponent ? : null + workspace.windows.length === 0 && WorkspaceEmptyComponent + ? + : null } - {/* */} diff --git a/packages/plugin-command/package.json b/packages/plugin-command/package.json index 4b38ba7f3..adc092037 100644 --- a/packages/plugin-command/package.json +++ b/packages/plugin-command/package.json @@ -6,6 +6,7 @@ "homepage": "https://github.com/alibaba/lowcode-engine#readme", "license": "ISC", "type": "module", + "private": true, "main": "dist/low-code-plugin-command.cjs", "module": "dist/low-code-plugin-command.js", "types": "dist/index.d.ts", @@ -39,6 +40,7 @@ "react-dom": "^18.2.0" }, "peerDependencies": { + "@alilc/lowcode-utils": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index 6055b5f5a..14d82f02a 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -2,6 +2,7 @@ "name": "@alilc/lowcode-plugin-designer", "version": "2.0.0-beta.0", "description": "alibaba lowcode editor designer plugin", + "private": true, "type": "module", "main": "dist/low-code-plugin-designer.cjs", "module": "dist/low-code-plugin-designer.js", @@ -11,6 +12,10 @@ "import": "./dist/low-code-plugin-designer.js", "require": "./dist/low-code-plugin-designer.cjs", "types": "./dist/index.d.ts" + }, + "./dist/": { + "import": "./dist/", + "require": "./dist/" } }, "files": [ @@ -41,7 +46,10 @@ }, "peerDependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "@alilc/lowcode-designer": "workspace:*", + "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-utils": "workspace:*" }, "publishConfig": { "access": "public", diff --git a/packages/plugin-outline-pane/package.json b/packages/plugin-outline-pane/package.json index 0c403a04e..a63ed195c 100644 --- a/packages/plugin-outline-pane/package.json +++ b/packages/plugin-outline-pane/package.json @@ -2,6 +2,7 @@ "name": "@alilc/lowcode-plugin-outline-pane", "version": "1.3.2", "description": "Outline pane for Ali lowCode engine", + "private": true, "type": "module", "main": "dist/low-code-plugin-outline-pane.cjs", "module": "dist/low-code-plugin-outline-pane.js", @@ -11,6 +12,10 @@ "import": "./dist/low-code-plugin-outline-pane.js", "require": "./dist/low-code-plugin-outline-pane.cjs", "types": "./dist/index.d.ts" + }, + "./dist/": { + "import": "./dist/", + "require": "./dist/" } }, "files": [ @@ -39,6 +44,8 @@ }, "peerDependencies": { "@alifd/next": "^1.27.8", + "@alilc/lowcode-types": "workspace:*", + "@alilc/lowcode-utils": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/playground/engine/src/index.ts b/playground/engine/src/index.ts index e65be4fab..270c78e10 100644 --- a/playground/engine/src/index.ts +++ b/playground/engine/src/index.ts @@ -1,10 +1,15 @@ -import { init, plugins } from '@alilc/lowcode-engine'; +import * as engine from '@alilc/lowcode-engine'; import EditorInitPlugin from './plugins/plugin-editor-init'; +import LogoSamplePlugin from './plugins/plugin-logo-sample'; import '@alilc/lowcode-engine/dist/style.css'; import './index.css'; +(window as any).AliLowCodeEngine = engine; + async function run() { + const { plugins, init, project, skeleton, config } = engine; + await plugins.register(EditorInitPlugin, { scenarioName: 'general', displayName: '综合场景', @@ -25,6 +30,7 @@ async function run() { ], }, }); + await plugins.register(LogoSamplePlugin); await init(document.getElementById('app')!, { locale: 'zh-CN', @@ -38,7 +44,10 @@ async function run() { 'https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@1.1.1/dist/js/react-simulator-renderer.js' ], enableContextMenu: true, + enableWorkspaceMode: false }); + + console.log(project, skeleton, plugins, config) } run() diff --git a/playground/engine/src/plugins/plugin-logo-sample/index.css b/playground/engine/src/plugins/plugin-logo-sample/index.css new file mode 100644 index 000000000..d8b63f65a --- /dev/null +++ b/playground/engine/src/plugins/plugin-logo-sample/index.css @@ -0,0 +1,25 @@ +.lowcode-plugin-logo { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-end; + width: 300px; + .logo { + display: block; + width: 139px; + height: 26px; + cursor: pointer; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + } + .scenario-name { + display: block; + margin-left: 20px; + margin-right: 5px; + font-size: 15px; + } + .info-dropdown { + display: block; + } +} diff --git a/playground/engine/src/plugins/plugin-logo-sample/index.tsx b/playground/engine/src/plugins/plugin-logo-sample/index.tsx new file mode 100644 index 000000000..bf2c058bf --- /dev/null +++ b/playground/engine/src/plugins/plugin-logo-sample/index.tsx @@ -0,0 +1,79 @@ +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; +import { Dropdown, Menu } from '@alifd/next'; + +import './index.css'; + +export interface IProps { + logo?: string; + href?: string; + scenarioInfo?: any; + scenarioDisplayName?: string; +} + +const Logo: React.FC = (props) => { + const { scenarioDisplayName, scenarioInfo } = props; + const urls = scenarioInfo?.urls || []; + + return ( + + ); +}; + +// 示例 Logo widget +const LogoSamplePlugin = (ctx: IPublicModelPluginContext) => { + return { + async init() { + const { skeleton, config } = ctx; + const scenarioDisplayName = config.get('scenarioDisplayName'); + const scenarioInfo = config.get('scenarioInfo'); + // 注册 logo widget + skeleton.add({ + area: 'topArea', + type: 'Widget', + name: 'logo', + content: , + contentProps: { + logo: 'https://img.alicdn.com/imgextra/i4/O1CN013w2bmQ25WAIha4Hx9_!!6000000007533-55-tps-137-26.svg', + href: 'https://lowcode-engine.cn', + }, + props: { + align: 'left', + }, + }); + }, + }; +} + +LogoSamplePlugin.pluginName = 'LogoSamplePlugin'; +LogoSamplePlugin.meta = { + dependencies: ['EditorInitPlugin'], +}; + +export default LogoSamplePlugin; diff --git a/scripts/build.js b/scripts/build.js index 2fe36693e..b04190a40 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,17 +1,17 @@ import { argv, cwd } from 'node:process'; import minimist from 'minimist'; import { execa } from 'execa'; -import { findWorkspacePackages } from '@pnpm/workspace.find-packages' +import { findWorkspacePackages } from '@pnpm/workspace.find-packages'; const args = minimist(argv.slice(2)); const targets = args['_']; const formatArgs = args['formats']; const prod = args['prod'] || args['p']; -const buildTypes = args['types'] || args['t'] +const buildTypes = args['types'] || args['t']; async function run() { const packages = await findWorkspacePackages(cwd()); - const targetPackageName = `@alilc/lowcode-${targets[0]}` + const targetPackageName = `@alilc/lowcode-${targets[0]}`; const finalName = packages .filter((item) => item.manifest.name === targetPackageName) .map(item => item.manifest.name); @@ -19,6 +19,7 @@ async function run() { await execa('pnpm', ['--filter', finalName[0], 'build:target'], { stdio: 'inherit', env: { + PROD: prod, FORMATS: formatArgs ? formatArgs : !prod ? 'es' : undefined, }, }); diff --git a/scripts/rollup-dts.js b/scripts/rollup-dts.js index 526937f8f..eb0ab5f4c 100644 --- a/scripts/rollup-dts.js +++ b/scripts/rollup-dts.js @@ -1,7 +1,7 @@ import { join } from 'node:path'; import { existsSync, readdirSync } from 'node:fs'; -import { env, exit } from 'node:process' -import console from 'node:console' +import { env, exit } from 'node:process'; +import console from 'node:console'; import { Extractor, ExtractorConfig } from '@microsoft/api-extractor'; import { rimraf } from 'rimraf'; @@ -30,13 +30,13 @@ async function run() { }); if (extractorResult.succeeded) { - console.log(`🚀类型声明文件生成成功!!!`); + console.log('🚀类型声明文件生成成功!!!'); await rimraf(join(libPath, 'temp')); } else { console.error( '🚨类型声明文件生成失败:' + - +`\n\t${extractorResult.errorCount} errors``\n\tand ${extractorResult.warningCount} warnings`, + +`\n\t${extractorResult.errorCount} errors``\n\tand ${extractorResult.warningCount} warnings`, ); exit(1); } diff --git a/vite.base.config.ts b/vite.base.config.ts index bba4c8496..77ad16e74 100644 --- a/vite.base.config.ts +++ b/vite.base.config.ts @@ -12,6 +12,7 @@ interface Options { } const resolvePath = (path: string) => resolve(cwd(), path) +const isProduction = !!env['PROD'] export default async ({ name, entry = 'src/index.ts', defaultFormats = ['es'], externalDeps = true }: Options) => { const formats = (env['FORMATS']?.split(',') ?? defaultFormats) as LibraryFormats[]; @@ -31,7 +32,8 @@ export default async ({ name, entry = 'src/index.ts', defaultFormats = ['es'], e fileName: camelCaseToKebabCase(name), formats, }, - minify: false, + minify: isProduction, + sourcemap: isProduction ? false : 'inline', rollupOptions: { external: externals }