diff --git a/packages/gui/src/components/loader/loader.css b/packages/gui/src/components/loader/loader.css index 8a6e47bc1..642ca3e31 100644 --- a/packages/gui/src/components/loader/loader.css +++ b/packages/gui/src/components/loader/loader.css @@ -14,6 +14,18 @@ color: white; } + +.scoped-loader { + z-index: calc(var(--clipcc-z-index-loader) - 1); + border-radius: 0.5rem; +} + +.scoped-loader .title { + font-size: 1.5rem; + margin: 0; +} + + .fullscreen { /* Break out of the layout using position: fixed to cover the whole screen */ position: fixed; diff --git a/packages/gui/src/components/loader/scoped-loader.tsx b/packages/gui/src/components/loader/scoped-loader.tsx new file mode 100644 index 000000000..2ea645729 --- /dev/null +++ b/packages/gui/src/components/loader/scoped-loader.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './loader.css'; + +import topBlock from './top-block.svg'; +import middleBlock from './middle-block.svg'; +import bottomBlock from './bottom-block.svg'; + +export interface ScopedLoaderProps { + /** The text to display while loading */ + text?: string; +} + +export default function ScopedLoaderComponent (props: ScopedLoaderProps) { + return ( +
+
+
+ + + +
+ {props.text ? ( +
+ {props.text} +
+ ) : null} +
+
+ ); +} diff --git a/packages/gui/src/containers/blocks.jsx b/packages/gui/src/containers/blocks.jsx index f82f9a682..8ea288e08 100644 --- a/packages/gui/src/containers/blocks.jsx +++ b/packages/gui/src/containers/blocks.jsx @@ -4,6 +4,7 @@ import defaultsDeep from 'lodash.defaultsdeep'; import makeToolbox from '../lib/make-toolbox'; import PropTypes from 'prop-types'; import React from 'react'; +import {injectBlock} from '../lib/blocks-loader-hoc.tsx'; import VMScratchBlocks, {setRecordSoundCallback} from '../lib/blocks'; import VM from 'clipcc-vm'; @@ -51,7 +52,7 @@ const DroppableBlocks = DropAreaHOC([ class Blocks extends React.Component { constructor (props) { super(props); - this.ScratchBlocks = VMScratchBlocks(props.vm); + this.ScratchBlocks = VMScratchBlocks(props.vm, props.blocks); bindAll(this, [ 'attachVM', 'checkoutWsByProccode', @@ -403,7 +404,8 @@ class Blocks extends React.Component { targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '', this.props.hideNonVanillaBlocks ); - } catch { + } catch (e) { + console.error(`Error making toolbox:`, e); return null; } } @@ -660,6 +662,7 @@ class Blocks extends React.Component { Blocks.propTypes = { anyModalVisible: PropTypes.bool, + blocks: PropTypes.object, // eslint-disable-line react/forbid-prop-types hideNonVanillaBlocks: PropTypes.bool.isRequired, canUseCloud: PropTypes.bool, customProceduresVisible: PropTypes.bool, @@ -776,5 +779,5 @@ export default errorBoundaryHOC('Blocks')( connect( mapStateToProps, mapDispatchToProps - )(Blocks) + )(injectBlock(Blocks)) ); diff --git a/packages/gui/src/containers/custom-procedures.jsx b/packages/gui/src/containers/custom-procedures.jsx index 730656a9f..763f487f4 100644 --- a/packages/gui/src/containers/custom-procedures.jsx +++ b/packages/gui/src/containers/custom-procedures.jsx @@ -3,8 +3,8 @@ import defaultsDeep from 'lodash.defaultsdeep'; import PropTypes from 'prop-types'; import React from 'react'; import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx'; -import * as ScratchBlocks from 'clipcc-block'; import {connect} from 'react-redux'; +import {injectBlock} from '../lib/blocks-loader-hoc.tsx'; class CustomProcedures extends React.Component { constructor (props) { @@ -39,6 +39,7 @@ class CustomProcedures extends React.Component { {rtl: this.props.isRtl} ); + const {blocks: ScratchBlocks} = this.props; this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig); // Create the procedure declaration block for editing the mutation. @@ -176,6 +177,8 @@ class CustomProcedures extends React.Component { } CustomProcedures.propTypes = { + // eslint-disable-next-line react/forbid-prop-types + blocks: PropTypes.object, isRtl: PropTypes.bool, state: PropTypes.shape({ proccode: PropTypes.string, @@ -200,6 +203,7 @@ CustomProcedures.propTypes = { }) }; +/** @import * as ScratchBlocks from 'clipcc-block' */ /** @type {ScratchBlocks.BlocklyOptions} */ CustomProcedures.defaultOptions = { zoom: { @@ -224,6 +228,6 @@ const mapStateToProps = state => ({ new: state.scratchGui.customProcedures.new }); -export default connect( +export default injectBlock(connect( mapStateToProps -)(CustomProcedures); +)(CustomProcedures)); diff --git a/packages/gui/src/containers/gui.tsx b/packages/gui/src/containers/gui.tsx index b20fb3256..63ead6dfc 100644 --- a/packages/gui/src/containers/gui.tsx +++ b/packages/gui/src/containers/gui.tsx @@ -38,6 +38,7 @@ import themeManagerHOC from '../lib/theme-manager-hoc.jsx'; import GUIComponent from '../components/gui/gui'; import {setIsScratchDesktop} from '../lib/isScratchDesktop'; +import {isScratchPaintLoaded, getScratchPaint} from '../lib/paint-loader'; import type {RootState} from '../lib/app-state-hoc'; import type {PropsOf} from '../lib/type-traits'; @@ -119,6 +120,9 @@ class GUI extends React.Component { setIsScratchDesktop(!!this.props.isScratchDesktop); this.props.onStorageInit!(storage); this.props.onVmInit!(this.props.vm); + if (!this.props.isPlayerOnly) { + this.preloadPaint(); + } } override componentDidUpdate (prevProps: GUIProps) { if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) { @@ -129,6 +133,15 @@ class GUI extends React.Component { // At this time the project view in www doesn't need to know when a project is unloaded this.props.onProjectLoaded!(); } + if (!this.props.isPlayerOnly && prevProps.isPlayerOnly) { + this.preloadPaint(); + } + } + preloadPaint () { + // Preload paint editor when main editor get ready + if (!isScratchPaintLoaded()) { + getScratchPaint(); + } } override render () { if (this.props.isError) { diff --git a/packages/gui/src/containers/paint-editor-wrapper.jsx b/packages/gui/src/containers/paint-editor-wrapper.jsx index e9f83a005..786a35338 100644 --- a/packages/gui/src/containers/paint-editor-wrapper.jsx +++ b/packages/gui/src/containers/paint-editor-wrapper.jsx @@ -2,8 +2,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; import VM from 'clipcc-vm'; -import PaintEditor from 'clipcc-paint'; import {inlineSvgFonts} from 'clipcc-svg-renderer'; +import {injectPaint} from '../lib/paint-loader-hoc'; import {connect} from 'react-redux'; @@ -56,8 +56,10 @@ class PaintEditorWrapper extends React.Component { const { selectedCostumeIndex, vm, + paint, ...componentProps } = this.props; + const PaintEditor = paint.default; return ( { @@ -107,6 +111,6 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => { }; }; -export default connect( +export default injectPaint(connect( mapStateToProps -)(PaintEditorWrapper); +)(PaintEditorWrapper)); diff --git a/packages/gui/src/lib/app-state-hoc.tsx b/packages/gui/src/lib/app-state-hoc.tsx index 5bbf4ed49..4f0a3245e 100644 --- a/packages/gui/src/lib/app-state-hoc.tsx +++ b/packages/gui/src/lib/app-state-hoc.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Provider} from 'react-redux'; import {createStore, combineReducers, compose} from 'redux'; -import type {Reducer, Store, StoreEnhancer} from 'redux'; +import type {Store} from 'redux'; import ConnectedIntlProvider from './connected-intl-provider.jsx'; import localesReducer, {initLocale, localesInitialState} from '../reducers/locales'; @@ -11,6 +11,7 @@ import {setPlayer, setFullScreen} from '../reducers/mode'; import locales from 'clipcc-l10n'; import {detectLocale} from './detect-locale'; +import {setInitialReducers, setStore} from '../reducers/utils'; import type {GuiState} from '../reducers/gui'; type ComposeEnhancers = typeof compose; @@ -48,7 +49,7 @@ const AppStateHOC = function

( localesOnly?: boolean ): React.ComponentType

{ class AppStateWrapper extends React.Component

{ - private store!: Store; + private store!: Store>; constructor (props: P) { super(props); @@ -62,8 +63,9 @@ const AppStateHOC = function

( // browser modal const reducers = {locales: localesReducer}; const initialState = {locales: initializedLocales}; - const enhancer: StoreEnhancer = composeEnhancers(); - const reducer = combineReducers(reducers); + const enhancer = composeEnhancers(); + setInitialReducers(reducers); + const reducer = combineReducers<{locales: LocalesState}>(reducers); this.store = createStore( reducer, initialState, @@ -82,8 +84,6 @@ const AppStateHOC = function

( initPlayer, initTelemetryModal } = guiRedux; - // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports - const {ScratchPaintReducer}: {ScratchPaintReducer: Reducer} = require('clipcc-paint'); let initializedGui: GuiState = guiInitialState; if (props.isFullScreen || props.isPlayerOnly) { @@ -98,21 +98,22 @@ const AppStateHOC = function

( } const reducers = { locales: localesReducer, - scratchGui: guiReducer, - scratchPaint: ScratchPaintReducer + scratchGui: guiReducer }; const initialState = { locales: initializedLocales, scratchGui: initializedGui }; - const enhancer: StoreEnhancer = composeEnhancers(guiMiddleware); - const reducer = combineReducers(reducers); + const enhancer = composeEnhancers(guiMiddleware); + setInitialReducers(reducers); + const reducer = combineReducers(reducers); this.store = createStore( reducer, initialState, enhancer ); } + setStore(this.store); } override componentDidUpdate (prevProps: Readonly

) { if (localesOnly) return; diff --git a/packages/gui/src/lib/backpack/block-to-image.js b/packages/gui/src/lib/backpack/block-to-image.js index 64e1704b9..366b225b9 100644 --- a/packages/gui/src/lib/backpack/block-to-image.js +++ b/packages/gui/src/lib/backpack/block-to-image.js @@ -1,12 +1,13 @@ import computedStyleToInlineStyle from 'computed-style-to-inline-style'; -import * as ScratchBlocks from 'clipcc-block'; +import {getScratchBlocks} from '../blocks-loader'; /** * Given a blockId, return a data-uri image that can be used to create a thumbnail. * @param {string} blockId the ID of the block to imagify * @returns {Promise} resolves to a data-url of a picture of the blocks */ -export default function (blockId) { +export default async function (blockId) { + const ScratchBlocks = await getScratchBlocks(); // Not sure any better way to access the scratch-blocks workspace than this... const block = ScratchBlocks.common.getMainWorkspace().getBlockById(blockId); const blockSvg = block.getSvgRoot().cloneNode(true /* deep */); diff --git a/packages/gui/src/lib/blocks-loader-hoc.tsx b/packages/gui/src/lib/blocks-loader-hoc.tsx new file mode 100644 index 000000000..430b040f9 --- /dev/null +++ b/packages/gui/src/lib/blocks-loader-hoc.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {getScratchBlocks} from './blocks-loader'; +import ScopedLoaderComponent from '../components/loader/scoped-loader'; + +interface BlockLoaderProps { + blocks: typeof import('clipcc-block'); +} + +export function injectBlock> (WrappedComponent: Component) { + class BlockLoaderHOC extends React.Component { + state = { + loaded: false + }; + blocks: typeof import('clipcc-block') | null = null; + + override async componentDidMount () { + if (!this.state.loaded) { + this.blocks = await getScratchBlocks(); + this.setState({loaded: true}); + } + } + + override render () { + if (!this.state.loaded) { + return ; + } + + return ; + } + } + + return BlockLoaderHOC; +} diff --git a/packages/gui/src/lib/blocks-loader.ts b/packages/gui/src/lib/blocks-loader.ts new file mode 100644 index 000000000..15896eb8f --- /dev/null +++ b/packages/gui/src/lib/blocks-loader.ts @@ -0,0 +1,25 @@ +// eslint-disable-next-line import/no-mutable-exports +export let ScratchBlocksModule: typeof import('clipcc-block') | null = null; + +/** + * Check if the ScratchBlocks module is loaded. + * @returns True if the ScratchBlocks module is loaded, false otherwise. + */ +export function isScratchBlocksLoaded () { + return !!ScratchBlocksModule; +} + +/** + * Get the ScratchBlocks module, which is loaded asynchronously to reduce the initial bundle size. + * @returns A promise that resolves to the ScratchBlocks module. + */ +export async function getScratchBlocks () { + if (!ScratchBlocksModule) { + // eslint-disable-next-line require-atomic-updates + ScratchBlocksModule = await import( + /* webpackChunkName: "clipcc-block" */ + 'clipcc-block' + ); + } + return ScratchBlocksModule; +} diff --git a/packages/gui/src/lib/blocks.js b/packages/gui/src/lib/blocks.js index bff950212..7d86bde9a 100644 --- a/packages/gui/src/lib/blocks.js +++ b/packages/gui/src/lib/blocks.js @@ -1,7 +1,6 @@ -import * as ScratchBlocks from 'clipcc-block'; - /** * @typedef {import('clipcc-vm')} VirtualMachine + * @import * as ScratchBlocks from 'clipcc-block' */ /** @@ -20,9 +19,10 @@ export const setRecordSoundCallback = callback => { /** * Connect scratch blocks with the vm * @param {VirtualMachine} vm - The scratch vm + * @param {ScratchBlocks} ScratchBlocks - The scratch blocks to connect * @returns {ScratchBlocks} ScratchBlocks connected with the vm */ -export default function (vm) { +export default function (vm, ScratchBlocks) { const jsonForMenuBlock = function (name, menuOptionsFn, category, start) { return { diff --git a/packages/gui/src/lib/make-toolbox.ts b/packages/gui/src/lib/make-toolbox.ts index 4d4be19fc..03cb9d31b 100644 --- a/packages/gui/src/lib/make-toolbox.ts +++ b/packages/gui/src/lib/make-toolbox.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import * as ClipCCBlocks from 'clipcc-block'; +import {ScratchBlocksModule as ClipCCBlocks, isScratchBlocksLoaded} from './blocks-loader'; +import type * as ClipCCBlockType from 'clipcc-block'; type ShadowFields = Record; -type ShadowInput = ClipCCBlocks.serialization.blocks.ConnectionState; -type ToolboxInfo = ClipCCBlocks.utils.toolbox.ToolboxInfo; -type ToolboxItem = ClipCCBlocks.utils.toolbox.ToolboxItemInfo; +type ShadowInput = ClipCCBlockType.serialization.blocks.ConnectionState; +type ToolboxInfo = ClipCCBlockType.utils.toolbox.ToolboxInfo; +type ToolboxItem = ClipCCBlockType.utils.toolbox.ToolboxItemInfo; interface ExtensionCategory { id: string; @@ -41,8 +42,22 @@ const createShadow = (type: string, fields?: ShadowFields): ShadowInput => { }; }; +/** + * Translate a message ID into a message, using the ScratchBlocks message system if possible. + * @param id - The message ID to translate + * @param defaultMessage - The default message to use if the ScratchBlocks isn't available + * @returns The translated message, or the default message if the ScratchBlocks isn't available + * or missing that message. + */ +const translate = (id: string, defaultMessage: string) => { + if (!isScratchBlocksLoaded()) { + return defaultMessage; + } + return ClipCCBlocks!.Msg[id] ?? defaultMessage; +}; + const motion = (isInitialSetup: boolean, isStage: boolean, targetId?: string | null): ToolboxItem => { - const stageSelected = ClipCCBlocks.Msg.MOTION_STAGE_SELECTED; + const stageSelected = translate('MOTION_STAGE_SELECTED', 'Stage selected: no motion blocks'); const motionContents: ToolboxItem[] = []; if (isStage) { @@ -176,8 +191,8 @@ const looks = ( costumeName: string, backdropName: string ): ToolboxItem => { - const hello = ClipCCBlocks.Msg.LOOKS_HELLO; - const hmm = ClipCCBlocks.Msg.LOOKS_HMM; + const hello = translate('LOOKS_HELLO', 'Hello!'); + const hmm = translate('LOOKS_HMM', 'Hmm...'); const looksContents: ToolboxItem[] = []; @@ -502,7 +517,7 @@ const sensing = ( targetId: string | null | undefined, hideNonVanillaBlocks: boolean ): ToolboxItem => { - const name = ClipCCBlocks.Msg.SENSING_ASK_TEXT; + const name = translate('SENSING_ASK_TEXT', `What's your name?`); const sensingContents: ToolboxItem[] = []; @@ -679,9 +694,9 @@ const operators = ( targetId: string | null | undefined, hideNonVanillaBlocks: boolean ): ToolboxItem => { - const apple = ClipCCBlocks.Msg.OPERATORS_JOIN_APPLE; - const banana = ClipCCBlocks.Msg.OPERATORS_JOIN_BANANA; - const letter = ClipCCBlocks.Msg.OPERATORS_LETTEROF_APPLE; + const apple = translate('OPERATORS_JOIN_APPLE', 'apple'); + const banana = translate('OPERATORS_JOIN_BANANA', 'banana'); + const letter = translate('OPERATORS_LETTEROF_APPLE', 'a'); const operatorsContents: ToolboxItem[] = [ { @@ -1011,8 +1026,8 @@ const makeToolbox = function ( // Convert xml toolbox to json. for (const category of categories) { - if (category.json || !category.xml) continue; - const toolbox = ClipCCBlocks.utils.toolbox.convertToolboxDefToJson( + if (category.json || !category.xml || !isScratchBlocksLoaded()) continue; + const toolbox = ClipCCBlocks!.utils.toolbox.convertToolboxDefToJson( `${category.xml}` ); if (!toolbox || toolbox.contents.length === 0) { diff --git a/packages/gui/src/lib/paint-loader-hoc.tsx b/packages/gui/src/lib/paint-loader-hoc.tsx new file mode 100644 index 000000000..b54633136 --- /dev/null +++ b/packages/gui/src/lib/paint-loader-hoc.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import {getScratchPaint} from './paint-loader'; +import ScopedLoaderComponent from '../components/loader/scoped-loader'; +import {injectReducer} from '../reducers/utils'; + +interface PaintLoaderProps { + paint: typeof import('clipcc-paint'); +} + +export function injectPaint> (WrappedComponent: Component) { + class PaintLoaderHOC extends React.Component { + state = { + loaded: false + }; + paint: typeof import('clipcc-paint') | null = null; + + override async componentDidMount () { + if (!this.state.loaded) { + this.paint = await getScratchPaint(); + injectReducer('scratchPaint', this.paint.ScratchPaintReducer); + this.setState({loaded: true}); + } + } + + override render () { + if (!this.state.loaded) { + return ; + } + + return ; + } + } + + return PaintLoaderHOC; +} diff --git a/packages/gui/src/lib/paint-loader.js b/packages/gui/src/lib/paint-loader.js new file mode 100644 index 000000000..5e11e2828 --- /dev/null +++ b/packages/gui/src/lib/paint-loader.js @@ -0,0 +1,28 @@ +// eslint-disable-next-line import/no-mutable-exports +export let ScratchPaintModule = null; +let loadingPromise = null; + +/** + * Check if the ScratchPaint module is loaded. + * @returns {boolean} True if the ScratchPaint module is loaded, false otherwise. + */ +export const isScratchPaintLoaded = function () { + return !!ScratchPaintModule; +}; + +/** + * Get the ScratchPaint module, which is loaded asynchronously to reduce the initial bundle size. + * @returns {Promise} A promise that resolves to the ScratchPaint module. + */ +export const getScratchPaint = function () { + if (!loadingPromise) { + loadingPromise = import( + /* webpackChunkName: "clipcc-paint" */ + 'clipcc-paint' + ).then(module => { + ScratchPaintModule = module; + return module; + }); + } + return loadingPromise; +}; diff --git a/packages/gui/src/reducers/utils.ts b/packages/gui/src/reducers/utils.ts new file mode 100644 index 000000000..56c6b6e2b --- /dev/null +++ b/packages/gui/src/reducers/utils.ts @@ -0,0 +1,43 @@ +import {combineReducers, type Reducer, type Store} from 'redux'; +import type {ReducerMap} from '../lib/type-traits'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let initialReducers: Record> = {}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let storeInstance: Store | null = null; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const dynamicReducers: Record> = {}; + +/** + * Set the initial reducers for the store. This should be called before creating the store, + * and the reducers passed here will be combined with any dynamically injected reducers. + * @param reducers The initial reducers to set for the store. + */ +export function setInitialReducers (reducers: ReducerMap) { + initialReducers = reducers; +} + +/** + * Set the Redux store instance. This is necessary for the injectReducer function to work, + * as it needs access to the store to replace the reducer. + * @param store The Redux store instance to set for dynamic reducer injection. + */ +export function setStore (store: Store) { + storeInstance = store; +} + +/** + * Dynamically inject a reducer into the store. This is useful for code-splitting and loading reducers on demand. + * @param key The key under which to store the reducer's state in the Redux store. + * @param reducer The reducer function to inject. + */ +export function injectReducer ( + key: string, + reducer: Reducer) { + dynamicReducers[key] = reducer; + if (!storeInstance) return; + storeInstance.replaceReducer(combineReducers({ + ...initialReducers, + ...dynamicReducers + })); +} diff --git a/packages/vm/src/extension-support/extension-manager.js b/packages/vm/src/extension-support/extension-manager.js index 235fa7358..b17e59909 100644 --- a/packages/vm/src/extension-support/extension-manager.js +++ b/packages/vm/src/extension-support/extension-manager.js @@ -7,25 +7,36 @@ import BlockType from './block-type'; // TODO: move these out into a separate repository? // TODO: change extension spec so that library info, including extension ID, can be collected through static methods -/* eslint-disable global-require */ +/** + * List of core extensions that should load at startup. + */ +const coreExtensions = { + // eslint-disable-next-line global-require + coreExample: () => require('../blocks/scratch3_core_example') +}; + +/** + * List of built-in extensions that are available to load by ID or URL but do not load at startup. + */ const builtinExtensions = { // This is an example that isn't loaded with the other core blocks, // but serves as a reference for loading core blocks as extensions. - coreExample: () => require('../blocks/scratch3_core_example'), // These are the non-core built-in extensions. - pen: () => require('../extensions/scratch3_pen'), - wedo2: () => require('../extensions/scratch3_wedo2'), - music: () => require('../extensions/scratch3_music'), - microbit: () => require('../extensions/scratch3_microbit'), - text2speech: () => require('../extensions/scratch3_text2speech'), - translate: () => require('../extensions/scratch3_translate'), - videoSensing: () => require('../extensions/scratch3_video_sensing'), - ev3: () => require('../extensions/scratch3_ev3'), - makeymakey: () => require('../extensions/scratch3_makeymakey'), - boost: () => require('../extensions/scratch3_boost'), - gdxfor: () => require('../extensions/scratch3_gdx_for') + pen: () => import(/* webpackChunkName: "ext_pen" */ '../extensions/scratch3_pen/index.js'), + wedo2: () => import(/* webpackChunkName: "ext_wedo2" */ '../extensions/scratch3_wedo2/index.js'), + music: () => import(/* webpackChunkName: "ext_music" */ '../extensions/scratch3_music/index.js'), + microbit: () => import(/* webpackChunkName: "ext_microbit" */ '../extensions/scratch3_microbit/index.js'), + text2speech: () => import(/* webpackChunkName: "ext_text2speech" */ '../extensions/scratch3_text2speech/index.js'), + translate: () => import(/* webpackChunkName: "ext_translate" */ '../extensions/scratch3_translate/index.js'), + videoSensing: () => import( + /* webpackChunkName: "ext_videoSensing" */ + '../extensions/scratch3_video_sensing/index.js' + ), + ev3: () => import(/* webpackChunkName: "ext_ev3" */ '../extensions/scratch3_ev3/index.js'), + makeymakey: () => import(/* webpackChunkName: "ext_makeymakey" */ '../extensions/scratch3_makeymakey/index.js'), + boost: () => import(/* webpackChunkName: "ext_boost" */ '../extensions/scratch3_boost/index.js'), + gdxfor: () => import(/* webpackChunkName: "ext_gdxfor" */ '../extensions/scratch3_gdx_for/index.js') }; -/* eslint-enable global-require */ /** * @typedef {object} ArgumentInfo - Information about an extension block argument @@ -117,8 +128,8 @@ class ExtensionManager { * @param {string} extensionId - the ID of an internal extension */ loadExtensionIdSync (extensionId) { - if (!Object.prototype.hasOwnProperty.call(builtinExtensions, extensionId)) { - log.warn(`Could not find extension ${extensionId} in the built in extensions.`); + if (!Object.prototype.hasOwnProperty.call(coreExtensions, extensionId)) { + log.warn(`Could not find extension ${extensionId} in core extensions.`); return; } @@ -129,7 +140,7 @@ class ExtensionManager { return; } - const {default: extension} = builtinExtensions[extensionId](); + const extension = coreExtensions[extensionId](); const extensionInstance = new extension(this.runtime); const serviceName = this._registerInternalExtension(extensionInstance); this._loadedExtensions.set(extensionId, serviceName); @@ -140,20 +151,24 @@ class ExtensionManager { * @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure */ - loadExtensionURL (extensionURL) { + async loadExtensionURL (extensionURL) { + if (Object.prototype.hasOwnProperty.call(coreExtensions, extensionURL)) { + this.loadExtensionIdSync(extensionURL); + return; + } if (Object.prototype.hasOwnProperty.call(builtinExtensions, extensionURL)) { /** @todo dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */ if (this.isExtensionLoaded(extensionURL)) { const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`; log.warn(message); - return Promise.resolve(); + return; } - const {default: extension} = builtinExtensions[extensionURL](); + const {default: extension} = await builtinExtensions[extensionURL](); const extensionInstance = new extension(this.runtime); const serviceName = this._registerInternalExtension(extensionInstance); this._loadedExtensions.set(extensionURL, serviceName); - return Promise.resolve(); + return; } return new Promise((resolve, reject) => { diff --git a/packages/vm/src/virtual-machine.js b/packages/vm/src/virtual-machine.js index 2caf03e38..4585c5d82 100644 --- a/packages/vm/src/virtual-machine.js +++ b/packages/vm/src/virtual-machine.js @@ -25,21 +25,6 @@ import 'canvas-toBlob'; const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; -/** - * @type {string[]} - */ -const CORE_EXTENSIONS = [ - // 'motion', - // 'looks', - // 'sound', - // 'events', - // 'control', - // 'sensing', - // 'operators', - // 'variables', - // 'myBlocks' -]; - /** * @typedef {number} int * @typedef {import('./engine/target')} Target @@ -186,11 +171,6 @@ class VirtualMachine extends EventEmitter { this.extensionManager = new ExtensionManager(this.runtime); - // Load core extensions - for (const id of CORE_EXTENSIONS) { - this.extensionManager.loadExtensionIdSync(id); - } - this.blockListener = this.blockListener.bind(this); this.flyoutBlockListener = this.flyoutBlockListener.bind(this); this.monitorBlockListener = this.monitorBlockListener.bind(this); @@ -454,9 +434,11 @@ class VirtualMachine extends EventEmitter { resolve(res); }); }) - .catch(error => { - // eslint-disable-next-line global-require - const {SB1File, ValidationError} = require('clipcc-sb1-converter'); + .catch(async error => { + const {SB1File, ValidationError} = await import( + /* webpackChunkName: "clipcc-sb1-convertor" */ + 'clipcc-sb1-converter' + ); try { const sb1 = new SB1File(input);