From 096eea7077089cc99c1bc12abaab8fa86c285112 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 24 Jun 2022 19:15:32 -0500 Subject: [PATCH] editor: add basic intellisense This adds basic intellisense for code completion and function signatures using the Python `jedi` package running in a Pyodide environment. --- CHANGELOG.md | 5 + config/jest/babelTransform.js | 4 +- config/webpackDevServer.config.js | 2 + package.json | 2 + src/editor/actions.ts | 5 + src/editor/reducers.ts | 2 + src/editor/redux/codeCompletion.ts | 38 +++ src/editor/sagas.ts | 289 ++++++++++++++++++++++ src/pybricksMicropython/python-message.ts | 101 ++++++++ src/pybricksMicropython/python-worker.ts | 115 +++++++++ src/status-bar/StatusBar.tsx | 71 +++++- src/status-bar/i18n.ts | 5 + src/status-bar/status-bar.scss | 9 +- src/status-bar/translations/en.json | 9 + yarn.lock | 51 +++- 15 files changed, 693 insertions(+), 15 deletions(-) create mode 100644 src/editor/redux/codeCompletion.ts create mode 100644 src/pybricksMicropython/python-message.ts create mode 100644 src/pybricksMicropython/python-worker.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b61343..c19cb90a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ # Changelog +## [Unreleased] + +### Added +- Added basic intellisense to the code editor. + ## [2.0.0-beta.1] - 2022-06-03 ### Added diff --git a/config/jest/babelTransform.js b/config/jest/babelTransform.js index 77f4860f..f4d396ae 100644 --- a/config/jest/babelTransform.js +++ b/config/jest/babelTransform.js @@ -20,7 +20,9 @@ module.exports = babelJest.createTransformer({ ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - '@shopify/react-i18n/babel'], + '@shopify/react-i18n/babel', + "babel-plugin-transform-import-meta", + ], presets: [ [ require.resolve('babel-preset-react-app'), diff --git a/config/webpackDevServer.config.js b/config/webpackDevServer.config.js index 52f4edf3..9e1e779d 100644 --- a/config/webpackDevServer.config.js +++ b/config/webpackDevServer.config.js @@ -40,6 +40,8 @@ module.exports = function (proxy, allowedHost) { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': '*', + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', }, // Enable gzip compression of generated files. compress: true, diff --git a/package.json b/package.json index 17d32da6..2b84b5ee 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "babel-loader": "^8.2.3", "babel-plugin-macros": "^3.0.1", "babel-plugin-named-asset-import": "^0.3.8", + "babel-plugin-transform-import-meta": "^2.2.0", "babel-preset-react-app": "^10.0.1", "bfj": "^7.0.2", "browser-fs-access": "^0.30.1", @@ -78,6 +79,7 @@ "postcss-preset-env": "^7.7.2", "prompts": "^2.4.2", "prop-types": "^15.8.1", + "pyodide": "0.20.1-alpha.2", "react": "^16.13.1", "react-app-polyfill": "^3.0.0", "react-aria": "^3.17.0", diff --git a/src/editor/actions.ts b/src/editor/actions.ts index f094f39b..b770c970 100644 --- a/src/editor/actions.ts +++ b/src/editor/actions.ts @@ -3,6 +3,11 @@ import { createAction } from '../actions'; import { UUID } from '../fileStorage'; +export { + didFailToInit as editorCompletionDidFailToInit, + didInit as editorCompletionDidInit, + init as editorCompletionInit, +} from './redux/codeCompletion'; /** Action that indicates that a code editor was created. */ export const editorDidCreate = createAction(() => ({ diff --git a/src/editor/reducers.ts b/src/editor/reducers.ts index 2274e985..6ae1acc4 100644 --- a/src/editor/reducers.ts +++ b/src/editor/reducers.ts @@ -9,6 +9,7 @@ import { editorDidCreate, editorDidOpenFile, } from './actions'; +import codeCompletion from './redux/codeCompletion'; /** Indicates that the code editor is ready for use. */ const isReady: Reducer = (state = false, action) => { @@ -46,6 +47,7 @@ const openFileUuids: Reducer = (state = [], action) => { }; export default combineReducers({ + codeCompletion, isReady, activeFileUuid, openFileUuids, diff --git a/src/editor/redux/codeCompletion.ts b/src/editor/redux/codeCompletion.ts new file mode 100644 index 00000000..9cfab142 --- /dev/null +++ b/src/editor/redux/codeCompletion.ts @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { createSlice } from '@reduxjs/toolkit'; + +export enum CompletionEngineStatus { + Unknown, + Loading, + Ready, + Failed, +} + +type State = { + status: CompletionEngineStatus; +}; + +const initialState: State = { + status: CompletionEngineStatus.Unknown, +}; + +const slice = createSlice({ + name: 'codeCompletion', + initialState, + reducers: { + init(state) { + state.status = CompletionEngineStatus.Loading; + }, + didInit(state) { + state.status = CompletionEngineStatus.Ready; + }, + didFailToInit(state) { + state.status = CompletionEngineStatus.Failed; + }, + }, +}); + +export const { init, didInit, didFailToInit } = slice.actions; +export default slice.reducer; diff --git a/src/editor/sagas.ts b/src/editor/sagas.ts index 8ccbef36..7a8b4aae 100644 --- a/src/editor/sagas.ts +++ b/src/editor/sagas.ts @@ -6,6 +6,7 @@ import { EventChannel, buffers, eventChannel } from 'redux-saga'; import { SagaGenerator, call, + cancelled, delay, fork, getContext, @@ -26,11 +27,27 @@ import { fileStorageStoreTextFileValue, fileStorageStoreTextFileViewState, } from '../fileStorage/actions'; +import { + pythonMessageComplete, + pythonMessageDidComplete, + pythonMessageDidFailToComplete, + pythonMessageDidFailToGetSignature, + pythonMessageDidFailToInit, + pythonMessageDidGetSignature, + pythonMessageDidInit, + pythonMessageGetSignature, + pythonMessageInit, + pythonMessageSetInterruptBuffer, +} from '../pybricksMicropython/python-message'; import { RootState } from '../reducers'; import { acquireLock, defined, ensureError } from '../utils'; +import { createCountFunc } from '../utils/iter'; import { editorActivateFile, editorCloseFile, + editorCompletionDidFailToInit, + editorCompletionDidInit, + editorCompletionInit, editorDidActivateFile, editorDidCloseFile, editorDidCreate, @@ -373,6 +390,278 @@ function* monitorEditors(): Generator { } } +/** + * Runs a web worker with Pyodide so that we can use Jedi for intellisense. + */ +function* runJedi(): Generator { + const defer = new Array<() => void>(); + + try { + console.debug('creating code completion worker'); + + // start the web worker and set up communication channels + + const worker = new Worker( + new URL('../pybricksMicropython/python-worker.ts', import.meta.url), + ); + + defer.push(() => worker.terminate()); + + const messageChannel = eventChannel((emit) => { + worker.addEventListener('message', emit); + + return () => worker.removeEventListener('message', emit); + }, buffers.expanding()); + + defer.push(() => messageChannel.close()); + + const errorChannel = eventChannel((emit) => { + worker.addEventListener('error', emit); + + return () => worker.removeEventListener('error', emit); + }, buffers.expanding()); + + defer.push(() => errorChannel.close()); + + // wait for the Python runtime to start and get in a ready state + + worker.postMessage(pythonMessageInit()); + yield* put(editorCompletionInit()); + + for (;;) { + const { messageEvent, errorEvent } = yield* race({ + messageEvent: take(messageChannel), + errorEvent: take(errorChannel), + }); + + if (errorEvent) { + yield* put(editorCompletionDidFailToInit()); + throw errorEvent.error; + } + + defined(messageEvent); + + if (pythonMessageDidFailToInit.matches(messageEvent.data)) { + yield* put(editorCompletionDidFailToInit()); + throw messageEvent.data.error; + } + + if (pythonMessageDidInit.matches(messageEvent.data)) { + break; + } + } + + console.debug('code completion engine is ready'); + yield* put(editorCompletionDidInit()); + + // configure interrupts + // https://pyodide.org/en/stable/usage/keyboard-interrupts.html + + // HACK: Using WebAssembly.Memory instead of SharedArrayBuffer to avoid exception. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/Planned_changes#api_changes + const interrupt = new Uint8Array( + new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }).buffer, + ); + + const setInterrupt = () => { + interrupt[0] = 2; //2 === SIGINT + }; + + const clearInterrupt = () => { + interrupt[0] = 0; + }; + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements + if (crossOriginIsolated) { + worker.postMessage(pythonMessageSetInterruptBuffer(interrupt)); + } else { + console.warn( + 'required headers missing for SharedArrayBuffer, cancellation will not work', + ); + } + + // register intellisense hooks with editor + + const nextId = createCountFunc(); + + const completionItemProviderChan = eventChannel<{ + model: monaco.editor.ITextModel; + position: monaco.Position; + context: monaco.languages.CompletionContext; + token: monaco.CancellationToken; + resolve: (value: monaco.languages.CompletionList | null) => void; + }>((emit) => { + const subscription = monaco.languages.registerCompletionItemProvider( + pybricksMicroPythonId, + { + triggerCharacters: ['.', ' '], + provideCompletionItems( + model, + position, + context, + token, + ): Promise { + return new Promise((resolve) => { + emit({ model, position, context, token, resolve }); + }); + }, + }, + ); + + return () => subscription.dispose(); + }, buffers.expanding()); + + defer.push(() => completionItemProviderChan.close()); + + const signatureProviderChan = eventChannel<{ + model: monaco.editor.ITextModel; + position: monaco.Position; + token: monaco.CancellationToken; + context: monaco.languages.SignatureHelpContext; + resolve: (value: monaco.languages.SignatureHelpResult | null) => void; + }>((emit) => { + const subscription = monaco.languages.registerSignatureHelpProvider( + pybricksMicroPythonId, + { + signatureHelpTriggerCharacters: ['('], + signatureHelpRetriggerCharacters: [','], + provideSignatureHelp(model, position, token, context) { + return new Promise((resolve) => { + emit({ model, position, token, context, resolve }); + }); + }, + }, + ); + + return () => subscription.dispose(); + }, buffers.expanding()); + + defer.push(() => signatureProviderChan.close()); + + // Serialize requests from editor. Due to the way cancellation works, we + // can only have one pending message from the web worker at a time. + + for (;;) { + const { complete, getSignature } = yield* race({ + complete: take(completionItemProviderChan), + getSignature: take(signatureProviderChan), + }); + + if (complete) { + // for debugging + const id = nextId(); + + console.debug(`${id}: requested completion item`); + + const subscription = complete.token.onCancellationRequested(() => { + console.debug(`${id}: requested cancelation`); + setInterrupt(); + }); + + try { + clearInterrupt(); + + worker.postMessage( + pythonMessageComplete( + complete.model.getValue(), + complete.position.lineNumber, + complete.position.column, + ), + ); + + for (;;) { + const msg = yield* take(messageChannel); + + if (pythonMessageDidFailToComplete.matches(msg.data)) { + if ( + msg.data.error instanceof DOMException && + msg.data.error.name === 'AbortError' + ) { + console.log(`${id} canceled`); + } else { + console.error(msg.data.error); + } + complete.resolve(null); + break; + } + + if (pythonMessageDidComplete.matches(msg.data)) { + const list = JSON.parse(msg.data.completionListJson); + console.debug(list); + complete.resolve({ suggestions: list }); + console.debug(`${id}: resolved: ${msg.data.type}`); + break; + } + } + } finally { + subscription.dispose(); + } + } else if (getSignature) { + // for debugging + const id = nextId(); + + console.debug(`${id}: requested signatures`); + + const subscription = getSignature.token.onCancellationRequested(() => { + console.debug(`${id}: requested cancelation`); + setInterrupt(); + }); + + try { + clearInterrupt(); + + worker.postMessage( + pythonMessageGetSignature( + getSignature.model.getValue(), + getSignature.position.lineNumber, + getSignature.position.column, + ), + ); + + for (;;) { + const msg = yield* take(messageChannel); + + if (pythonMessageDidFailToGetSignature.matches(msg.data)) { + if ( + msg.data.error instanceof DOMException && + msg.data.error.name === 'AbortError' + ) { + console.log(`${id} canceled`); + } else { + console.error(msg.data.error); + } + getSignature.resolve(null); + break; + } + + if (pythonMessageDidGetSignature.matches(msg.data)) { + const signatures = JSON.parse(msg.data.signatureHelpJson); + console.debug(signatures); + getSignature.resolve({ + value: signatures, + dispose: () => undefined, + }); + console.debug(`${id}: resolved: ${msg.data.type}`); + break; + } + } + } finally { + subscription.dispose(); + } + } + } + } catch (err) { + const isCancelled = yield* cancelled(); + + if (!isCancelled) { + console.error(err); + } + } finally { + defer.forEach((item) => item()); + } +} + export default function* (): Generator { yield* fork(monitorEditors); + yield* fork(runJedi); } diff --git a/src/pybricksMicropython/python-message.ts b/src/pybricksMicropython/python-message.ts new file mode 100644 index 00000000..728b3f65 --- /dev/null +++ b/src/pybricksMicropython/python-message.ts @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { createAction } from '../actions'; + +// NB: although we are using the same action creator as we do for redux, these +// actions are not used by redux but rather are to sent between workers. + +/** + * Message sent from main to work to request initialization of Pyodide. + */ +export const pythonMessageInit = createAction(() => ({ + type: 'python.message.init', +})); + +/** + * Message sent from worker to main that indicates {@link pythonMessageInit} + * succeeded. + */ +export const pythonMessageDidInit = createAction(() => ({ + type: 'python.message.didInit', +})); + +/** + * Message sent from worker to main that indicates {@link pythonMessageInit} + * failed. + */ +export const pythonMessageDidFailToInit = createAction((error: Error) => ({ + type: 'python.message.didFailToInit', + error, +})); + +/** + * Message sent from main to worker to set the shared interrupt buffer. + */ +export const pythonMessageSetInterruptBuffer = createAction((buffer: Uint8Array) => ({ + type: 'python.message.setInterruptBuffer', + buffer, +})); + +/** + * Message sent from main to worker to request code completion. + */ +export const pythonMessageComplete = createAction( + (code: string, lineNumber: number, column: number) => ({ + type: 'python.message.complete', + code, + lineNumber, + column, + }), +); + +/** + * Message sent from worker to main that indicates {@link pythonMessageComplete} + * succeeded. + */ +export const pythonMessageDidComplete = createAction((completionListJson: string) => ({ + type: 'python.message.didComplete', + completionListJson, +})); + +/** + * Message sent from worker to main that indicates {@link pythonMessageComplete} + * failed. + */ +export const pythonMessageDidFailToComplete = createAction((error: Error) => ({ + type: 'python.message.didFailToComplete', + error, +})); + +/** + * Message sent from main to worker to request function signature. + */ +export const pythonMessageGetSignature = createAction( + (code: string, lineNumber: number, column: number) => ({ + type: 'python.message.getSignature', + code, + lineNumber, + column, + }), +); + +/** + * Message sent from worker to main that indicates {@link pythonMessageGetSignature} + * succeeded. + */ +export const pythonMessageDidGetSignature = createAction( + (signatureHelpJson: string) => ({ + type: 'python.message.didGetSignature', + signatureHelpJson, + }), +); + +/** + * Message sent from worker to main that indicates {@link pythonMessageGetSignature} + * failed. + */ +export const pythonMessageDidFailToGetSignature = createAction((error: Error) => ({ + type: 'python.message.didFailToGetSignature', + error, +})); diff --git a/src/pybricksMicropython/python-worker.ts b/src/pybricksMicropython/python-worker.ts new file mode 100644 index 00000000..2c094eda --- /dev/null +++ b/src/pybricksMicropython/python-worker.ts @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +// This file runs as a web worker. + +// NB: We need to be very careful about imports here since many libraries for +// web aren't compatible with web workers! + +import type { loadPyodide as loadPyodideFunc } from 'pyodide'; +import { ensureError } from '../utils'; +import { + pythonMessageComplete, + pythonMessageDidComplete, + pythonMessageDidFailToComplete, + pythonMessageDidFailToGetSignature, + pythonMessageDidFailToInit, + pythonMessageDidGetSignature, + pythonMessageDidInit, + pythonMessageGetSignature, + pythonMessageInit, + pythonMessageSetInterruptBuffer, +} from './python-message'; + +importScripts('https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js'); + +declare const loadPyodide: typeof loadPyodideFunc; + +/** + * Wrapper around {@link ensureError} that also converts KeyboardInterrupt to + * AbortError. + * @param err The value from the catch clause. + * @returns The fixed up error. + */ +function fixUpError(err: unknown): Error { + const error = ensureError(err); + + if ( + error.constructor.name === 'PythonError' && + error.message.match(/KeyboardInterrupt/) + ) { + return new DOMException('cancelled', 'AbortError'); + } + + return error; +} + +const setUpPythonEnvironment = ` +import jedi +import micropip + +print('loading pybricks...') +await micropip.install('pybricks-jedi') +print('loaded pybricks.') + +import pybricks_jedi + +print('preloading...') +pybricks_jedi.initialize() +print('preloading done.') +`; + +async function init(): Promise { + console.log('starting Pyodide...'); + + const pyodide = await loadPyodide(); + await pyodide.loadPackage(['micropip', 'jedi']); + await pyodide.runPythonAsync(setUpPythonEnvironment); + + const complete = pyodide.runPython('pybricks_jedi.complete'); + const getSignatures = pyodide.runPython('pybricks_jedi.get_signatures'); + + self.addEventListener('message', async (e) => { + if (pythonMessageSetInterruptBuffer.matches(e.data)) { + pyodide.setInterruptBuffer(e.data.buffer); + return; + } + + if (pythonMessageComplete.matches(e.data)) { + console.debug('worker received complete message'); + try { + const { code, lineNumber, column } = e.data; + const list = complete(code, lineNumber, column); + self.postMessage(pythonMessageDidComplete(list)); + } catch (err) { + self.postMessage(pythonMessageDidFailToComplete(fixUpError(err))); + } + return; + } + + if (pythonMessageGetSignature.matches(e.data)) { + console.debug('worker received getSignatures message'); + try { + const { code, lineNumber, column } = e.data; + const list = getSignatures(code, lineNumber, column); + self.postMessage(pythonMessageDidGetSignature(list)); + } catch (err) { + self.postMessage(pythonMessageDidFailToGetSignature(fixUpError(err))); + } + return; + } + }); + + console.log('Pyodide is ready.'); +} + +self.addEventListener('message', async (e) => { + if (pythonMessageInit.matches(e.data)) { + try { + await init(); + postMessage(pythonMessageDidInit()); + } catch (err) { + postMessage(pythonMessageDidFailToInit(ensureError(err))); + } + } +}); diff --git a/src/status-bar/StatusBar.tsx b/src/status-bar/StatusBar.tsx index 224baeed..6ab0c30a 100644 --- a/src/status-bar/StatusBar.tsx +++ b/src/status-bar/StatusBar.tsx @@ -1,10 +1,18 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2022 The Pybricks Authors -import { Button, Intent, ProgressBar } from '@blueprintjs/core'; +import { + Button, + Icon, + IconSize, + Intent, + ProgressBar, + Spinner, +} from '@blueprintjs/core'; import { Classes as Classes2, Popover2, Popover2Props } from '@blueprintjs/popover2'; -import React from 'react'; +import React, { useMemo } from 'react'; import { BleConnectionState } from '../ble/reducers'; +import { CompletionEngineStatus } from '../editor/redux/codeCompletion'; import { useSelector } from '../reducers'; import { I18nId, useI18n } from './i18n'; @@ -15,6 +23,48 @@ const commonPopoverProps: Partial = { placement: 'top', }; +const CompletionEngineIndicator: React.VoidFunctionComponent = () => { + const { status } = useSelector((s) => s.editor.codeCompletion); + const i18n = useI18n(); + + const icon = useMemo(() => { + switch (status) { + case CompletionEngineStatus.Loading: + return ; + case CompletionEngineStatus.Ready: + return ; + case CompletionEngineStatus.Failed: + return ; + default: + return ; + } + }, [status]); + + const message = useMemo(() => { + switch (status) { + case CompletionEngineStatus.Loading: + return i18n.translate(I18nId.CompletionEngineStatusMessageLoading); + case CompletionEngineStatus.Ready: + return i18n.translate(I18nId.CompletionEngineStatusMessageReady); + case CompletionEngineStatus.Failed: + return i18n.translate(I18nId.CompletionEngineStatusMessageFailed); + default: + return i18n.translate(I18nId.CompletionEngineStatusMessageUnknown); + } + }, [status, i18n]); + + return ( + +
+ {icon} +
+
+ ); +}; + const HubInfoButton: React.VoidFunctionComponent = () => { const i18n = useI18n(); const deviceName = useSelector((s) => s.ble.deviceName); @@ -99,12 +149,17 @@ const StatusBar: React.VFC = (_props) => { return (
- {connection === BleConnectionState.Connected && ( - <> - - - - )} +
+ +
+
+ {connection === BleConnectionState.Connected && ( + <> + + + + )} +
); }; diff --git a/src/status-bar/i18n.ts b/src/status-bar/i18n.ts index cc20a639..f6252dde 100644 --- a/src/status-bar/i18n.ts +++ b/src/status-bar/i18n.ts @@ -12,6 +12,11 @@ export function useI18n(): I18n { } export enum I18nId { + CompletionEngineStatusLabel = 'completionEngineStatus.label', + CompletionEngineStatusMessageLoading = 'completionEngineStatus.message.loading', + CompletionEngineStatusMessageReady = 'completionEngineStatus.message.ready', + CompletionEngineStatusMessageFailed = 'completionEngineStatus.message.failed', + CompletionEngineStatusMessageUnknown = 'completionEngineStatus.message.unknown', BatteryTitle = 'battery.title', BatteryLow = 'battery.low', BatteryOk = 'battery.ok', diff --git a/src/status-bar/status-bar.scss b/src/status-bar/status-bar.scss index e6203e15..622cfd2c 100644 --- a/src/status-bar/status-bar.scss +++ b/src/status-bar/status-bar.scss @@ -15,11 +15,12 @@ padding-inline: 5px; display: flex; align-items: center; -} + justify-content: space-between; -// make status bar items right-aligned -.pb-status-bar :first-child { - margin-left: auto; + &-group { + display: flex; + align-items: center; + } } .pb-battery-indicator { diff --git a/src/status-bar/translations/en.json b/src/status-bar/translations/en.json index fc5b974b..1fa3ea66 100644 --- a/src/status-bar/translations/en.json +++ b/src/status-bar/translations/en.json @@ -1,4 +1,13 @@ { + "completionEngineStatus": { + "label": "Code completion engine status indicator", + "message": { + "loading": "Code completion engine is starting up and will be ready soon.", + "ready": "Code completion engine is ready to use.", + "failed": "Code completion engine has stopped working.", + "unknown": "Code completion engine has not started yet." + } + }, "hubInfo": { "title": "Connected hub", "connectedTo": "Connected to:", diff --git a/yarn.lock b/yarn.lock index 561fb3f2..2b87855d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1451,7 +1451,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.0.0, @babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.0.0, @babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3, @babel/template@npm:^7.4.4": version: 7.16.7 resolution: "@babel/template@npm:7.16.7" dependencies: @@ -2319,6 +2319,7 @@ __metadata: babel-loader: ^8.2.3 babel-plugin-macros: ^3.0.1 babel-plugin-named-asset-import: ^0.3.8 + babel-plugin-transform-import-meta: ^2.2.0 babel-preset-react-app: ^10.0.1 bfj: ^7.0.2 browser-fs-access: ^0.30.1 @@ -2370,6 +2371,7 @@ __metadata: prettier: ^2.7.1 prompts: ^2.4.2 prop-types: ^15.8.1 + pyodide: 0.20.1-alpha.2 react: ^16.13.1 react-app-polyfill: ^3.0.0 react-aria: ^3.17.0 @@ -5662,6 +5664,18 @@ __metadata: languageName: node linkType: hard +"babel-plugin-transform-import-meta@npm:^2.2.0": + version: 2.2.0 + resolution: "babel-plugin-transform-import-meta@npm:2.2.0" + dependencies: + "@babel/template": ^7.4.4 + tslib: ^2.4.0 + peerDependencies: + "@babel/core": ^7.10.0 + checksum: 305b0750f957e4df3ce08e0b24359dc55fb789c96477e944d113d259a8292e7bb8d665944e04b5a690c82a8dc23f54b711a85917d80988dffe1e046e1db940ee + languageName: node + linkType: hard + "babel-plugin-transform-react-remove-prop-types@npm:^0.4.24": version: 0.4.24 resolution: "babel-plugin-transform-react-remove-prop-types@npm:0.4.24" @@ -5734,6 +5748,13 @@ __metadata: languageName: node linkType: hard +"base-64@npm:^1.0.0": + version: 1.0.0 + resolution: "base-64@npm:1.0.0" + checksum: d10b64a1fc9b2c5a5f39f1ce1e6c9d1c5b249222bbfa3a0604c592d90623caf74419983feadd8a170f27dc0c3389704f72faafa3e645aeb56bfc030c93ff074a + languageName: node + linkType: hard + "base64-arraybuffer-es6@npm:^0.7.0": version: 0.7.0 resolution: "base64-arraybuffer-es6@npm:0.7.0" @@ -10785,7 +10806,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.5": +"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" dependencies: @@ -12363,6 +12384,17 @@ __metadata: languageName: node linkType: hard +"pyodide@npm:0.20.1-alpha.2": + version: 0.20.1-alpha.2 + resolution: "pyodide@npm:0.20.1-alpha.2" + dependencies: + base-64: ^1.0.0 + node-fetch: ^2.6.1 + ws: ^8.5.0 + checksum: aeadfd5822660b435561c7f010f1a029b28bfa6080a781a7fbb2fe95b02ca2ea95f234d3c79a5f750c71a6eb3b10872731814805b82be5cbb1a2da2a6386e65f + languageName: node + linkType: hard + "qs@npm:6.10.3": version: 6.10.3 resolution: "qs@npm:6.10.3" @@ -15345,6 +15377,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.5.0": + version: 8.8.0 + resolution: "ws@npm:8.8.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 6ceed1ca1cb800ef60c7fc8346c7d5d73d73be754228eb958765abf5d714519338efa20ffe674167039486eb3a813aae5a497f8d319e16b4d96216a31df5bd95 + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0"