From 68c6b91811ce996ff99b625259ff24f4e6c0c528 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Tue, 26 Mar 2024 21:44:17 +0800 Subject: [PATCH 01/39] Reformat devserver with Prettier --- devserver/README.md | 2 +- devserver/index.html | 2 +- devserver/src/components/ControlButton.tsx | 12 +- devserver/src/components/Playground.tsx | 192 ++++++++++-------- devserver/src/components/Workspace.tsx | 26 ++- .../src/components/controlBar/ControlBar.tsx | 11 +- .../controlBar/ControlBarClearButton.tsx | 18 +- .../controlBar/ControlBarRefreshButton.tsx | 18 +- .../controlBar/ControlBarRunButton.tsx | 4 +- devserver/src/components/editor/Editor.tsx | 26 ++- devserver/src/components/repl/Repl.tsx | 20 +- .../components/sideContent/SideContent.tsx | 38 +++- .../src/components/sideContent/TestTab.tsx | 37 ++-- devserver/src/components/sideContent/types.ts | 16 +- devserver/src/components/sideContent/utils.ts | 32 +-- devserver/src/components/utils/AceHelper.ts | 17 +- devserver/src/components/utils/Hooks.ts | 27 ++- devserver/src/main.tsx | 17 +- devserver/src/styles/_application.scss | 2 +- devserver/src/styles/_global.scss | 4 +- devserver/src/styles/_workspace.scss | 16 +- devserver/src/styles/index.scss | 18 +- devserver/src/types.ts | 6 +- 23 files changed, 335 insertions(+), 226 deletions(-) diff --git a/devserver/README.md b/devserver/README.md index b43160729..0107c4a8f 100644 --- a/devserver/README.md +++ b/devserver/README.md @@ -2,4 +2,4 @@ This server relies on [`Vite`](https://vitejs.dev) to create a server that automatically reloads when it detects file system changes. This allows Source Academy developers to make changes to their tabs without having to use the frontend, and have it render code changes live. -The server is designed to be run using `yarn devserver` from the repository's root directory, hence `vite.config.ts` is not located within this folder. \ No newline at end of file +The server is designed to be run using `yarn devserver` from the repository's root directory, hence `vite.config.ts` is not located within this folder. diff --git a/devserver/index.html b/devserver/index.html index 840263ca9..02cac35ad 100644 --- a/devserver/index.html +++ b/devserver/index.html @@ -1,4 +1,4 @@ - + diff --git a/devserver/src/components/ControlButton.tsx b/devserver/src/components/ControlButton.tsx index 4b64f6ffe..8ac2b8984 100644 --- a/devserver/src/components/ControlButton.tsx +++ b/devserver/src/components/ControlButton.tsx @@ -1,4 +1,10 @@ -import { AnchorButton, Button, Icon, type IconName, Intent } from '@blueprintjs/core'; +import { + AnchorButton, + Button, + Icon, + Intent, + type IconName +} from '@blueprintjs/core'; import React from 'react'; type ButtonOptions = { @@ -38,7 +44,9 @@ const ControlButton: React.FC = ({ ...defaultOptions, ...options }; - const iconElement = icon && ; + const iconElement = icon && ( + + ); // Refer to #2417 and #2422 for why we conditionally // set the button component. See also: // https://blueprintjs.com/docs/#core/components/button diff --git a/devserver/src/components/Playground.tsx b/devserver/src/components/Playground.tsx index 734d6a633..9aff5b75b 100644 --- a/devserver/src/components/Playground.tsx +++ b/devserver/src/components/Playground.tsx @@ -1,6 +1,16 @@ -import { Classes, Intent, OverlayToaster, type ToastProps } from '@blueprintjs/core'; +import { + Classes, + Intent, + OverlayToaster, + type ToastProps +} from '@blueprintjs/core'; import classNames from 'classnames'; -import { SourceDocumentation, getNames, runInContext, type Context } from 'js-slang'; +import { + SourceDocumentation, + getNames, + runInContext, + type Context +} from 'js-slang'; // Importing this straight from js-slang doesn't work for whatever reason import createContext from 'js-slang/dist/createContext'; @@ -42,9 +52,15 @@ const createContextHelper = () => { const Playground: React.FC<{}> = () => { const [dynamicTabs, setDynamicTabs] = React.useState([]); const [selectedTabId, setSelectedTab] = React.useState(testTabContent.id); - const [codeContext, setCodeContext] = React.useState(createContextHelper()); - const [editorValue, setEditorValue] = React.useState(localStorage.getItem('editorValue') ?? ''); - const [replOutput, setReplOutput] = React.useState(null); + const [codeContext, setCodeContext] = React.useState( + createContextHelper() + ); + const [editorValue, setEditorValue] = React.useState( + localStorage.getItem('editorValue') ?? '' + ); + const [replOutput, setReplOutput] = React.useState( + null + ); const [alerts, setAlerts] = React.useState([]); const toaster = React.useRef(null); @@ -58,87 +74,89 @@ const Playground: React.FC<{}> = () => { } }; - const getAutoComplete = useCallback((row: number, col: number, callback: any) => { - getNames(editorValue, row, col, codeContext) - .then(([editorNames, displaySuggestions]) => { - if (!displaySuggestions) { - callback(); - return; - } - - const editorSuggestions = editorNames.map((editorName: any) => ({ - ...editorName, - caption: editorName.name, - value: editorName.name, - score: editorName.score ? editorName.score + 1000 : 1000, - name: undefined - })); - - const builtins: Record = SourceDocumentation.builtins[Chapter.SOURCE_4]; - const builtinSuggestions = Object.entries(builtins) - .map(([builtin, thing]) => ({ - ...thing, - caption: builtin, - value: builtin, - score: 100, - name: builtin, - docHTML: thing.description + const getAutoComplete = useCallback( + (row: number, col: number, callback: any) => { + getNames(editorValue, row, col, codeContext).then( + ([editorNames, displaySuggestions]) => { + if (!displaySuggestions) { + callback(); + return; + } + + const editorSuggestions = editorNames.map((editorName: any) => ({ + ...editorName, + caption: editorName.name, + value: editorName.name, + score: editorName.score ? editorName.score + 1000 : 1000, + name: undefined })); - callback(null, [ - ...builtinSuggestions, - ...editorSuggestions - ]); - }); - }, [editorValue, codeContext]); + const builtins: Record = + SourceDocumentation.builtins[Chapter.SOURCE_4]; + const builtinSuggestions = Object.entries(builtins).map( + ([builtin, thing]) => ({ + ...thing, + caption: builtin, + value: builtin, + score: 100, + name: builtin, + docHTML: thing.description + }) + ); + + callback(null, [...builtinSuggestions, ...editorSuggestions]); + } + ); + }, + [editorValue, codeContext] + ); - const loadTabs = () => getDynamicTabs(codeContext) - .then((tabs) => { - setDynamicTabs(tabs); + const loadTabs = () => + getDynamicTabs(codeContext) + .then((tabs) => { + setDynamicTabs(tabs); - const newIds = tabs.map(({ id }) => id); - // If the currently selected tab no longer exists, - // switch to the default test tab - if (!newIds.includes(selectedTabId)) { - setSelectedTab(testTabContent.id); - } - setAlerts(newIds); - }) - .catch((error) => { - showToast(errorToast); - console.log(error); - }); + const newIds = tabs.map(({ id }) => id); + // If the currently selected tab no longer exists, + // switch to the default test tab + if (!newIds.includes(selectedTabId)) { + setSelectedTab(testTabContent.id); + } + setAlerts(newIds); + }) + .catch((error) => { + showToast(errorToast); + console.log(error); + }); const evalCode = () => { codeContext.errors = []; // eslint-disable-next-line no-multi-assign codeContext.moduleContexts = mockModuleContext.moduleContexts = {}; - runInContext(editorValue, codeContext) - .then((result) => { - if (codeContext.errors.length > 0) { - showToast(errorToast); - } else { - loadTabs() - .then(() => showToast(evalSuccessToast)); - } + runInContext(editorValue, codeContext).then((result) => { + if (codeContext.errors.length > 0) { + showToast(errorToast); + } else { + loadTabs().then(() => showToast(evalSuccessToast)); + } - // TODO: Add support for console.log? - if (result.status === 'finished') { - setReplOutput({ - type: 'result', - // code: editorValue, - consoleLogs: [], - value: stringify(result.value) - }); - } else if (result.status === 'error') { - setReplOutput({ - type: 'errors', - errors: codeContext.errors, - consoleLogs: [] - }); - } - }); + // TODO: Add support for console.log? + if (result.status === 'finished') { + setReplOutput({ + type: 'result', + // code: editorValue, + consoleLogs: [], + value: stringify(result.value) + }); + } else if (result.status === 'error') { + setReplOutput({ + type: 'errors', + errors: codeContext.errors, + consoleLogs: [] + }); + } + }); }; const resetEditor = () => { @@ -160,13 +178,8 @@ const Playground: React.FC<{}> = () => { controlBarProps: { editorButtons: [ , - , - + , + ] }, replProps: { @@ -182,18 +195,19 @@ const Playground: React.FC<{}> = () => { sideContentProps: { dynamicTabs: [testTabContent, ...dynamicTabs], selectedTabId, - onChange: useCallback((newId: string) => { - setSelectedTab(newId); - setAlerts(alerts.filter((id) => id !== newId)); - }, [alerts]), + onChange: useCallback( + (newId: string) => { + setSelectedTab(newId); + setAlerts(alerts.filter((id) => id !== newId)); + }, + [alerts] + ), alerts } }; return ( - + diff --git a/devserver/src/components/Workspace.tsx b/devserver/src/components/Workspace.tsx index 858f7a9f1..8d50b843b 100644 --- a/devserver/src/components/Workspace.tsx +++ b/devserver/src/components/Workspace.tsx @@ -1,5 +1,5 @@ import { FocusStyleManager } from '@blueprintjs/core'; -import { type Enable, Resizable, type ResizeCallback } from 're-resizable'; +import { Resizable, type Enable, type ResizeCallback } from 're-resizable'; import React from 'react'; import ControlBar, { type ControlBarProps } from './controlBar/ControlBar'; @@ -10,8 +10,8 @@ import { useDimensions } from './utils/Hooks'; type DispatchProps = { handleEditorEval: () => void; - handleEditorValueChange: (newValue: string) => void - handlePromptAutocomplete: (row: number, col: number, callback: any) => void + handleEditorValueChange: (newValue: string) => void; + handlePromptAutocomplete: (row: number, col: number, callback: any) => void; }; type StateProps = { @@ -21,9 +21,9 @@ type StateProps = { replProps: ReplProps; sideContentHeight?: number; sideContentIsResizeable?: boolean; - editorValue: string + editorValue: string; - sideContentProps: SideContentProps + sideContentProps: SideContentProps; }; const rightResizeOnly: Enable = { right: true }; @@ -40,7 +40,9 @@ const Workspace: React.FC = (props) => { const [contentContainerWidth] = useDimensions(contentContainerDiv); - const [sideContentHeight, setSideContentHeight] = React.useState(undefined); + const [sideContentHeight, setSideContentHeight] = React.useState< + number | undefined + >(undefined); FocusStyleManager.onlyShowFocusOnTabs(); @@ -58,8 +60,8 @@ const Workspace: React.FC = (props) => { const toggleEditorDividerDisplay: ResizeCallback = (_a, _b, ref) => { const leftThreshold = 5; const rightThreshold = 95; - const editorWidthPercentage - = ((ref as HTMLDivElement).clientWidth / contentContainerWidth) * 100; + const editorWidthPercentage = + ((ref as HTMLDivElement).clientWidth / contentContainerWidth) * 100; // update resizable size if (editorWidthPercentage > rightThreshold) { leftParentResizable.current!.updateSize({ @@ -79,8 +81,8 @@ const Workspace: React.FC = (props) => { * so that it's bottom border snaps flush with editor's bottom border */ const toggleDividerDisplay: ResizeCallback = (_a, _b, ref) => { - maxDividerHeight.current - = sideDividerDiv.current!.clientHeight > maxDividerHeight.current! + maxDividerHeight.current = + sideDividerDiv.current!.clientHeight > maxDividerHeight.current! ? sideDividerDiv.current!.clientHeight : maxDividerHeight.current; const resizableHeight = (ref as HTMLDivElement).clientHeight; @@ -125,7 +127,9 @@ const Workspace: React.FC = (props) => { className="resize-side-content" enable={bottomResizeOnly} onResize={toggleDividerDisplay} - onResizeStop={(_a, _b, ref) => setSideContentHeight(ref.clientHeight)} + onResizeStop={(_a, _b, ref) => + setSideContentHeight(ref.clientHeight) + } > = (props) => { ); const flowControl = props.flowButtons && ( -
{props.flowButtons}
+
+ {props.flowButtons} +
); const editingWorkspaceControl = ( -
+
{props.editingWorkspaceButtons}
); diff --git a/devserver/src/components/controlBar/ControlBarClearButton.tsx b/devserver/src/components/controlBar/ControlBarClearButton.tsx index adb04e941..44ce463c1 100644 --- a/devserver/src/components/controlBar/ControlBarClearButton.tsx +++ b/devserver/src/components/controlBar/ControlBarClearButton.tsx @@ -3,13 +3,15 @@ import { IconNames } from '@blueprintjs/icons'; import ControlButton from '../ControlButton'; type Props = { - onClick: () => void + onClick: () => void; }; -export const ControlBarClearButton = (props: Props) => - -; +export const ControlBarClearButton = (props: Props) => ( + + + +); diff --git a/devserver/src/components/controlBar/ControlBarRefreshButton.tsx b/devserver/src/components/controlBar/ControlBarRefreshButton.tsx index f586b8744..5ee8edb2b 100644 --- a/devserver/src/components/controlBar/ControlBarRefreshButton.tsx +++ b/devserver/src/components/controlBar/ControlBarRefreshButton.tsx @@ -3,13 +3,15 @@ import { IconNames } from '@blueprintjs/icons'; import ControlButton from '../ControlButton'; type Props = { - onClick: () => void + onClick: () => void; }; -export const ControlBarRefreshButton = (props: Props) => - -; +export const ControlBarRefreshButton = (props: Props) => ( + + + +); diff --git a/devserver/src/components/controlBar/ControlBarRunButton.tsx b/devserver/src/components/controlBar/ControlBarRunButton.tsx index e07163fd1..5fc0fbdd8 100644 --- a/devserver/src/components/controlBar/ControlBarRunButton.tsx +++ b/devserver/src/components/controlBar/ControlBarRunButton.tsx @@ -16,7 +16,9 @@ type StateProps = { type ControlButtonRunButtonProps = DispatchProps & StateProps; -export const ControlBarRunButton: React.FC = (props) => { +export const ControlBarRunButton: React.FC = ( + props +) => { const tooltipContent = 'Evaluate the program'; return ( diff --git a/devserver/src/components/editor/Editor.tsx b/devserver/src/components/editor/Editor.tsx index 3bc31c9cc..ff183ae5a 100644 --- a/devserver/src/components/editor/Editor.tsx +++ b/devserver/src/components/editor/Editor.tsx @@ -1,7 +1,7 @@ -import { type Ace, require as acequire } from 'ace-builds'; +import { require as acequire, type Ace } from 'ace-builds'; +import 'ace-builds/esm-resolver'; import 'ace-builds/src-noconflict/ext-language_tools'; import 'ace-builds/src-noconflict/ext-searchbox'; -import 'ace-builds/esm-resolver'; import 'js-slang/dist/editors/ace/theme/source'; @@ -28,13 +28,15 @@ type DispatchProps = { export type EditorStateProps = { newCursorPosition?: Position; - editorValue: string - handleEditorValueChange: (newCode: string) => void + editorValue: string; + handleEditorValueChange: (newCode: string) => void; }; export type EditorProps = DispatchProps & EditorStateProps; -const makeCompleter = (handlePromptAutocomplete: DispatchProps['handlePromptAutocomplete']) => ({ +const makeCompleter = ( + handlePromptAutocomplete: DispatchProps['handlePromptAutocomplete'] +) => ({ getCompletions( _editor: Ace.Editor, _session: Ace.EditSession, @@ -66,10 +68,13 @@ const handlers = { }; const Editor: React.FC = (props: EditorProps) => { - const reactAceRef: React.MutableRefObject = React.useRef(null); + const reactAceRef: React.MutableRefObject = + React.useRef(null); // Refs for things that technically shouldn't change... but just in case. - const handlePromptAutocompleteRef = React.useRef(props.handlePromptAutocomplete); + const handlePromptAutocompleteRef = React.useRef( + props.handlePromptAutocomplete + ); const editor = reactAceRef.current?.editor; @@ -91,10 +96,9 @@ const Editor: React.FC = (props: EditorProps) => { // The () => ref.current() are designed to use the latest instance only. // Start autocompletion - acequire('ace/ext/language_tools') - .setCompleters([ - makeCompleter((...args) => handlePromptAutocompleteRef.current(...args)) - ]); + acequire('ace/ext/language_tools').setCompleters([ + makeCompleter((...args) => handlePromptAutocompleteRef.current(...args)) + ]); }, [editor]); React.useLayoutEffect(() => { diff --git a/devserver/src/components/repl/Repl.tsx b/devserver/src/components/repl/Repl.tsx index 60e8b5f71..cd8320c29 100644 --- a/devserver/src/components/repl/Repl.tsx +++ b/devserver/src/components/repl/Repl.tsx @@ -19,7 +19,9 @@ const Output: React.FC = (props: OutputProps) => { case 'running': return ( -
{props.output.consoleLogs.join('\n')}
+
+            {props.output.consoleLogs.join('\n')}
+          
); case 'result': @@ -32,7 +34,9 @@ const Output: React.FC = (props: OutputProps) => { } return ( -
{props.output.consoleLogs.join('\n')}
+
+            {props.output.consoleLogs.join('\n')}
+          
{props.output.value}
); @@ -41,13 +45,17 @@ const Output: React.FC = (props: OutputProps) => { if (props.output.consoleLogs.length === 0) { return ( -
{parseError(props.output.errors)}
+
+              {parseError(props.output.errors)}
+            
); } return ( -
{props.output.consoleLogs.join('\n')}
+
+            {props.output.consoleLogs.join('\n')}
+          

{parseError(props.output.errors)}
@@ -69,9 +77,7 @@ export type ReplProps = { const Repl: React.FC = (props: ReplProps) => (
- {props.output === null - ? - : } + {props.output === null ? : } {/* {cards.length > 0 ? cards : ()} */}
diff --git a/devserver/src/components/sideContent/SideContent.tsx b/devserver/src/components/sideContent/SideContent.tsx index 27125198f..645cd5754 100644 --- a/devserver/src/components/sideContent/SideContent.tsx +++ b/devserver/src/components/sideContent/SideContent.tsx @@ -1,4 +1,11 @@ -import { Card, Icon, Tab, type TabProps, Tabs, Tooltip } from '@blueprintjs/core'; +import { + Card, + Icon, + Tab, + Tabs, + Tooltip, + type TabProps +} from '@blueprintjs/core'; import React from 'react'; import type { SideContentTab } from './types'; @@ -21,11 +28,11 @@ export type SideContentProps = { renderActiveTabPanelOnly?: boolean; editorWidth?: string; sideContentHeight?: number; - dynamicTabs: SideContentTab[] + dynamicTabs: SideContentTab[]; - selectedTabId: string - alerts: string[] - onChange?: (newId: string, oldId: string) => void + selectedTabId: string; + alerts: string[]; + onChange?: (newId: string, oldId: string) => void; }; const renderTab = ( @@ -37,7 +44,13 @@ const renderTab = ( const iconSize = 20; const tabTitle = ( -
+
@@ -64,7 +77,9 @@ const renderTab = ( // } // } // : tab.body; - const tabPanel: React.JSX.Element =
{tab.body}
; + const tabPanel: React.JSX.Element = ( +
{tab.body}
+ ); return ; }; @@ -89,7 +104,14 @@ const SideContent: React.FC = ({ if (onChange) onChange(newId, oldId); }} > - {dynamicTabs.map((tab) => renderTab(tab, alerts.includes(tab.id), editorWidth, sideContentHeight))} + {dynamicTabs.map((tab) => + renderTab( + tab, + alerts.includes(tab.id), + editorWidth, + sideContentHeight + ) + )}
diff --git a/devserver/src/components/sideContent/TestTab.tsx b/devserver/src/components/sideContent/TestTab.tsx index cc1fcd011..eb07aa435 100644 --- a/devserver/src/components/sideContent/TestTab.tsx +++ b/devserver/src/components/sideContent/TestTab.tsx @@ -1,26 +1,33 @@ import { IconNames } from '@blueprintjs/icons'; import type { SideContentTab } from './types'; -const TestTab = () =>
-

Source Academy Tab Development Server

-

- Run some code that imports modules in the editor on the left. You should see the corresponding module tab spawn.
- Whenever you make changes to the tab, the server should automatically reload and show the changes that you've made
- If that does not happen, you can click the refresh button to manually reload tabs -

-
; +const TestTab = () => ( +
+

Source Academy Tab Development Server

+

+ Run some code that imports modules in the editor on the left. You should + see the corresponding module tab spawn. +
+ Whenever you make changes to the tab, the server should automatically + reload and show the changes that you've made
+ If that does not happen, you can click the refresh button to manually + reload tabs +

+
+); const testTabContent: SideContentTab = { id: 'test', label: 'Welcome to the tab development server!', iconName: IconNames.LabTest, - body: + body: }; export default testTabContent; diff --git a/devserver/src/components/sideContent/types.ts b/devserver/src/components/sideContent/types.ts index c9108f5f3..b4891a7ce 100644 --- a/devserver/src/components/sideContent/types.ts +++ b/devserver/src/components/sideContent/types.ts @@ -3,19 +3,19 @@ import type { Context } from 'js-slang'; import type { JSX } from 'react'; export type DebuggerContext = { - context: Context + context: Context; }; export type SideContentTab = { - id: string - label: string - iconName: IconName - body: JSX.Element + id: string; + label: string; + iconName: IconName; + body: JSX.Element; }; export type ModuleSideContent = { label: string; - iconName: IconName - toSpawn?: (context: DebuggerContext) => boolean - body: (context: DebuggerContext) => JSX.Element + iconName: IconName; + toSpawn?: (context: DebuggerContext) => boolean; + body: (context: DebuggerContext) => JSX.Element; }; diff --git a/devserver/src/components/sideContent/utils.ts b/devserver/src/components/sideContent/utils.ts index c6e5529fe..e521c38d0 100644 --- a/devserver/src/components/sideContent/utils.ts +++ b/devserver/src/components/sideContent/utils.ts @@ -5,17 +5,25 @@ import type { ModuleSideContent, SideContentTab } from './types'; const moduleManifest = manifest as Record; export const getDynamicTabs = async (context: Context) => { - const moduleSideContents = await Promise.all(Object.keys(context.moduleContexts) - .flatMap((moduleName) => moduleManifest[moduleName].tabs.map(async (tabName) => { - const { default: rawTab } = await import(`../../../../src/tabs/${tabName}/index.tsx`); - return rawTab as ModuleSideContent; - }))); + const moduleSideContents = await Promise.all( + Object.keys(context.moduleContexts).flatMap((moduleName) => + moduleManifest[moduleName].tabs.map(async (tabName) => { + const { default: rawTab } = await import( + `../../../../src/tabs/${tabName}/index.tsx` + ); + return rawTab as ModuleSideContent; + }) + ) + ); - return moduleSideContents.filter(({ toSpawn }) => !toSpawn || toSpawn({ context })) - .map((tab): SideContentTab => ({ - ...tab, - // In the frontend, module tabs use their labels as IDs - id: tab.label, - body: tab.body({ context }) - })); + return moduleSideContents + .filter(({ toSpawn }) => !toSpawn || toSpawn({ context })) + .map( + (tab): SideContentTab => ({ + ...tab, + // In the frontend, module tabs use their labels as IDs + id: tab.label, + body: tab.body({ context }) + }) + ); }; diff --git a/devserver/src/components/utils/AceHelper.ts b/devserver/src/components/utils/AceHelper.ts index 770e07a92..8118dd531 100644 --- a/devserver/src/components/utils/AceHelper.ts +++ b/devserver/src/components/utils/AceHelper.ts @@ -1,9 +1,13 @@ /* eslint-disable new-cap */ -import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; +import { + HighlightRulesSelector, + ModeSelector +} from 'js-slang/dist/editors/ace/modes/source'; import { Chapter, Variant } from 'js-slang/dist/types'; import ace from 'react-ace'; -export const getModeString = () => `source${Chapter.SOURCE_4}${Variant.DEFAULT}${''}`; +export const getModeString = () => + `source${Chapter.SOURCE_4}${Variant.DEFAULT}${''}`; /** * This _modifies global state_ and defines a new Ace mode globally, if it does not already exist. @@ -16,10 +20,11 @@ export const selectMode = () => { const library = ''; if ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - typeof ace.define.modules[`ace/mode/${getModeString(chapter, variant, library)}`]?.Mode - === 'function' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + typeof ace.define.modules[ + `ace/mode/${getModeString(chapter, variant, library)}` + ]?.Mode === 'function' ) { return; } diff --git a/devserver/src/components/utils/Hooks.ts b/devserver/src/components/utils/Hooks.ts index 6e292e650..e14df68a7 100644 --- a/devserver/src/components/utils/Hooks.ts +++ b/devserver/src/components/utils/Hooks.ts @@ -7,21 +7,26 @@ import React, { type RefObject } from 'react'; -export const useDimensions = (ref: RefObject): [width: number, height: number] => { +export const useDimensions = ( + ref: RefObject +): [width: number, height: number] => { const [width, setWidth] = React.useState(0); const [height, setHeight] = React.useState(0); const resizeObserver = React.useMemo( - () => new ResizeObserver((entries: ResizeObserverEntry[], _observer: ResizeObserver) => { - if (entries.length !== 1) { - throw new Error( - 'Expected only a single HTML element to be observed by the ResizeObserver.' - ); - } - const contentRect = entries[0].contentRect; - setWidth(contentRect.width); - setHeight(contentRect.height); - }), + () => + new ResizeObserver( + (entries: ResizeObserverEntry[], _observer: ResizeObserver) => { + if (entries.length !== 1) { + throw new Error( + 'Expected only a single HTML element to be observed by the ResizeObserver.' + ); + } + const contentRect = entries[0].contentRect; + setWidth(contentRect.width); + setHeight(contentRect.height); + } + ), [] ); diff --git a/devserver/src/main.tsx b/devserver/src/main.tsx index 38c8f7044..26c718f89 100644 --- a/devserver/src/main.tsx +++ b/devserver/src/main.tsx @@ -1,13 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './styles/index.scss'; import Playground from './components/Playground'; +import './styles/index.scss'; -ReactDOM.render( -
-
- +ReactDOM.render( + +
+
+ +
-
-, document.getElementById('root')!); + , + document.getElementById('root')! +); diff --git a/devserver/src/styles/_application.scss b/devserver/src/styles/_application.scss index b276c81e8..ba3fa1bc8 100644 --- a/devserver/src/styles/_application.scss +++ b/devserver/src/styles/_application.scss @@ -6,7 +6,7 @@ */ html { background-size: cover; - background-image: url('#{$images-path}/academy_background.jpg'); + background-image: url("#{$images-path}/academy_background.jpg"); background-repeat: no-repeat; background-attachment: fixed; ::-webkit-scrollbar { diff --git a/devserver/src/styles/_global.scss b/devserver/src/styles/_global.scss index 5922a82b6..4b59affbc 100644 --- a/devserver/src/styles/_global.scss +++ b/devserver/src/styles/_global.scss @@ -5,8 +5,8 @@ $cadet-color-3: #34495e; $cadet-color-4: #ced9e0; $cadet-color-5: #ffffff; -$images-path: '../assets'; -$achievement-assets: 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement'; +$images-path: "../assets"; +$achievement-assets: "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement"; /* Fixes height behaviour of nested flexboxes in the code editor and REPL, diff --git a/devserver/src/styles/_workspace.scss b/devserver/src/styles/_workspace.scss index f03004779..baa53645b 100644 --- a/devserver/src/styles/_workspace.scss +++ b/devserver/src/styles/_workspace.scss @@ -144,7 +144,7 @@ $code-color-error: #ff4444; } .ace_breakpoint:before { - content: ' \25CF'; + content: " \25CF"; margin-left: -10px; color: red; } @@ -425,7 +425,10 @@ $code-color-error: #ff4444; * output. Taken from react-ace * sourcecode, font size modified. */ - font: 16px / normal 'Inconsolata', 'Consolas', monospace; + font: + 16px / normal "Inconsolata", + "Consolas", + monospace; .canvas-container { display: -webkit-box; @@ -455,7 +458,7 @@ $code-color-error: #ff4444; // Set colour of icons in blueprintjs tabs color: #a7b6c2; - &[aria-selected='true'] { + &[aria-selected="true"] { .side-content-tooltip { background-color: #495a6b; @@ -468,7 +471,7 @@ $code-color-error: #ff4444; } } - &[aria-disabled='true'] { + &[aria-disabled="true"] { .side-content-tooltip { // Set tooltip colour to always be the same as the background background-color: inherit; @@ -676,7 +679,10 @@ $code-color-error: #ff4444; * output. Taken from react-ace * sourcecode, font size modified. */ - font: 16px / normal 'Inconsolata', 'Consolas', monospace; + font: + 16px / normal "Inconsolata", + "Consolas", + monospace; } .code-output { diff --git a/devserver/src/styles/index.scss b/devserver/src/styles/index.scss index 49e5b163d..8bbc6474a 100644 --- a/devserver/src/styles/index.scss +++ b/devserver/src/styles/index.scss @@ -1,7 +1,7 @@ -@use 'sass:math'; +@use "sass:math"; -@import '@blueprintjs/core/lib/css/blueprint.css'; -@import '@blueprintjs/core/lib/scss/variables'; +@import "@blueprintjs/core/lib/css/blueprint.css"; +@import "@blueprintjs/core/lib/scss/variables"; // CSS styles for react-mde Markdown editor // (currently this editor is only used for grading comments) @@ -9,10 +9,10 @@ // styles in the preview tab of the editor, providing a more accurate // depiction of what the actual comment will look like -@import 'global'; +@import "global"; -@import 'application'; -@import 'commons'; -@import 'editorTabs'; -@import 'playground'; -@import 'workspace'; +@import "application"; +@import "commons"; +@import "editorTabs"; +@import "playground"; +@import "workspace"; diff --git a/devserver/src/types.ts b/devserver/src/types.ts index dddd7cbb2..de976f026 100644 --- a/devserver/src/types.ts +++ b/devserver/src/types.ts @@ -44,4 +44,8 @@ export type ErrorOutput = { consoleLogs: string[]; }; -export type InterpreterOutput = CodeOutput | ErrorOutput | ResultOutput | RunningOutput; +export type InterpreterOutput = + | CodeOutput + | ErrorOutput + | ResultOutput + | RunningOutput; From 98b3571d71d8ced0a806ca270452499c03a01475 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Tue, 26 Mar 2024 21:48:43 +0800 Subject: [PATCH 02/39] Reformat scripts with Prettier --- scripts/jest.setup.ts | 33 ++- scripts/scripts_manager.js | 2 +- scripts/src/build/__tests__/buildAll.test.ts | 67 ++--- .../src/build/__tests__/buildUtils.test.ts | 91 ++++--- scripts/src/build/__tests__/testingUtils.ts | 35 ++- scripts/src/build/docs/__mocks__/docsUtils.ts | 3 +- scripts/src/build/docs/__tests__/docs.test.ts | 44 ++-- scripts/src/build/docs/__tests__/json.test.ts | 28 +- scripts/src/build/docs/docsUtils.ts | 10 +- scripts/src/build/docs/docsreadme.md | 9 +- scripts/src/build/docs/drawdown.ts | 8 +- scripts/src/build/docs/html.ts | 111 ++++---- scripts/src/build/docs/index.ts | 48 ++-- scripts/src/build/docs/json.ts | 74 ++++-- scripts/src/build/index.ts | 43 ++- .../build/modules/__tests__/bundle.test.ts | 32 ++- .../build/modules/__tests__/output.test.ts | 37 +-- .../build/modules/__tests__/streamMocker.ts | 2 +- .../src/build/modules/__tests__/tab.test.ts | 29 +- scripts/src/build/modules/bundles.ts | 71 +++-- scripts/src/build/modules/commons.ts | 26 +- scripts/src/build/modules/index.ts | 15 +- scripts/src/build/modules/tabs.ts | 70 +++-- scripts/src/build/prebuild/__mocks__/lint.ts | 15 +- scripts/src/build/prebuild/__mocks__/tsc.ts | 15 +- .../build/prebuild/__tests__/prebuild.test.ts | 62 ++--- scripts/src/build/prebuild/index.ts | 17 +- scripts/src/build/prebuild/lint.ts | 108 +++++--- scripts/src/build/prebuild/tsc.ts | 153 +++++++---- scripts/src/build/prebuild/utils.ts | 19 +- scripts/src/build/utils.ts | 249 ++++++++++++------ scripts/src/commandUtils.ts | 88 ++++--- scripts/src/manifest.ts | 5 +- .../src/templates/__tests__/create.test.ts | 92 +++---- scripts/src/templates/__tests__/names.test.ts | 6 +- scripts/src/templates/index.ts | 5 +- scripts/src/templates/module.ts | 30 ++- scripts/src/templates/print.ts | 11 +- scripts/src/templates/tab.ts | 12 +- scripts/src/templates/templates/__bundle__.ts | 1 - scripts/src/templates/templates/__tab__.tsx | 6 +- scripts/src/testing/__tests__/runner.test.ts | 16 +- scripts/src/testing/index.ts | 40 +-- scripts/tsconfig.json | 2 +- 44 files changed, 1076 insertions(+), 764 deletions(-) diff --git a/scripts/jest.setup.ts b/scripts/jest.setup.ts index 94c883175..cdf79e6b5 100644 --- a/scripts/jest.setup.ts +++ b/scripts/jest.setup.ts @@ -2,9 +2,16 @@ const chalkFunction = new Proxy((x: string) => x, { get: () => chalkFunction }); -jest.mock('chalk', () => new Proxy({}, { - get: () => chalkFunction -})); +jest.mock( + 'chalk', + () => + new Proxy( + {}, + { + get: () => chalkFunction + } + ) +); jest.mock('fs/promises', () => ({ copyFile: jest.fn(() => Promise.resolve()), @@ -14,15 +21,17 @@ jest.mock('fs/promises', () => ({ })); jest.mock('./src/manifest', () => ({ - retrieveManifest: jest.fn(() => Promise.resolve({ - test0: { - tabs: ['tab0'] - }, - test1: { tabs: [] }, - test2: { - tabs: ['tab1'] - } - })) + retrieveManifest: jest.fn(() => + Promise.resolve({ + test0: { + tabs: ['tab0'] + }, + test1: { tabs: [] }, + test2: { + tabs: ['tab1'] + } + }) + ) })); global.process.exit = jest.fn(code => { diff --git a/scripts/scripts_manager.js b/scripts/scripts_manager.js index 412c7e372..c807df7fd 100644 --- a/scripts/scripts_manager.js +++ b/scripts/scripts_manager.js @@ -3,9 +3,9 @@ import { build as esbuild } from 'esbuild' import jest from 'jest' import _ from 'lodash' import pathlib from 'path' +import { pathsToModuleNameMapper } from 'ts-jest' import { fileURLToPath } from 'url' import tsconfig from './tsconfig.json' with { type: 'json' } -import { pathsToModuleNameMapper } from 'ts-jest' function cjsDirname(url) { return pathlib.join(pathlib.dirname(fileURLToPath(url))) diff --git a/scripts/src/build/__tests__/buildAll.test.ts b/scripts/src/build/__tests__/buildAll.test.ts index fc7ac69ed..c6affe8ab 100644 --- a/scripts/src/build/__tests__/buildAll.test.ts +++ b/scripts/src/build/__tests__/buildAll.test.ts @@ -12,8 +12,7 @@ jest.mock('../prebuild/lint'); jest.mock('../docs/docsUtils'); jest.mock('esbuild', () => ({ - build: jest.fn() - .mockResolvedValue({ outputFiles: [] }) + build: jest.fn().mockResolvedValue({ outputFiles: [] }) })); jest.spyOn(jsonModule, 'buildJsons'); @@ -21,63 +20,55 @@ jest.spyOn(htmlModule, 'buildHtml'); jest.spyOn(tabsModule, 'bundleTabs'); jest.spyOn(bundleModule, 'bundleBundles'); -const asMock = any>(func: T) => func as MockedFunction; -const runCommand = (...args: string[]) => getBuildAllCommand() - .parseAsync(args, { from: 'user' }); +const asMock = any>(func: T) => + func as MockedFunction; +const runCommand = (...args: string[]) => + getBuildAllCommand().parseAsync(args, { from: 'user' }); describe('test build all command', () => { - testBuildCommand( - 'buildAll', - getBuildAllCommand, - [ - jsonModule.buildJsons, - htmlModule.buildHtml, - tabsModule.bundleTabs, - bundleModule.bundleBundles - ] - ); + testBuildCommand('buildAll', getBuildAllCommand, [ + jsonModule.buildJsons, + htmlModule.buildHtml, + tabsModule.bundleTabs, + bundleModule.bundleBundles + ]); it('should exit with code 1 if buildJsons returns with an error', async () => { - asMock(jsonModule.buildJsons) - .mockResolvedValueOnce({ - jsons: [{ + asMock(jsonModule.buildJsons).mockResolvedValueOnce({ + jsons: [ + { severity: 'error', name: 'test0', error: {} - }] - }); + } + ] + }); try { await runCommand(); } catch (error) { - expect(error) - .toEqual(new Error('process.exit called with 1')); + expect(error).toEqual(new Error('process.exit called with 1')); } - expect(process.exit) - .toHaveBeenCalledWith(1); + expect(process.exit).toHaveBeenCalledWith(1); }); it('should exit with code 1 if buildHtml returns with an error', async () => { - asMock(htmlModule.buildHtml) - .mockResolvedValueOnce({ - elapsed: 0, - result: { - severity: 'error', - error: {} - } - }); + asMock(htmlModule.buildHtml).mockResolvedValueOnce({ + elapsed: 0, + result: { + severity: 'error', + error: {} + } + }); try { await runCommand(); } catch (error) { - expect(error) - .toEqual(new Error('process.exit called with 1')); + expect(error).toEqual(new Error('process.exit called with 1')); } - expect(process.exit) - .toHaveBeenCalledWith(1); + expect(process.exit).toHaveBeenCalledWith(1); - expect(htmlModule.buildHtml) - .toHaveBeenCalledTimes(1); + expect(htmlModule.buildHtml).toHaveBeenCalledTimes(1); }); }); diff --git a/scripts/src/build/__tests__/buildUtils.test.ts b/scripts/src/build/__tests__/buildUtils.test.ts index b60835847..83b7d1d54 100644 --- a/scripts/src/build/__tests__/buildUtils.test.ts +++ b/scripts/src/build/__tests__/buildUtils.test.ts @@ -1,9 +1,14 @@ import { retrieveBundlesAndTabs } from '@src/commandUtils'; -type TestCase = [desc: string, { - bundles?: string[] | null - tabs?: string[] | null -}, boolean, Awaited>]; +type TestCase = [ + desc: string, + { + bundles?: string[] | null; + tabs?: string[] | null; + }, + boolean, + Awaited> +]; const testCases: TestCase[] = [ [ @@ -105,41 +110,57 @@ const testCases: TestCase[] = [ ]; describe('Test retrieveBundlesAndTabs', () => { - test.each(testCases)('%#. %s:', async (_, inputs, shouldAddModuleTabs, expected) => { - const outputs = await retrieveBundlesAndTabs({ - ...inputs, - manifest: 'modules.json' - }, shouldAddModuleTabs); - expect(outputs) - .toMatchObject(expected); - }); + test.each(testCases)( + '%#. %s:', + async (_, inputs, shouldAddModuleTabs, expected) => { + const outputs = await retrieveBundlesAndTabs( + { + ...inputs, + manifest: 'modules.json' + }, + shouldAddModuleTabs + ); + expect(outputs).toMatchObject(expected); + } + ); - it('should throw an exception when encountering unknown modules or tabs', () => Promise.all([ - expect(retrieveBundlesAndTabs({ - manifest: '', - bundles: ['random'], - tabs: null - }, true)).rejects.toMatchObject(new Error('Unknown bundles: random')), + it('should throw an exception when encountering unknown modules or tabs', () => + Promise.all([ + expect( + retrieveBundlesAndTabs( + { + manifest: '', + bundles: ['random'], + tabs: null + }, + true + ) + ).rejects.toMatchObject(new Error('Unknown bundles: random')), - expect(retrieveBundlesAndTabs({ - manifest: '', - bundles: [], - tabs: ['random1', 'random2'] - }, false)).rejects.toMatchObject(new Error('Unknown tabs: random1, random2')) - ])); + expect( + retrieveBundlesAndTabs( + { + manifest: '', + bundles: [], + tabs: ['random1', 'random2'] + }, + false + ) + ).rejects.toMatchObject(new Error('Unknown tabs: random1, random2')) + ])); it('should always return unique modules and tabs', async () => { - const result = await retrieveBundlesAndTabs({ - manifest: '', - bundles: ['test0', 'test0'], - tabs: ['tab0'] - }, false); + const result = await retrieveBundlesAndTabs( + { + manifest: '', + bundles: ['test0', 'test0'], + tabs: ['tab0'] + }, + false + ); - expect(result.bundles) - .toEqual(['test0']); - expect(result.modulesSpecified) - .toBe(true); - expect(result.tabs) - .toEqual(['tab0']); + expect(result.bundles).toEqual(['test0']); + expect(result.modulesSpecified).toBe(true); + expect(result.tabs).toEqual(['tab0']); }); }); diff --git a/scripts/src/build/__tests__/testingUtils.ts b/scripts/src/build/__tests__/testingUtils.ts index d052dcd87..41aa64c52 100644 --- a/scripts/src/build/__tests__/testingUtils.ts +++ b/scripts/src/build/__tests__/testingUtils.ts @@ -17,13 +17,13 @@ export function testBuildCommand( mockedFunctions: MockedFunction[] ) { function expectToBeCalled(times: number) { - mockedFunctions.forEach((func) => expect(func) - .toHaveBeenCalledTimes(times)); + mockedFunctions.forEach((func) => + expect(func).toHaveBeenCalledTimes(times) + ); } function runCommand(...args: string[]) { - return commandGetter() - .parseAsync(args, { from: 'user' }); + return commandGetter().parseAsync(args, { from: 'user' }); } test(`${commandName} should run tsc when --tsc is specified`, async () => { @@ -36,8 +36,7 @@ export function testBuildCommand( }); await runCommand('--tsc'); - expect(tsc.runTsc) - .toHaveBeenCalledTimes(1); + expect(tsc.runTsc).toHaveBeenCalledTimes(1); expectToBeCalled(1); }); @@ -50,12 +49,11 @@ export function testBuildCommand( } }); - await expect(runCommand('--tsc')) - .rejects - .toMatchInlineSnapshot('[Error: process.exit called with 1]'); + await expect(runCommand('--tsc')).rejects.toMatchInlineSnapshot( + '[Error: process.exit called with 1]' + ); - expect(tsc.runTsc) - .toHaveBeenCalledTimes(1); + expect(tsc.runTsc).toHaveBeenCalledTimes(1); expectToBeCalled(0); }); @@ -68,8 +66,7 @@ export function testBuildCommand( } }); await runCommand('--lint'); - expect(lint.runEslint) - .toHaveBeenCalledTimes(1); + expect(lint.runEslint).toHaveBeenCalledTimes(1); expectToBeCalled(1); }); @@ -82,19 +79,17 @@ export function testBuildCommand( } }); - await expect(runCommand('--lint')) - .rejects - .toMatchInlineSnapshot('[Error: process.exit called with 1]'); + await expect(runCommand('--lint')).rejects.toMatchInlineSnapshot( + '[Error: process.exit called with 1]' + ); - expect(lint.runEslint) - .toHaveBeenCalledTimes(1); + expect(lint.runEslint).toHaveBeenCalledTimes(1); expectToBeCalled(0); }); test(`${commandName} should copy the manifest if there are no errors`, async () => { await runCommand(); expectToBeCalled(1); - expect(fs.copyFile) - .toHaveBeenCalledTimes(1); + expect(fs.copyFile).toHaveBeenCalledTimes(1); }); } diff --git a/scripts/src/build/docs/__mocks__/docsUtils.ts b/scripts/src/build/docs/__mocks__/docsUtils.ts index 8532cf4da..e74da0b3d 100644 --- a/scripts/src/build/docs/__mocks__/docsUtils.ts +++ b/scripts/src/build/docs/__mocks__/docsUtils.ts @@ -7,8 +7,7 @@ export const initTypedoc = jest.fn(() => { } as any; const app = { - convert: jest.fn() - .mockReturnValue(proj), + convert: jest.fn().mockReturnValue(proj), generateDocs: jest.fn(() => Promise.resolve()) }; diff --git a/scripts/src/build/docs/__tests__/docs.test.ts b/scripts/src/build/docs/__tests__/docs.test.ts index 5af72a20c..7f905872b 100644 --- a/scripts/src/build/docs/__tests__/docs.test.ts +++ b/scripts/src/build/docs/__tests__/docs.test.ts @@ -9,42 +9,40 @@ jest.mock('../docsUtils'); jest.spyOn(json, 'buildJsons'); jest.spyOn(html, 'buildHtml'); -const asMock = any>(func: T) => func as MockedFunction; +const asMock = any>(func: T) => + func as MockedFunction; const mockBuildJson = asMock(json.buildJsons); -const runCommand = (...args: string[]) => getBuildDocsCommand() - .parseAsync(args, { from: 'user' }); +const runCommand = (...args: string[]) => + getBuildDocsCommand().parseAsync(args, { from: 'user' }); describe('test the docs command', () => { - testBuildCommand( - 'buildDocs', - getBuildDocsCommand, - [json.buildJsons, html.buildHtml] - ); + testBuildCommand('buildDocs', getBuildDocsCommand, [ + json.buildJsons, + html.buildHtml + ]); it('should only build the documentation for specified modules', async () => { await runCommand('-b', 'test0', 'test1'); - expect(json.buildJsons) - .toHaveBeenCalledTimes(1); + expect(json.buildJsons).toHaveBeenCalledTimes(1); const buildJsonCall = mockBuildJson.mock.calls[0]; - expect(buildJsonCall[0]) - .toEqual({ - bundles: ['test0', 'test1'], - tabs: [], - modulesSpecified: true - }); - - expect(html.buildHtml) - .toHaveBeenCalledTimes(1); - - expect(html.buildHtml) - .toReturnWith(Promise.resolve({ + expect(buildJsonCall[0]).toEqual({ + bundles: ['test0', 'test1'], + tabs: [], + modulesSpecified: true + }); + + expect(html.buildHtml).toHaveBeenCalledTimes(1); + + expect(html.buildHtml).toReturnWith( + Promise.resolve({ elapsed: 0, result: { severity: 'warn' } - })); + }) + ); }); }); diff --git a/scripts/src/build/docs/__tests__/json.test.ts b/scripts/src/build/docs/__tests__/json.test.ts index df85d1cc8..7565b2852 100644 --- a/scripts/src/build/docs/__tests__/json.test.ts +++ b/scripts/src/build/docs/__tests__/json.test.ts @@ -7,38 +7,30 @@ jest.spyOn(json, 'buildJsons'); jest.mock('../docsUtils'); const mockBuildJson = json.buildJsons as MockedFunction; -const runCommand = (...args: string[]) => json.getBuildJsonsCommand() - .parseAsync(args, { from: 'user' }); +const runCommand = (...args: string[]) => + json.getBuildJsonsCommand().parseAsync(args, { from: 'user' }); // TODO Figure out why expect(json.buildJsons).toHaveBeenCalledTimes is always 0 describe.skip('test json command', () => { - testBuildCommand( - 'buildJsons', - json.getBuildJsonsCommand, - [json.buildJsons] - ); + testBuildCommand('buildJsons', json.getBuildJsonsCommand, [json.buildJsons]); test('normal function', async () => { await runCommand(); - expect(fs.mkdir) - .toBeCalledWith('build/jsons', { recursive: true }); + expect(fs.mkdir).toBeCalledWith('build/jsons', { recursive: true }); - expect(json.buildJsons) - .toHaveBeenCalledTimes(1); + expect(json.buildJsons).toHaveBeenCalledTimes(1); }); it('should only build the jsons for specified modules', async () => { await runCommand('-b', 'test0', 'test1'); - expect(json.buildJsons) - .toHaveBeenCalledTimes(1); + expect(json.buildJsons).toHaveBeenCalledTimes(1); const buildJsonCall = mockBuildJson.mock.calls[0]; - expect(buildJsonCall[1]) - .toMatchObject({ - outDir: 'build', - bundles: ['test0', 'test1'] - }); + expect(buildJsonCall[1]).toMatchObject({ + outDir: 'build', + bundles: ['test0', 'test1'] + }); }); }); diff --git a/scripts/src/build/docs/docsUtils.ts b/scripts/src/build/docs/docsUtils.ts index 9ecc6edc3..df222b961 100644 --- a/scripts/src/build/docs/docsUtils.ts +++ b/scripts/src/build/docs/docsUtils.ts @@ -1,7 +1,11 @@ import * as td from 'typedoc'; import { expandBundleNames } from '../utils'; -export async function initTypedoc(bundles: string[], srcDir: string, verbose: boolean) { +export async function initTypedoc( + bundles: string[], + srcDir: string, + verbose: boolean +) { const app = await td.Application.bootstrap({ categorizeByGroup: true, entryPoints: expandBundleNames(srcDir, bundles), @@ -17,7 +21,9 @@ export async function initTypedoc(bundles: string[], srcDir: string, verbose: bo app.options.addReader(new td.TSConfigReader()); const project = await app.convert(); if (!project) { - throw new Error('Failed to initialize typedoc - Make sure to check that the source files have no compilation errors!'); + throw new Error( + 'Failed to initialize typedoc - Make sure to check that the source files have no compilation errors!' + ); } return [project, app] as [td.ProjectReflection, td.Application]; } diff --git a/scripts/src/build/docs/docsreadme.md b/scripts/src/build/docs/docsreadme.md index 0adf8f5be..79538a4fd 100644 --- a/scripts/src/build/docs/docsreadme.md +++ b/scripts/src/build/docs/docsreadme.md @@ -1,18 +1,23 @@ # Overview The Source Academy allows programmers to import functions and constants from a module, using JavaScript's `import` directive. For example, the programmer may decide to import the function `thrice` from the module `repeat` by starting the program with + ``` import { thrice } from "repeat"; ``` -When evaluating such a directive, the Source Academy looks for a module with the matching name, here `repeat`, in a preconfigured modules site. The Source Academy at https://sourceacademy.org uses the default modules site (located at https://source-academy.github.io/modules). +When evaluating such a directive, the Source Academy looks for a module with the matching name, here `repeat`, in a preconfigured modules site. The Source Academy at uses the default modules site (located at ). After importing functions or constants from a module, they can be used as usual. + ``` thrice(display)(8); // displays 8 three times ``` + if `thrice` is declared in the module `repeat` as follows: + ``` const thrice = f => x => f(f(f(x))); ``` -[List of modules](modules.html) available at the default modules site. \ No newline at end of file + +[List of modules](modules.html) available at the default modules site. diff --git a/scripts/src/build/docs/drawdown.ts b/scripts/src/build/docs/drawdown.ts index 8fe5d5aaa..cb55d3afc 100644 --- a/scripts/src/build/docs/drawdown.ts +++ b/scripts/src/build/docs/drawdown.ts @@ -12,9 +12,11 @@ export default (src: string): string => { var rx_escape = /\\([\\\|`*_{}\[\]()#+\-~])/g; var rx_hr = /^([*\-=_] *){3,}$/gm; var rx_blockquote = /\n *> *([^]*?)(?=(\n|$){2})/g; - var rx_list = /\n( *)(?:[*\-+]|((\d+)|([a-z])|[A-Z])[.)]) +([^]*?)(?=(\n|$){2})/g; + var rx_list = + /\n( *)(?:[*\-+]|((\d+)|([a-z])|[A-Z])[.)]) +([^]*?)(?=(\n|$){2})/g; var rx_listjoin = /<\/(ol|ul)>\n\n<\1>/g; - var rx_highlight = /(^|[^A-Za-z\d\\])(([*_])|(~)|(\^)|(--)|(\+\+)|`)(\2?)([^<]*?)\2\8(?!\2)(?=\W|_|$)/g; + var rx_highlight = + /(^|[^A-Za-z\d\\])(([*_])|(~)|(\^)|(--)|(\+\+)|`)(\2?)([^<]*?)\2\8(?!\2)(?=\W|_|$)/g; var rx_code = /\n((```|~~~).*\n?([^]*?)\n?\2|(( {4}.*?\n)+))/g; var rx_link = /((!?)\[(.*?)\]\((.*?)( ".*")?\)|\\([\\`*_{}\[\]()#+\-.!~]))/g; var rx_table = /\n(( *\|.*?\| *\n)+)/g; @@ -187,4 +189,4 @@ export default (src: string): string => { }); return src.trim(); -}; \ No newline at end of file +}; diff --git a/scripts/src/build/docs/html.ts b/scripts/src/build/docs/html.ts index fa4e8a336..4135f879c 100644 --- a/scripts/src/build/docs/html.ts +++ b/scripts/src/build/docs/html.ts @@ -1,61 +1,84 @@ import { Command } from '@commander-js/extra-typings'; import chalk from 'chalk'; -import { manifestOption, outDirOption, retrieveBundlesAndTabs, srcDirOption, wrapWithTimer } from '@src/commandUtils'; -import type { BuildInputs, AwaitedReturn } from '../utils'; +import { + manifestOption, + outDirOption, + retrieveBundlesAndTabs, + srcDirOption, + wrapWithTimer +} from '@src/commandUtils'; +import type { AwaitedReturn, BuildInputs } from '../utils'; import { initTypedoc, type TypedocResult } from './docsUtils'; -export type HtmlResult = { - severity: 'error' | 'warn' - error: any -} | { - severity: 'success' -}; - -export const buildHtml = wrapWithTimer(async ( - inputs: BuildInputs, - outDir: string, - [project, app]: TypedocResult -): Promise => { - if (inputs.modulesSpecified) { - return { - severity: 'warn', - error: 'Not all modules were built, skipping building HTML documentation' - }; +export type HtmlResult = + | { + severity: 'error' | 'warn'; + error: any; } + | { + severity: 'success'; + }; + +export const buildHtml = wrapWithTimer( + async ( + inputs: BuildInputs, + outDir: string, + [project, app]: TypedocResult + ): Promise => { + if (inputs.modulesSpecified) { + return { + severity: 'warn', + error: + 'Not all modules were built, skipping building HTML documentation' + }; + } - try { - await app.generateDocs(project, `${outDir}/documentation`); - return { - severity: 'success' - }; - } catch (error) { - return { - severity: 'error', - error - }; + try { + await app.generateDocs(project, `${outDir}/documentation`); + return { + severity: 'success' + }; + } catch (error) { + return { + severity: 'error', + error + }; + } } -}); +); -export function htmlLogger({ result, elapsed }: AwaitedReturn) { +export function htmlLogger({ + result, + elapsed +}: AwaitedReturn) { const timeStr = `${(elapsed / 1000).toFixed(2)}s`; switch (result.severity) { case 'success': - return `${chalk.cyanBright('Built HTML documentation')} ${chalk.greenBright('successfully')} in ${timeStr}`; + return `${chalk.cyanBright( + 'Built HTML documentation' + )} ${chalk.greenBright('successfully')} in ${timeStr}`; case 'warn': return chalk.yellowBright(result.error); case 'error': - return `${chalk.redBright('Failed')} ${chalk.cyanBright('to build HTML documentation: ')} ${result.error}`; + return `${chalk.redBright('Failed')} ${chalk.cyanBright( + 'to build HTML documentation: ' + )} ${result.error}`; } } -export const getBuildHtmlCommand = () => new Command('html') - .addOption(srcDirOption) - .addOption(outDirOption) - .addOption(manifestOption) - .option('-v, --verbose') - .action(async opts => { - const inputs = await retrieveBundlesAndTabs({ ...opts, tabs: [] }, false); - const tdResult = await initTypedoc(inputs.bundles, opts.srcDir, opts.verbose); - const result = await buildHtml(inputs, opts.outDir, tdResult); - console.log(htmlLogger(result)); - }); +export const getBuildHtmlCommand = () => + new Command('html') + .addOption(srcDirOption) + .addOption(outDirOption) + .addOption(manifestOption) + .option('-v, --verbose') + .action(async opts => { + const inputs = await retrieveBundlesAndTabs({ ...opts, tabs: [] }, false); + const tdResult = await initTypedoc( + inputs.bundles, + opts.srcDir, + opts.verbose + ); + const result = await buildHtml(inputs, opts.outDir, tdResult); + console.log(htmlLogger(result)); + }); diff --git a/scripts/src/build/docs/index.ts b/scripts/src/build/docs/index.ts index 007d23aa8..6f82da719 100644 --- a/scripts/src/build/docs/index.ts +++ b/scripts/src/build/docs/index.ts @@ -1,12 +1,21 @@ import { bundlesOption } from '@src/commandUtils'; -import { createBuildCommand, type BuildInputs, createBuildCommandHandler, type AwaitedReturn } from '../utils'; +import { + createBuildCommand, + createBuildCommandHandler, + type AwaitedReturn, + type BuildInputs +} from '../utils'; import { initTypedoc, type TypedocResult } from './docsUtils'; import { buildHtml } from './html'; import { buildJsons } from './json'; -export async function buildDocs(inputs: BuildInputs, outDir: string, tdResult: TypedocResult): Promise< +export async function buildDocs( + inputs: BuildInputs, + outDir: string, + tdResult: TypedocResult +): Promise< AwaitedReturn & { html: AwaitedReturn } -> { + > { const [jsonsResult, htmlResult] = await Promise.all([ buildJsons(inputs, outDir, tdResult[0]), buildHtml(inputs, outDir, tdResult) @@ -18,22 +27,25 @@ export async function buildDocs(inputs: BuildInputs, outDir: string, tdResult: T }; } -const docsCommandHandler = createBuildCommandHandler(async (inputs, { srcDir, outDir, verbose }) => { - const tdResult = await initTypedoc(inputs.bundles, srcDir, verbose); - return buildDocs(inputs, outDir, tdResult); -}, false); +const docsCommandHandler = createBuildCommandHandler( + async (inputs, { srcDir, outDir, verbose }) => { + const tdResult = await initTypedoc(inputs.bundles, srcDir, verbose); + return buildDocs(inputs, outDir, tdResult); + }, + false +); -export const getBuildDocsCommand = () => createBuildCommand( - 'docs', - 'Build HTML and json documentation' -) - .addOption(bundlesOption) - .action(opts => docsCommandHandler({ - ...opts, - tabs: [] - })); +export const getBuildDocsCommand = () => + createBuildCommand('docs', 'Build HTML and json documentation') + .addOption(bundlesOption) + .action(opts => + docsCommandHandler({ + ...opts, + tabs: [] + }) + ); -export { getBuildJsonsCommand } from './json'; export { getBuildHtmlCommand } from './html'; +export { getBuildJsonsCommand } from './json'; -export { buildJsons, buildHtml }; +export { buildHtml, buildJsons }; diff --git a/scripts/src/build/docs/json.ts b/scripts/src/build/docs/json.ts index 1bb64e2b2..12b506539 100644 --- a/scripts/src/build/docs/json.ts +++ b/scripts/src/build/docs/json.ts @@ -1,7 +1,12 @@ import fs from 'fs/promises'; import * as td from 'typedoc'; import { bundlesOption } from '@src/commandUtils'; -import { createBuildCommand, createBuildCommandHandler, type BuildInputs, type OperationResult } from '../utils'; +import { + createBuildCommand, + createBuildCommandHandler, + type BuildInputs, + type OperationResult +} from '../utils'; import { initTypedoc } from './docsUtils'; import drawdown from './drawdown'; @@ -14,13 +19,16 @@ const parsers = { let description: string; if (signature.comment) { - description = drawdown(signature.comment.summary.map(({ text }) => text) - .join('')); + description = drawdown( + signature.comment.summary.map(({ text }) => text).join('') + ); } else { description = 'No description available'; } - const params = signature.parameters.map(({ type, name }) => [name, typeToName(type)] as [string, string]); + const params = signature.parameters.map( + ({ type, name }) => [name, typeToName(type)] as [string, string] + ); return { kind: 'function', @@ -33,8 +41,9 @@ const parsers = { [td.ReflectionKind.Variable](obj) { let description: string; if (obj.comment) { - description = drawdown(obj.comment.summary.map(({ text }) => text) - .join('')); + description = drawdown( + obj.comment.summary.map(({ text }) => text).join('') + ); } else { description = 'No description available'; } @@ -46,21 +55,28 @@ const parsers = { type: typeToName(obj.type) }; } -} satisfies Partial any>>; - -async function buildJson(name: string, reflection: td.DeclarationReflection, outDir: string): Promise { +} satisfies Partial< + Record any> +>; + +async function buildJson( + name: string, + reflection: td.DeclarationReflection, + outDir: string +): Promise { try { const jsonData = reflection.children.reduce((res, element) => { const parser = parsers[element.kind]; return { ...res, - [element.name]: parser - ? parser(element) - : { kind: 'unknown' } + [element.name]: parser ? parser(element) : { kind: 'unknown' } }; }, {}); - await fs.writeFile(`${outDir}/jsons/${name}.json`, JSON.stringify(jsonData, null, 2)); + await fs.writeFile( + `${outDir}/jsons/${name}.json`, + JSON.stringify(jsonData, null, 2) + ); return { name, @@ -95,22 +111,30 @@ export async function buildJsons( }; } - const results = await Promise.all(bundles.map(bundle => buildJson( - bundle, - project.getChildByName(bundle) as td.DeclarationReflection, - outDir - ))); + const results = await Promise.all( + bundles.map(bundle => + buildJson( + bundle, + project.getChildByName(bundle) as td.DeclarationReflection, + outDir + ) + ) + ); return { jsons: results }; } -const jsonCommandHandler = createBuildCommandHandler(async (inputs, { srcDir, outDir, verbose }) => { - const [project] = await initTypedoc(inputs.bundles, srcDir, verbose); - return buildJsons(inputs, outDir, project); -}, false); +const jsonCommandHandler = createBuildCommandHandler( + async (inputs, { srcDir, outDir, verbose }) => { + const [project] = await initTypedoc(inputs.bundles, srcDir, verbose); + return buildJsons(inputs, outDir, project); + }, + false +); -export const getBuildJsonsCommand = () => createBuildCommand('jsons', 'Build json documentation') - .addOption(bundlesOption) - .action(opts => jsonCommandHandler({ ...opts, tabs: [] })); +export const getBuildJsonsCommand = () => + createBuildCommand('jsons', 'Build json documentation') + .addOption(bundlesOption) + .action(opts => jsonCommandHandler({ ...opts, tabs: [] })); diff --git a/scripts/src/build/index.ts b/scripts/src/build/index.ts index 9d9fe936c..727e49d82 100644 --- a/scripts/src/build/index.ts +++ b/scripts/src/build/index.ts @@ -1,9 +1,22 @@ import { Command } from '@commander-js/extra-typings'; import { bundlesOption, tabsOption } from '@src/commandUtils'; -import { buildDocs, getBuildDocsCommand, getBuildHtmlCommand, getBuildJsonsCommand } from './docs'; +import { + buildDocs, + getBuildDocsCommand, + getBuildHtmlCommand, + getBuildJsonsCommand +} from './docs'; import { initTypedoc } from './docs/docsUtils'; -import { buildModules, getBuildBundlesCommand, getBuildTabsCommand } from './modules'; -import { createBuildCommand, type BuildTask, createBuildCommandHandler } from './utils'; +import { + buildModules, + getBuildBundlesCommand, + getBuildTabsCommand +} from './modules'; +import { + createBuildCommand, + createBuildCommandHandler, + type BuildTask +} from './utils'; const buildAll: BuildTask = async (inputs, opts) => { const tdResult = await initTypedoc(inputs.bundles, opts.srcDir, opts.verbose); @@ -20,17 +33,19 @@ const buildAll: BuildTask = async (inputs, opts) => { }; const buildAllCommandHandler = createBuildCommandHandler(buildAll, true); -const getBuildAllCommand = () => createBuildCommand('all', 'Build bundles and tabs and documentation') - .addOption(bundlesOption) - .addOption(tabsOption) - .action(buildAllCommandHandler); +const getBuildAllCommand = () => + createBuildCommand('all', 'Build bundles and tabs and documentation') + .addOption(bundlesOption) + .addOption(tabsOption) + .action(buildAllCommandHandler); -const getBuildCommand = () => new Command('build') - .addCommand(getBuildAllCommand(), { isDefault: true }) - .addCommand(getBuildBundlesCommand()) - .addCommand(getBuildDocsCommand()) - .addCommand(getBuildHtmlCommand()) - .addCommand(getBuildJsonsCommand()) - .addCommand(getBuildTabsCommand()); +const getBuildCommand = () => + new Command('build') + .addCommand(getBuildAllCommand(), { isDefault: true }) + .addCommand(getBuildBundlesCommand()) + .addCommand(getBuildDocsCommand()) + .addCommand(getBuildHtmlCommand()) + .addCommand(getBuildJsonsCommand()) + .addCommand(getBuildTabsCommand()); export default getBuildCommand; diff --git a/scripts/src/build/modules/__tests__/bundle.test.ts b/scripts/src/build/modules/__tests__/bundle.test.ts index ea917ec23..dc0e0b643 100644 --- a/scripts/src/build/modules/__tests__/bundle.test.ts +++ b/scripts/src/build/modules/__tests__/bundle.test.ts @@ -5,28 +5,26 @@ import * as bundles from '../bundles'; jest.spyOn(bundles, 'bundleBundles'); jest.mock('esbuild', () => ({ - build: jest.fn() - .mockResolvedValue({ outputFiles: [] }) + build: jest.fn().mockResolvedValue({ outputFiles: [] }) })); -testBuildCommand( - 'buildBundles', - bundles.getBuildBundlesCommand, - [bundles.bundleBundles] -); +testBuildCommand('buildBundles', bundles.getBuildBundlesCommand, [ + bundles.bundleBundles +]); test('Normal command', async () => { - await bundles.getBuildBundlesCommand() + await bundles + .getBuildBundlesCommand() .parseAsync(['-b', 'test0'], { from: 'user' }); - expect(bundles.bundleBundles) - .toHaveBeenCalledTimes(1); + expect(bundles.bundleBundles).toHaveBeenCalledTimes(1); - const [args] = (bundles.bundleBundles as MockedFunction).mock.calls[0]; - expect(args) - .toMatchObject({ - bundles: ['test0'], - tabs: ['tab0'], - modulesSpecified: true - }); + const [args] = ( + bundles.bundleBundles as MockedFunction + ).mock.calls[0]; + expect(args).toMatchObject({ + bundles: ['test0'], + tabs: ['tab0'], + modulesSpecified: true + }); }); diff --git a/scripts/src/build/modules/__tests__/output.test.ts b/scripts/src/build/modules/__tests__/output.test.ts index 6849a7aba..1534924af 100644 --- a/scripts/src/build/modules/__tests__/output.test.ts +++ b/scripts/src/build/modules/__tests__/output.test.ts @@ -12,7 +12,9 @@ const testBundle = ` `; test('building a bundle', async () => { - const { outputFiles: [file] } = await esbuild({ + const { + outputFiles: [file] + } = await esbuild({ ...commonEsbuildOptions, stdin: { contents: testBundle @@ -25,10 +27,11 @@ test('building a bundle', async () => { const rawBundleTextPromise = mockStream(); const result = await outputBundleOrTab(file, 'build'); - expect(result.severity) - .toEqual('success'); + expect(result.severity).toEqual('success'); - const bundleText = (await rawBundleTextPromise).slice('export default'.length); + const bundleText = (await rawBundleTextPromise).slice( + 'export default'.length + ); const mockContext = { moduleContexts: { test0: { @@ -36,17 +39,17 @@ test('building a bundle', async () => { } } }; - const bundleFuncs = eval(bundleText)((x) => ({ - 'js-slang/context': mockContext - }[x])); - expect(bundleFuncs.foo()) - .toEqual('foo'); - expect(bundleFuncs.bar()) - .toEqual(undefined); - expect(mockContext.moduleContexts) - .toMatchObject({ - test0: { - state: 'bar' - } - }); + const bundleFuncs = eval(bundleText)( + (x) => + ({ + 'js-slang/context': mockContext + }[x]) + ); + expect(bundleFuncs.foo()).toEqual('foo'); + expect(bundleFuncs.bar()).toEqual(undefined); + expect(mockContext.moduleContexts).toMatchObject({ + test0: { + state: 'bar' + } + }); }); diff --git a/scripts/src/build/modules/__tests__/streamMocker.ts b/scripts/src/build/modules/__tests__/streamMocker.ts index 6b2007557..6c4408ef0 100644 --- a/scripts/src/build/modules/__tests__/streamMocker.ts +++ b/scripts/src/build/modules/__tests__/streamMocker.ts @@ -2,7 +2,7 @@ import fs from 'fs/promises'; import { PassThrough } from 'stream'; import type { MockedFunction } from 'jest-mock'; -const mockedFsOpen = (fs.open as MockedFunction); +const mockedFsOpen = fs.open as MockedFunction; export function mockStream() { const stream = new PassThrough(); diff --git a/scripts/src/build/modules/__tests__/tab.test.ts b/scripts/src/build/modules/__tests__/tab.test.ts index 0d028f587..63aa00619 100644 --- a/scripts/src/build/modules/__tests__/tab.test.ts +++ b/scripts/src/build/modules/__tests__/tab.test.ts @@ -3,30 +3,23 @@ import type { MockedFunction } from 'jest-mock'; import * as tabs from '../tabs'; jest.mock('esbuild', () => ({ - build: jest.fn() - .mockResolvedValue({ outputFiles: [] }) + build: jest.fn().mockResolvedValue({ outputFiles: [] }) })); jest.spyOn(tabs, 'bundleTabs'); -testBuildCommand( - 'buildTabs', - tabs.getBuildTabsCommand, - [tabs.bundleTabs] -); +testBuildCommand('buildTabs', tabs.getBuildTabsCommand, [tabs.bundleTabs]); test('Normal command', async () => { - await tabs.getBuildTabsCommand() - .parseAsync(['-t', 'tab0'], { from: 'user' }); + await tabs.getBuildTabsCommand().parseAsync(['-t', 'tab0'], { from: 'user' }); - expect(tabs.bundleTabs) - .toHaveBeenCalledTimes(1); + expect(tabs.bundleTabs).toHaveBeenCalledTimes(1); - const [args] = (tabs.bundleTabs as MockedFunction).mock.calls[0]; - expect(args) - .toMatchObject({ - bundles: [], - tabs: ['tab0'], - modulesSpecified: true - }); + const [args] = (tabs.bundleTabs as MockedFunction) + .mock.calls[0]; + expect(args).toMatchObject({ + bundles: [], + tabs: ['tab0'], + modulesSpecified: true + }); }); diff --git a/scripts/src/build/modules/bundles.ts b/scripts/src/build/modules/bundles.ts index 85471b002..8d9fbc3c3 100644 --- a/scripts/src/build/modules/bundles.ts +++ b/scripts/src/build/modules/bundles.ts @@ -1,7 +1,12 @@ import fs from 'fs/promises'; import { build as esbuild, type Plugin as ESBuildPlugin } from 'esbuild'; import { bundlesOption, promiseAll } from '@src/commandUtils'; -import { expandBundleNames, type BuildTask, createBuildCommandHandler, createBuildCommand } from '../utils'; +import { + createBuildCommand, + createBuildCommandHandler, + expandBundleNames, + type BuildTask +} from '../utils'; import { commonEsbuildOptions, outputBundleOrTab } from './commons'; export const assertPolyfillPlugin: ESBuildPlugin = { @@ -13,11 +18,13 @@ export const assertPolyfillPlugin: ESBuildPlugin = { namespace: 'bundleAssert' })); - build.onLoad({ - filter: /^assert/u, - namespace: 'bundleAssert' - }, () => ({ - contents: ` + build.onLoad( + { + filter: /^assert/u, + namespace: 'bundleAssert' + }, + () => ({ + contents: ` export default function assert(condition, message) { if (condition) return; @@ -28,7 +35,8 @@ export const assertPolyfillPlugin: ESBuildPlugin = { throw message; } ` - })); + }) + ); } }; @@ -59,28 +67,37 @@ export const assertPolyfillPlugin: ESBuildPlugin = { // } // } -export const bundleBundles: BuildTask = async ({ bundles }, { srcDir, outDir }) => { - const [{ outputFiles }] = await promiseAll(esbuild({ - ...commonEsbuildOptions, - entryPoints: expandBundleNames(srcDir, bundles), - outbase: outDir, - outdir: outDir, - plugins: [ - assertPolyfillPlugin - // jsSlangExportCheckingPlugin, - ], - tsconfig: `${srcDir}/tsconfig.json` - }), fs.mkdir(`${outDir}/bundles`, { recursive: true })); +export const bundleBundles: BuildTask = async ( + { bundles }, + { srcDir, outDir } +) => { + const [{ outputFiles }] = await promiseAll( + esbuild({ + ...commonEsbuildOptions, + entryPoints: expandBundleNames(srcDir, bundles), + outbase: outDir, + outdir: outDir, + plugins: [ + assertPolyfillPlugin + // jsSlangExportCheckingPlugin, + ], + tsconfig: `${srcDir}/tsconfig.json` + }), + fs.mkdir(`${outDir}/bundles`, { recursive: true }) + ); - const results = await Promise.all(outputFiles.map(file => outputBundleOrTab(file, outDir))); + const results = await Promise.all( + outputFiles.map(file => outputBundleOrTab(file, outDir)) + ); return { bundles: results }; }; -const bundlesCommandHandler = createBuildCommandHandler((...args) => bundleBundles(...args), true); +const bundlesCommandHandler = createBuildCommandHandler( + (...args) => bundleBundles(...args), + true +); -export const getBuildBundlesCommand = () => createBuildCommand( - 'bundles', - 'Build bundles' -) - .addOption(bundlesOption) - .action(opts => bundlesCommandHandler({ ...opts, tabs: [] })); +export const getBuildBundlesCommand = () => + createBuildCommand('bundles', 'Build bundles') + .addOption(bundlesOption) + .action(opts => bundlesCommandHandler({ ...opts, tabs: [] })); diff --git a/scripts/src/build/modules/commons.ts b/scripts/src/build/modules/commons.ts index 41105337a..4ee1e39a7 100644 --- a/scripts/src/build/modules/commons.ts +++ b/scripts/src/build/modules/commons.ts @@ -3,7 +3,13 @@ import pathlib from 'path'; import { parse } from 'acorn'; import { generate } from 'astring'; import type { BuildOptions as ESBuildOptions, OutputFile } from 'esbuild'; -import type { ArrowFunctionExpression, CallExpression, ExportDefaultDeclaration, Program, VariableDeclaration } from 'estree'; +import type { + ArrowFunctionExpression, + CallExpression, + ExportDefaultDeclaration, + Program, + VariableDeclaration +} from 'estree'; import type { OperationResult } from '../utils'; export const commonEsbuildOptions: ESBuildOptions = { @@ -23,9 +29,11 @@ export const commonEsbuildOptions: ESBuildOptions = { write: false }; -export async function outputBundleOrTab({ path, text }: OutputFile, outDir: string): Promise { - const [type, name] = path.split(pathlib.sep) - .slice(-3, -1); +export async function outputBundleOrTab( + { path, text }: OutputFile, + outDir: string +): Promise { + const [type, name] = path.split(pathlib.sep).slice(-3, -1); let file: fs.FileHandle | null = null; try { const parsed = parse(text, { ecmaVersion: 6 }) as unknown as Program; @@ -45,10 +53,12 @@ export async function outputBundleOrTab({ path, text }: OutputFile, outDir: stri type: 'ExportDefaultDeclaration', declaration: { ...moduleCode, - params: [{ - type: 'Identifier', - name: 'require' - }] + params: [ + { + type: 'Identifier', + name: 'require' + } + ] } }; diff --git a/scripts/src/build/modules/index.ts b/scripts/src/build/modules/index.ts index a308d1443..d6484cb50 100644 --- a/scripts/src/build/modules/index.ts +++ b/scripts/src/build/modules/index.ts @@ -1,5 +1,9 @@ import { bundlesOption, tabsOption } from '@src/commandUtils'; -import { createBuildCommand, type BuildTask, createBuildCommandHandler } from '../utils'; +import { + createBuildCommand, + createBuildCommandHandler, + type BuildTask +} from '../utils'; import { bundleBundles } from './bundles'; import { bundleTabs } from './tabs'; @@ -17,10 +21,11 @@ export const buildModules: BuildTask = async (inputs, opts) => { const modulesCommandHandler = createBuildCommandHandler(buildModules, true); -export const getBuildModulesCommand = () => createBuildCommand('modules', 'Build bundles and tabs') - .addOption(bundlesOption) - .addOption(tabsOption) - .action(modulesCommandHandler); +export const getBuildModulesCommand = () => + createBuildCommand('modules', 'Build bundles and tabs') + .addOption(bundlesOption) + .addOption(tabsOption) + .action(modulesCommandHandler); export { getBuildBundlesCommand } from './bundles'; export { getBuildTabsCommand } from './tabs'; diff --git a/scripts/src/build/modules/tabs.ts b/scripts/src/build/modules/tabs.ts index 1ffebda2c..0f39b6867 100644 --- a/scripts/src/build/modules/tabs.ts +++ b/scripts/src/build/modules/tabs.ts @@ -1,45 +1,61 @@ import fs from 'fs/promises'; import { build as esbuild, type Plugin as ESBuildPlugin } from 'esbuild'; import { promiseAll, tabsOption } from '@src/commandUtils'; -import { expandTabNames, createBuildCommandHandler, type BuildTask, createBuildCommand } from '../utils'; +import { + createBuildCommand, + createBuildCommandHandler, + expandTabNames, + type BuildTask +} from '../utils'; import { commonEsbuildOptions, outputBundleOrTab } from './commons'; export const tabContextPlugin: ESBuildPlugin = { name: 'Tab Context', setup(build) { build.onResolve({ filter: /^js-slang\/context/u }, () => ({ - errors: [{ - text: 'If you see this message, it means that your tab code is importing js-slang/context directly or indirectly. Do not do this' - }] + errors: [ + { + text: 'If you see this message, it means that your tab code is importing js-slang/context directly or indirectly. Do not do this' + } + ] })); } }; export const bundleTabs: BuildTask = async ({ tabs }, { srcDir, outDir }) => { - const [{ outputFiles }] = await promiseAll(esbuild({ - ...commonEsbuildOptions, - entryPoints: expandTabNames(srcDir, tabs), - external: [ - ...commonEsbuildOptions.external, - 'react', - 'react-ace', - 'react-dom', - 'react/jsx-runtime', - '@blueprintjs/*' - // 'phaser', - ], - jsx: 'automatic', - outbase: outDir, - outdir: outDir, - tsconfig: `${srcDir}/tsconfig.json`, - plugins: [tabContextPlugin] - }), fs.mkdir(`${outDir}/tabs`, { recursive: true })); + const [{ outputFiles }] = await promiseAll( + esbuild({ + ...commonEsbuildOptions, + entryPoints: expandTabNames(srcDir, tabs), + external: [ + ...commonEsbuildOptions.external, + 'react', + 'react-ace', + 'react-dom', + 'react/jsx-runtime', + '@blueprintjs/*' + // 'phaser', + ], + jsx: 'automatic', + outbase: outDir, + outdir: outDir, + tsconfig: `${srcDir}/tsconfig.json`, + plugins: [tabContextPlugin] + }), + fs.mkdir(`${outDir}/tabs`, { recursive: true }) + ); - const results = await Promise.all(outputFiles.map(file => outputBundleOrTab(file, outDir))); + const results = await Promise.all( + outputFiles.map(file => outputBundleOrTab(file, outDir)) + ); return { tabs: results }; }; -const tabCommandHandler = createBuildCommandHandler((...args) => bundleTabs(...args), false); -export const getBuildTabsCommand = () => createBuildCommand('tabs', 'Build tabs') - .addOption(tabsOption) - .action(opts => tabCommandHandler({ ...opts, bundles: [] })); +const tabCommandHandler = createBuildCommandHandler( + (...args) => bundleTabs(...args), + false +); +export const getBuildTabsCommand = () => + createBuildCommand('tabs', 'Build tabs') + .addOption(tabsOption) + .action(opts => tabCommandHandler({ ...opts, bundles: [] })); diff --git a/scripts/src/build/prebuild/__mocks__/lint.ts b/scripts/src/build/prebuild/__mocks__/lint.ts index eac3ba04f..0ff31fee8 100644 --- a/scripts/src/build/prebuild/__mocks__/lint.ts +++ b/scripts/src/build/prebuild/__mocks__/lint.ts @@ -1,10 +1,9 @@ -export const runEslint = jest.fn() - .mockImplementation(() => ({ - elapsed: 0, - result: { - formatted: '', - severity: 'error' - } - })); +export const runEslint = jest.fn().mockImplementation(() => ({ + elapsed: 0, + result: { + formatted: '', + severity: 'error' + } +})); export const eslintResultsLogger = jest.fn(() => ''); diff --git a/scripts/src/build/prebuild/__mocks__/tsc.ts b/scripts/src/build/prebuild/__mocks__/tsc.ts index 18f7fd9a6..c58f4175b 100644 --- a/scripts/src/build/prebuild/__mocks__/tsc.ts +++ b/scripts/src/build/prebuild/__mocks__/tsc.ts @@ -1,10 +1,9 @@ export const tscResultsLogger = jest.fn(() => ''); -export const runTsc = jest.fn() - .mockResolvedValue({ - elapsed: 0, - result: { - severity: 'error', - results: [] - } - }); +export const runTsc = jest.fn().mockResolvedValue({ + elapsed: 0, + result: { + severity: 'error', + results: [] + } +}); diff --git a/scripts/src/build/prebuild/__tests__/prebuild.test.ts b/scripts/src/build/prebuild/__tests__/prebuild.test.ts index 934883a48..0c4328da8 100644 --- a/scripts/src/build/prebuild/__tests__/prebuild.test.ts +++ b/scripts/src/build/prebuild/__tests__/prebuild.test.ts @@ -6,14 +6,14 @@ import * as tscModule from '../tsc'; jest.spyOn(lintModule, 'runEslint'); jest.spyOn(tscModule, 'runTsc'); -const asMock = any>(func: T) => func as MockedFunction; +const asMock = any>(func: T) => + func as MockedFunction; const mockedTsc = asMock(tscModule.runTsc); const mockedEslint = asMock(lintModule.runEslint); describe('test eslint command', () => { const runCommand = async (...args: string[]) => { - await lintModule.getLintCommand() - .parseAsync(args, { from: 'user' }); + await lintModule.getLintCommand().parseAsync(args, { from: 'user' }); }; test('regular command function', async () => { @@ -27,8 +27,7 @@ describe('test eslint command', () => { await runCommand(); - expect(lintModule.runEslint) - .toHaveBeenCalledTimes(1); + expect(lintModule.runEslint).toHaveBeenCalledTimes(1); }); it('should only lint specified bundles and tabs', async () => { @@ -42,16 +41,14 @@ describe('test eslint command', () => { await runCommand('-b', 'test0', '-t', 'tab0'); - expect(lintModule.runEslint) - .toHaveBeenCalledTimes(1); + expect(lintModule.runEslint).toHaveBeenCalledTimes(1); const [lintCall] = mockedEslint.mock.calls[0]; - expect(lintCall) - .toMatchObject({ - bundles: ['test0'], - tabs: ['tab0'], - srcDir: 'src' - }); + expect(lintCall).toMatchObject({ + bundles: ['test0'], + tabs: ['tab0'], + srcDir: 'src' + }); }); it('should exit with code 1 if there are linting errors', async () => { @@ -66,20 +63,17 @@ describe('test eslint command', () => { try { await runCommand(); } catch (error) { - expect(error) - .toEqual(new Error('process.exit called with 1')); + expect(error).toEqual(new Error('process.exit called with 1')); } - expect(lintModule.runEslint) - .toHaveBeenCalledTimes(1); + expect(lintModule.runEslint).toHaveBeenCalledTimes(1); - expect(process.exit) - .toHaveBeenCalledWith(1); + expect(process.exit).toHaveBeenCalledWith(1); }); }); describe('test tsc command', () => { - const runCommand = (...args: string[]) => tscModule.getTscCommand() - .parseAsync(args, { from: 'user' }); + const runCommand = (...args: string[]) => + tscModule.getTscCommand().parseAsync(args, { from: 'user' }); test('regular command function', async () => { mockedTsc.mockResolvedValueOnce({ @@ -92,8 +86,7 @@ describe('test tsc command', () => { await runCommand(); - expect(tscModule.runTsc) - .toHaveBeenCalledTimes(1); + expect(tscModule.runTsc).toHaveBeenCalledTimes(1); }); it('should only typecheck specified bundles and tabs', async () => { @@ -107,16 +100,14 @@ describe('test tsc command', () => { await runCommand('-b', 'test0', '-t', 'tab0'); - expect(tscModule.runTsc) - .toHaveBeenCalledTimes(1); + expect(tscModule.runTsc).toHaveBeenCalledTimes(1); const [tscCall] = mockedTsc.mock.calls[0]; - expect(tscCall) - .toMatchObject({ - bundles: ['test0'], - tabs: ['tab0'], - srcDir: 'src' - }); + expect(tscCall).toMatchObject({ + bundles: ['test0'], + tabs: ['tab0'], + srcDir: 'src' + }); }); it('should exit with code 1 if there are type check errors', async () => { @@ -131,14 +122,11 @@ describe('test tsc command', () => { try { await runCommand(); } catch (error) { - expect(error) - .toEqual(new Error('process.exit called with 1')); + expect(error).toEqual(new Error('process.exit called with 1')); } - expect(tscModule.runTsc) - .toHaveBeenCalledTimes(1); + expect(tscModule.runTsc).toHaveBeenCalledTimes(1); - expect(process.exit) - .toHaveBeenCalledWith(1); + expect(process.exit).toHaveBeenCalledWith(1); }); }); diff --git a/scripts/src/build/prebuild/index.ts b/scripts/src/build/prebuild/index.ts index f050b0b72..7001517fd 100644 --- a/scripts/src/build/prebuild/index.ts +++ b/scripts/src/build/prebuild/index.ts @@ -1,12 +1,16 @@ -import { type Severity, findSeverity, type BuildOptions } from '@src/build/utils'; +import { + findSeverity, + type BuildOptions, + type Severity +} from '@src/build/utils'; import { promiseAll } from '@src/commandUtils'; import { eslintResultsLogger, runEslint } from './lint'; import { runTsc, tscResultsLogger } from './tsc'; interface PrebuildResult { - lint?: Awaited> - tsc?: Awaited> - severity: Severity + lint?: Awaited>; + tsc?: Awaited>; + severity: Severity; } export default async function prebuild( @@ -34,7 +38,10 @@ export default async function prebuild( runEslint(combinedOpts) ); - const overallSev = findSeverity([tscResult, lintResult], ({ result: { severity } }) => severity); + const overallSev = findSeverity( + [tscResult, lintResult], + ({ result: { severity } }) => severity + ); return { tsc: tscResult, diff --git a/scripts/src/build/prebuild/lint.ts b/scripts/src/build/prebuild/lint.ts index adcdd5f2d..519c9e341 100644 --- a/scripts/src/build/prebuild/lint.ts +++ b/scripts/src/build/prebuild/lint.ts @@ -7,66 +7,92 @@ import chalk from 'chalk'; */ // @ts-expect-error 2305 import { loadESLint, type ESLint } from 'eslint'; -import { lintFixOption, retrieveBundlesAndTabs, wrapWithTimer } from '@src/commandUtils'; -import { findSeverity, divideAndRound, type Severity, type AwaitedReturn } from '../utils'; -import { createPrebuildCommand, createPrebuildCommandHandler, type PrebuildOptions } from './utils'; +import { + lintFixOption, + retrieveBundlesAndTabs, + wrapWithTimer +} from '@src/commandUtils'; +import { + divideAndRound, + findSeverity, + type AwaitedReturn, + type Severity +} from '../utils'; +import { + createPrebuildCommand, + createPrebuildCommandHandler, + type PrebuildOptions +} from './utils'; -const severityFinder = (results: ESLint.LintResult[]) => findSeverity(results, ({ warningCount, fatalErrorCount }) => { - if (fatalErrorCount > 0) return 'error'; - if (warningCount > 0) return 'warn'; - return 'success'; -}); +const severityFinder = (results: ESLint.LintResult[]) => + findSeverity(results, ({ warningCount, fatalErrorCount }) => { + if (fatalErrorCount > 0) return 'error'; + if (warningCount > 0) return 'warn'; + return 'success'; + }); interface LintResults { - formatted: string - severity: Severity + formatted: string; + severity: Severity; } interface LintOptions extends PrebuildOptions { - fix?: boolean + fix?: boolean; } -export const runEslint = wrapWithTimer(async ({ bundles, tabs, srcDir, fix }: LintOptions): Promise => { - const ESlint = await loadESLint({ useFlatConfig: true }); - const linter = new ESlint({ fix }); +export const runEslint = wrapWithTimer( + async ({ bundles, tabs, srcDir, fix }: LintOptions): Promise => { + const ESlint = await loadESLint({ useFlatConfig: true }); + const linter = new ESlint({ fix }); - const fileNames = [ - ...bundles.map(bundleName => `${srcDir}/bundles/${bundleName}/**/*.ts`), - ...tabs.map(tabName => `${srcDir}/tabs/${tabName}/**/*.ts*`) - ]; + const fileNames = [ + ...bundles.map(bundleName => `${srcDir}/bundles/${bundleName}/**/*.ts`), + ...tabs.map(tabName => `${srcDir}/tabs/${tabName}/**/*.ts*`) + ]; - try { - const linterResults = await linter.lintFiles(fileNames); - if (fix) { - await ESlint.outputFixes(linterResults); - } + try { + const linterResults = await linter.lintFiles(fileNames); + if (fix) { + await ESlint.outputFixes(linterResults); + } - const outputFormatter = await linter.loadFormatter('stylish'); - const formatted = await outputFormatter.format(linterResults); - const severity = severityFinder(linterResults); - return { - formatted, - severity - }; - } catch (error) { - return { - severity: 'error', - formatted: error.toString() - }; + const outputFormatter = await linter.loadFormatter('stylish'); + const formatted = await outputFormatter.format(linterResults); + const severity = severityFinder(linterResults); + return { + formatted, + severity + }; + } catch (error) { + return { + severity: 'error', + formatted: error.toString() + }; + } } -}); +); -export function eslintResultsLogger({ elapsed, result: { formatted, severity } }: AwaitedReturn) { +export function eslintResultsLogger({ + elapsed, + result: { formatted, severity } +}: AwaitedReturn) { let errStr: string; - if (severity === 'error') errStr = chalk.cyanBright('with ') + chalk.redBright('errors'); - else if (severity === 'warn') errStr = chalk.cyanBright('with ') + chalk.yellowBright('warnings'); + if (severity === 'error') + errStr = chalk.cyanBright('with ') + chalk.redBright('errors'); + else if (severity === 'warn') + errStr = chalk.cyanBright('with ') + chalk.yellowBright('warnings'); else errStr = chalk.greenBright('successfully'); - return `${chalk.cyanBright(`Linting completed in ${divideAndRound(elapsed, 1000)}s ${errStr}:`)}\n${formatted}`; + return `${chalk.cyanBright( + `Linting completed in ${divideAndRound(elapsed, 1000)}s ${errStr}:` + )}\n${formatted}`; } -const lintCommandHandler = createPrebuildCommandHandler((...args) => runEslint(...args), eslintResultsLogger); +const lintCommandHandler = createPrebuildCommandHandler( + (...args) => runEslint(...args), + eslintResultsLogger +); export function getLintCommand() { return createPrebuildCommand('lint', 'Run eslint') diff --git a/scripts/src/build/prebuild/tsc.ts b/scripts/src/build/prebuild/tsc.ts index 24b24ced0..6b6f47109 100644 --- a/scripts/src/build/prebuild/tsc.ts +++ b/scripts/src/build/prebuild/tsc.ts @@ -3,26 +3,39 @@ import pathlib from 'path'; import chalk from 'chalk'; import ts from 'typescript'; import { retrieveBundlesAndTabs, wrapWithTimer } from '@src/commandUtils'; -import { expandBundleNames, expandTabNames, divideAndRound, type AwaitedReturn } from '../utils'; -import { createPrebuildCommand, createPrebuildCommandHandler, type PrebuildOptions } from './utils'; - -type TsconfigResult = { - severity: 'error', - results?: ts.Diagnostic[] - error?: any -} | { - severity: 'success', - results: ts.CompilerOptions -}; - -type TscResult = { - severity: 'error' - results?: ts.Diagnostic[] - error?: any -} | { - severity: 'success', - results: ts.Diagnostic[] -}; +import { + divideAndRound, + expandBundleNames, + expandTabNames, + type AwaitedReturn +} from '../utils'; +import { + createPrebuildCommand, + createPrebuildCommandHandler, + type PrebuildOptions +} from './utils'; + +type TsconfigResult = + | { + severity: 'error'; + results?: ts.Diagnostic[]; + error?: any; + } + | { + severity: 'success'; + results: ts.CompilerOptions; + }; + +type TscResult = + | { + severity: 'error'; + results?: ts.Diagnostic[]; + error?: any; + } + | { + severity: 'success'; + results: ts.Diagnostic[]; + }; async function getTsconfig(srcDir: string): Promise { // Step 1: Read the text from tsconfig.json @@ -31,7 +44,8 @@ async function getTsconfig(srcDir: string): Promise { const configText = await fs.readFile(tsconfigLocation, 'utf-8'); // Step 2: Parse the raw text into a json object - const { error: configJsonError, config: configJson } = ts.parseConfigFileTextToJson(tsconfigLocation, configText); + const { error: configJsonError, config: configJson } = + ts.parseConfigFileTextToJson(tsconfigLocation, configText); if (configJsonError) { return { severity: 'error', @@ -40,7 +54,8 @@ async function getTsconfig(srcDir: string): Promise { } // Step 3: Parse the json object into a config object for use by tsc - const { errors: parseErrors, options: tsconfig } = ts.parseJsonConfigFileContent(configJson, ts.sys, srcDir); + const { errors: parseErrors, options: tsconfig } = + ts.parseJsonConfigFileContent(configJson, ts.sys, srcDir); if (parseErrors.length > 0) { return { severity: 'error', @@ -60,45 +75,56 @@ async function getTsconfig(srcDir: string): Promise { } } -export const runTsc = wrapWithTimer(async ({ bundles, tabs, srcDir }: PrebuildOptions): Promise => { - const tsconfigRes = await getTsconfig(srcDir); - if (tsconfigRes.severity === 'error') { - return tsconfigRes; - } +export const runTsc = wrapWithTimer( + async ({ bundles, tabs, srcDir }: PrebuildOptions): Promise => { + const tsconfigRes = await getTsconfig(srcDir); + if (tsconfigRes.severity === 'error') { + return tsconfigRes; + } - const fileNames: string[] = []; + const fileNames: string[] = []; - if (bundles.length > 0) { - expandBundleNames(srcDir, bundles) - .forEach(name => fileNames.push(name)); - } + if (bundles.length > 0) { + expandBundleNames(srcDir, bundles).forEach(name => + fileNames.push(name) + ); + } - if (tabs.length > 0) { - expandTabNames(srcDir, tabs) - .forEach(name => fileNames.push(name)); - } + if (tabs.length > 0) { + expandTabNames(srcDir, tabs).forEach(name => fileNames.push(name)); + } - try { - const tsc = ts.createProgram(fileNames, tsconfigRes.results); - const results = tsc.emit(); - const diagnostics = ts.getPreEmitDiagnostics(tsc) - .concat(results.diagnostics); + try { + const tsc = ts.createProgram(fileNames, tsconfigRes.results); + const results = tsc.emit(); + const diagnostics = ts + .getPreEmitDiagnostics(tsc) + .concat(results.diagnostics); - return { - severity: diagnostics.length > 0 ? 'error' : 'success', - results: diagnostics - }; - } catch (error) { - return { - severity: 'error', - error - }; + return { + severity: diagnostics.length > 0 ? 'error' : 'success', + results: diagnostics + }; + } catch (error) { + return { + severity: 'error', + error + }; + } } -}); +); -export function tscResultsLogger({ elapsed, result: tscResult }: AwaitedReturn) { +export function tscResultsLogger({ + elapsed, + result: tscResult +}: AwaitedReturn) { if (tscResult.severity === 'error' && tscResult.error) { - return `${chalk.cyanBright(`tsc finished with ${chalk.redBright('errors')} in ${divideAndRound(elapsed, 1000)}s: ${tscResult.error}`)}`; + return `${chalk.cyanBright( + `tsc finished with ${chalk.redBright('errors')} in ${divideAndRound( + elapsed, + 1000 + )}s: ${tscResult.error}` + )}`; } const diagStr = ts.formatDiagnosticsWithColorAndContext(tscResult.results, { @@ -108,9 +134,19 @@ export function tscResultsLogger({ elapsed, result: tscResult }: AwaitedReturn createPrebuildCommand('tsc', 'Run the typescript compiler to perform type checking') - .action(async opts => { +export const getTscCommand = () => + createPrebuildCommand( + 'tsc', + 'Run the typescript compiler to perform type checking' + ).action(async opts => { const inputs = await retrieveBundlesAndTabs(opts, false); await tscCommandHandler({ ...opts, ...inputs }); }); diff --git a/scripts/src/build/prebuild/utils.ts b/scripts/src/build/prebuild/utils.ts index c80369b9e..549b3d10a 100644 --- a/scripts/src/build/prebuild/utils.ts +++ b/scripts/src/build/prebuild/utils.ts @@ -1,15 +1,22 @@ import { Command } from '@commander-js/extra-typings'; -import { bundlesOption, manifestOption, srcDirOption, tabsOption, type TimedResult } from '@src/commandUtils'; +import { + bundlesOption, + manifestOption, + srcDirOption, + tabsOption, + type TimedResult +} from '@src/commandUtils'; import { logInputs, type Severity } from '../utils'; export interface PrebuildOptions { - srcDir: string - manifest: string - bundles: string[] - tabs: string[] + srcDir: string; + manifest: string; + bundles: string[]; + tabs: string[]; } -export interface PrebuildResult extends TimedResult {} +export interface PrebuildResult + extends TimedResult {} export function createPrebuildCommand( commandName: string, diff --git a/scripts/src/build/utils.ts b/scripts/src/build/utils.ts index d62ae2827..251383aa1 100644 --- a/scripts/src/build/utils.ts +++ b/scripts/src/build/utils.ts @@ -2,50 +2,63 @@ import { copyFile } from 'fs/promises'; import { Command } from '@commander-js/extra-typings'; import chalk from 'chalk'; import { Table } from 'console-table-printer'; -import { lintFixOption, lintOption, manifestOption, objectEntries, outDirOption, retrieveBundlesAndTabs, srcDirOption } from '@src/commandUtils'; +import { + lintFixOption, + lintOption, + manifestOption, + objectEntries, + outDirOption, + retrieveBundlesAndTabs, + srcDirOption +} from '@src/commandUtils'; import { htmlLogger, type buildHtml } from './docs/html'; import prebuild, { formatPrebuildResults } from './prebuild'; export interface BuildInputs { - bundles?: string[] | null - tabs?: string[] | null - modulesSpecified?: boolean + bundles?: string[] | null; + tabs?: string[] | null; + modulesSpecified?: boolean; } export interface BuildOptions { - srcDir: string - outDir: string - manifest: string - lint?: boolean - fix?: boolean - tsc?: boolean - verbose?: boolean + srcDir: string; + outDir: string; + manifest: string; + lint?: boolean; + fix?: boolean; + tsc?: boolean; + verbose?: boolean; } export interface SuccessResult { - name: string - severity: 'success', + name: string; + severity: 'success'; } export interface WarnResult { - name: string, - severity: 'warn', - error: any + name: string; + severity: 'warn'; + error: any; } export interface ErrorResult { - name: string, - severity: 'error', - error: any + name: string; + severity: 'error'; + error: any; } export type OperationResult = ErrorResult | SuccessResult | WarnResult; export type Severity = OperationResult['severity']; -export const isSuccessResult = (obj: OperationResult): obj is SuccessResult => obj.severity === 'success'; -export const isWarnResult = (obj: OperationResult): obj is WarnResult => obj.severity === 'warn'; +export const isSuccessResult = (obj: OperationResult): obj is SuccessResult => + obj.severity === 'success'; +export const isWarnResult = (obj: OperationResult): obj is WarnResult => + obj.severity === 'warn'; -export function findSeverity(results: T[], mapper?: (item: T) => Severity): Severity { +export function findSeverity( + results: T[], + mapper?: (item: T) => Severity +): Severity { let overallSev: Severity = 'success'; for (const result of results) { @@ -53,7 +66,10 @@ export function findSeverity(results: T[], mapper?: (item: T) if ('severity' in result) { severity = result.severity as Severity; } else { - if (!mapper) throw new Error(`Mapping function required to convert ${result} to severity`); + if (!mapper) + throw new Error( + `Mapping function required to convert ${result} to severity` + ); severity = mapper(result); } @@ -66,26 +82,38 @@ export function findSeverity(results: T[], mapper?: (item: T) return overallSev; } -export const expandBundleNames = (srcDir: string, bundles: string[]) => bundles.map(bundle => `${srcDir}/bundles/${bundle}/index.ts`); -export const expandTabNames = (srcDir: string, tabNames: string[]) => tabNames.map(tabName => `${srcDir}/tabs/${tabName}/index.tsx`); +export const expandBundleNames = (srcDir: string, bundles: string[]) => + bundles.map(bundle => `${srcDir}/bundles/${bundle}/index.ts`); +export const expandTabNames = (srcDir: string, tabNames: string[]) => + tabNames.map(tabName => `${srcDir}/tabs/${tabName}/index.tsx`); -export type AwaitedReturn = T extends (...args: any) => Promise ? U : never; +export type AwaitedReturn = T extends (...args: any) => Promise + ? U + : never; -export const divideAndRound = (n: number, divisor: number) => (n / divisor).toFixed(2); +export const divideAndRound = (n: number, divisor: number) => + (n / divisor).toFixed(2); type AssetType = 'bundles' | 'jsons' | 'tabs'; -type LogType = Partial & { html: Awaited> }>; +type LogType = Partial< + Record & { + html: Awaited>; + } +>; -export type BuildTask = (inputs: BuildInputs, opts: BuildOptions) => Promise; +export type BuildTask = ( + inputs: BuildInputs, + opts: BuildOptions +) => Promise; -function processResults( - results: LogType, - verbose: boolean -) { - const notSuccessFilter = (result: OperationResult): result is Exclude => result.severity !== 'success'; +function processResults(results: LogType, verbose: boolean) { + const notSuccessFilter = ( + result: OperationResult + ): result is Exclude => + result.severity !== 'success'; - const logs = objectEntries(results) - .map(([label, results]): [Severity, string] => { + const logs = objectEntries(results).map( + ([label, results]): [Severity, string] => { if (label === 'html') { return [results.result.severity, htmlLogger(results)]; } @@ -94,68 +122,117 @@ function processResults( const upperCaseLabel = label[0].toUpperCase() + label.slice(1); if (!verbose) { if (overallSev === 'success') { - return ['success', `${chalk.cyanBright(`${upperCaseLabel} built`)} ${chalk.greenBright('successfully')}\n`]; + return [ + 'success', + `${chalk.cyanBright(`${upperCaseLabel} built`)} ${chalk.greenBright( + 'successfully' + )}\n` + ]; } if (overallSev === 'warn') { - return ['warn', chalk.cyanBright(`${upperCaseLabel} built with ${chalk.yellowBright('warnings')}:\n${results - .filter(isWarnResult) - .map(({ name: bundle, error }, i) => chalk.yellowBright(`${i + 1}. ${bundle}: ${error}`)) - .join('\n')}\n`)]; + return [ + 'warn', + chalk.cyanBright( + `${upperCaseLabel} built with ${chalk.yellowBright( + 'warnings' + )}:\n${results + .filter(isWarnResult) + .map(({ name: bundle, error }, i) => + chalk.yellowBright(`${i + 1}. ${bundle}: ${error}`) + ) + .join('\n')}\n` + ) + ]; } - return ['error', chalk.cyanBright(`${upperCaseLabel} build ${chalk.redBright('failed')} with errors:\n${results - .filter(notSuccessFilter) - .map(({ name: bundle, error, severity }, i) => (severity === 'error' - ? chalk.redBright(`${i + 1}. Error ${bundle}: ${error}`) - : chalk.yellowBright(`${i + 1}. Warning ${bundle}: ${error}`))) - .join('\n')}\n`)]; + return [ + 'error', + chalk.cyanBright( + `${upperCaseLabel} build ${chalk.redBright( + 'failed' + )} with errors:\n${results + .filter(notSuccessFilter) + .map(({ name: bundle, error, severity }, i) => + severity === 'error' + ? chalk.redBright(`${i + 1}. Error ${bundle}: ${error}`) + : chalk.yellowBright(`${i + 1}. Warning ${bundle}: ${error}`) + ) + .join('\n')}\n` + ) + ]; } const outputTable = new Table({ - columns: [{ - name: 'name', - title: upperCaseLabel - }, - { - name: 'severity', - title: 'Status' - }, - { - name: 'error', - title: 'Errors' - }] + columns: [ + { + name: 'name', + title: upperCaseLabel + }, + { + name: 'severity', + title: 'Status' + }, + { + name: 'error', + title: 'Errors' + } + ] }); results.forEach(result => { if (isWarnResult(result)) { - outputTable.addRow({ - ...result, - severity: 'Warning' - }, { color: 'yellow' }); + outputTable.addRow( + { + ...result, + severity: 'Warning' + }, + { color: 'yellow' } + ); } else if (isSuccessResult(result)) { - outputTable.addRow({ - ...result, - error: '-', - severity: 'Success' - }, { color: 'green' }); + outputTable.addRow( + { + ...result, + error: '-', + severity: 'Success' + }, + { color: 'green' } + ); } else { - outputTable.addRow({ - ...result, - severity: 'Error' - }, { color: 'red' }); + outputTable.addRow( + { + ...result, + severity: 'Error' + }, + { color: 'red' } + ); } }); if (overallSev === 'success') { - return ['success', `${chalk.cyanBright(`${upperCaseLabel} built`)} ${chalk.greenBright('successfully')}:\n${outputTable.render()}\n`]; + return [ + 'success', + `${chalk.cyanBright(`${upperCaseLabel} built`)} ${chalk.greenBright( + 'successfully' + )}:\n${outputTable.render()}\n` + ]; } if (overallSev === 'warn') { - return ['warn', `${chalk.cyanBright(`${upperCaseLabel} built`)} with ${chalk.yellowBright('warnings')}:\n${outputTable.render()}\n`]; + return [ + 'warn', + `${chalk.cyanBright( + `${upperCaseLabel} built` + )} with ${chalk.yellowBright('warnings')}:\n${outputTable.render()}\n` + ]; } - return ['error', `${chalk.cyanBright(`${upperCaseLabel} build ${chalk.redBright('failed')} with errors`)}:\n${outputTable.render()}\n`]; - }); + return [ + 'error', + `${chalk.cyanBright( + `${upperCaseLabel} build ${chalk.redBright('failed')} with errors` + )}:\n${outputTable.render()}\n` + ]; + } + ); - console.log(logs.map(x => x[1]) - .join('\n')); + console.log(logs.map(x => x[1]).join('\n')); const overallOverallSev = findSeverity(logs, ([sev]) => sev); if (overallOverallSev === 'error') { @@ -163,10 +240,15 @@ function processResults( } } -export function logInputs({ bundles, tabs }: BuildInputs, { tsc, lint }: Partial>) { +export function logInputs( + { bundles, tabs }: BuildInputs, + { tsc, lint }: Partial> +) { const output: string[] = []; if (tsc) { - output.push(chalk.yellowBright('--tsc specified, will run typescript checker')); + output.push( + chalk.yellowBright('--tsc specified, will run typescript checker') + ); } if (lint) { @@ -191,7 +273,7 @@ export function createBuildCommandHandler( shouldAddModuleTabs: boolean ) { return async ( - opts: BuildOptions & { bundles: string[] | null, tabs: string[] | null } + opts: BuildOptions & { bundles: string[] | null; tabs: string[] | null } ) => { const inputs = await retrieveBundlesAndTabs(opts, shouldAddModuleTabs); @@ -211,10 +293,7 @@ export function createBuildCommandHandler( }; } -export function createBuildCommand( - commandName: string, - description: string -) { +export function createBuildCommand(commandName: string, description: string) { return new Command(commandName) .description(description) .addOption(srcDirOption) diff --git a/scripts/src/commandUtils.ts b/scripts/src/commandUtils.ts index a24eb8118..487f16dc1 100644 --- a/scripts/src/commandUtils.ts +++ b/scripts/src/commandUtils.ts @@ -9,47 +9,65 @@ class OptionNew< CoerceT = undefined, Mandatory extends boolean = false, ChoicesT = undefined -> - extends Option { - default(value: T, description?: string): Option { +> extends Option { + default( + value: T, + description?: string + ): Option { return super.default(value, description); } } -export const srcDirOption = new OptionNew('--srcDir ', 'Location of the source files') - .default('src'); +export const srcDirOption = new OptionNew( + '--srcDir ', + 'Location of the source files' +).default('src'); -export const outDirOption = new OptionNew('--outDir ', 'Location of output directory') - .default('build'); +export const outDirOption = new OptionNew( + '--outDir ', + 'Location of output directory' +).default('build'); -export const manifestOption = new OptionNew('--manifest ', 'Location of manifest') - .default('modules.json'); +export const manifestOption = new OptionNew( + '--manifest ', + 'Location of manifest' +).default('modules.json'); export const lintOption = new OptionNew('--lint', 'Run ESLint'); -export const lintFixOption = new OptionNew('--fix', 'Fix automatically fixable linting errors') - .implies({ lint: true }); +export const lintFixOption = new OptionNew( + '--fix', + 'Fix automatically fixable linting errors' +).implies({ lint: true }); -export const bundlesOption = new OptionNew('-b, --bundles ', 'Manually specify which bundles') - .default(null); +export const bundlesOption = new OptionNew( + '-b, --bundles ', + 'Manually specify which bundles' +).default(null); -export const tabsOption = new OptionNew('-t, --tabs ', 'Manually specify which tabs') - .default(null); +export const tabsOption = new OptionNew( + '-t, --tabs ', + 'Manually specify which tabs' +).default(null); export async function retrieveBundlesAndTabs( - { bundles, tabs, manifest: manifestFile }: { - bundles?: string[] | null, - tabs?: string[] | null, - manifest: string - }, shouldAddModuleTabs: boolean + { + bundles, + tabs, + manifest: manifestFile + }: { + bundles?: string[] | null; + tabs?: string[] | null; + manifest: string; + }, + shouldAddModuleTabs: boolean ) { const manifest = await retrieveManifest(manifestFile); const knownBundles = Object.keys(manifest); - const knownTabs = Object - .values(manifest) - .flatMap(x => x.tabs); + const knownTabs = Object.values(manifest).flatMap(x => x.tabs); - const isUndefinedOrNull = (x: any): x is null | undefined => x === undefined || x === null; + const isUndefinedOrNull = (x: any): x is null | undefined => + x === undefined || x === null; let bundlesOutput: string[]; let tabsOutput: string[]; @@ -58,7 +76,9 @@ export async function retrieveBundlesAndTabs( // User did not specify any bundles, select all bundlesOutput = knownBundles; } else { - const unknownBundles = bundles.filter(bundleName => !knownBundles.includes(bundleName)); + const unknownBundles = bundles.filter( + bundleName => !knownBundles.includes(bundleName) + ); if (unknownBundles.length > 0) { throw new Error(`Unknown bundles: ${unknownBundles.join(', ')}`); } @@ -95,17 +115,23 @@ export async function retrieveBundlesAndTabs( }; } -export function promiseAll[]>(...args: T): Promise<{ [K in keyof T]: Awaited }> { +export function promiseAll[]>( + ...args: T +): Promise<{ [K in keyof T]: Awaited }> { return Promise.all(args); } export interface TimedResult { - result: T - elapsed: number + result: T; + elapsed: number; } -export function wrapWithTimer Promise>(func: T) { - return async (...args: Parameters): Promise>> => { +export function wrapWithTimer Promise>( + func: T +) { + return async ( + ...args: Parameters + ): Promise>> => { const startTime = performance.now(); const result = await func(...args); return { @@ -118,7 +144,7 @@ export function wrapWithTimer Promise>(func: T type ValuesOfRecord = T extends Record ? U : never; export type EntriesOfRecord> = ValuesOfRecord<{ - [K in keyof T]: [K, T[K]] + [K in keyof T]: [K, T[K]]; }>; export function objectEntries>(obj: T) { diff --git a/scripts/src/manifest.ts b/scripts/src/manifest.ts index 53b38a02c..e8f798dc0 100644 --- a/scripts/src/manifest.ts +++ b/scripts/src/manifest.ts @@ -1,13 +1,14 @@ import fs from 'fs/promises'; -export type ModuleManifest = Record; +export type ModuleManifest = Record; export async function retrieveManifest(manifest: string) { try { const rawManifest = await fs.readFile(manifest, 'utf-8'); return JSON.parse(rawManifest) as ModuleManifest; } catch (error) { - if (error.code === 'ENOENT') throw new Error(`Could not locate manifest file at ${manifest}`); + if (error.code === 'ENOENT') + throw new Error(`Could not locate manifest file at ${manifest}`); throw error; } } diff --git a/scripts/src/templates/__tests__/create.test.ts b/scripts/src/templates/__tests__/create.test.ts index a0ea05cbc..dc7c7457f 100644 --- a/scripts/src/templates/__tests__/create.test.ts +++ b/scripts/src/templates/__tests__/create.test.ts @@ -21,33 +21,31 @@ jest.mock('../print', () => ({ } })); -const asMock = any>(func: T) => func as MockedFunction; +const asMock = any>(func: T) => + func as MockedFunction; const mockedAskQuestion = asMock(askQuestion); function runCommand(...args: string[]) { - return getCreateCommand() - .parseAsync(args, { from: 'user' }); + return getCreateCommand().parseAsync(args, { from: 'user' }); } -function expectCall any>( +function expectCall any>( func: T, - ...expected: Parameters[]) { + ...expected: Parameters[] +) { const mocked = asMock(func); - expect(func) - .toHaveBeenCalledTimes(expected.length); + expect(func).toHaveBeenCalledTimes(expected.length); mocked.mock.calls.forEach((actual, i) => { - expect(actual) - .toEqual(expected[i]); + expect(actual).toEqual(expected[i]); }); } async function expectCommandFailure(snapshot: string) { await expect(runCommand()) - .rejects - // eslint-disable-next-line jest/no-interpolation-in-snapshots + .rejects // eslint-disable-next-line jest/no-interpolation-in-snapshots .toMatchInlineSnapshot(`[Error: ERROR: ${snapshot}]`); expect(fs.writeFile).not.toHaveBeenCalled(); @@ -62,7 +60,9 @@ describe('Test adding new module', () => { it('should require camel case for module names', async () => { mockedAskQuestion.mockResolvedValueOnce('pascalCase'); - await expectCommandFailure('Module names must be in snake case. (eg. binary_tree)'); + await expectCommandFailure( + 'Module names must be in snake case. (eg. binary_tree)' + ); }); it('should check for existing modules', async () => { @@ -74,29 +74,21 @@ describe('Test adding new module', () => { mockedAskQuestion.mockResolvedValueOnce('new_module'); await runCommand(); - expectCall( - fs.mkdir, - ['src/bundles/new_module', { recursive: true }] - ); + expectCall(fs.mkdir, ['src/bundles/new_module', { recursive: true }]); - expectCall( - fs.copyFile, - [ - './scripts/src/templates/templates/__bundle__.ts', - 'src/bundles/new_module/index.ts' - ] - ); + expectCall(fs.copyFile, [ + './scripts/src/templates/templates/__bundle__.ts', + 'src/bundles/new_module/index.ts' + ]); const oldManifest = await retrieveManifest('modules.json'); const [[manifestPath, newManifest]] = asMock(fs.writeFile).mock.calls; - expect(manifestPath) - .toEqual('modules.json'); - - expect(JSON.parse(newManifest as string)) - .toMatchObject({ - ...oldManifest, - new_module: { tabs: [] } - }); + expect(manifestPath).toEqual('modules.json'); + + expect(JSON.parse(newManifest as string)).toMatchObject({ + ...oldManifest, + new_module: { tabs: [] } + }); }); }); @@ -108,7 +100,9 @@ describe('Test adding new tab', () => { it('should require pascal case for tab names', async () => { mockedAskQuestion.mockResolvedValueOnce('test0'); mockedAskQuestion.mockResolvedValueOnce('unknown_tab'); - await expectCommandFailure('Tab names must be in pascal case. (eg. BinaryTree)'); + await expectCommandFailure( + 'Tab names must be in pascal case. (eg. BinaryTree)' + ); }); it('should check if the given tab already exists', async () => { @@ -128,30 +122,22 @@ describe('Test adding new tab', () => { await runCommand(); - expectCall( - fs.mkdir, - ['src/tabs/TabNew', { recursive: true }] - ); + expectCall(fs.mkdir, ['src/tabs/TabNew', { recursive: true }]); - expectCall( - fs.copyFile, - [ - './scripts/src/templates/templates/__tab__.tsx', - 'src/tabs/TabNew/index.tsx' - ] - ); + expectCall(fs.copyFile, [ + './scripts/src/templates/templates/__tab__.tsx', + 'src/tabs/TabNew/index.tsx' + ]); const oldManifest = await retrieveManifest('modules.json'); const [[manifestPath, newManifest]] = asMock(fs.writeFile).mock.calls; - expect(manifestPath) - .toEqual('modules.json'); - - expect(JSON.parse(newManifest as string)) - .toMatchObject({ - ...oldManifest, - test0: { - tabs: ['tab0', 'TabNew'] - } - }); + expect(manifestPath).toEqual('modules.json'); + + expect(JSON.parse(newManifest as string)).toMatchObject({ + ...oldManifest, + test0: { + tabs: ['tab0', 'TabNew'] + } + }); }); }); diff --git a/scripts/src/templates/__tests__/names.test.ts b/scripts/src/templates/__tests__/names.test.ts index eaf0b6ce6..f4a90b8cf 100644 --- a/scripts/src/templates/__tests__/names.test.ts +++ b/scripts/src/templates/__tests__/names.test.ts @@ -4,8 +4,10 @@ function testFunction( func: (value: string) => boolean, tcs: [string, boolean][] ) { - describe(`Testing ${func.name}`, () => test.each(tcs)('%#: %s', (value, valid) => expect(func(value)) - .toEqual(valid))); + describe(`Testing ${func.name}`, () => + test.each(tcs)('%#: %s', (value, valid) => + expect(func(value)).toEqual(valid) + )); } testFunction(isPascalCase, [ diff --git a/scripts/src/templates/index.ts b/scripts/src/templates/index.ts index 7db181a61..1659e2bae 100644 --- a/scripts/src/templates/index.ts +++ b/scripts/src/templates/index.ts @@ -9,7 +9,10 @@ import { addNew as addNewTab } from './tab'; async function askMode(rl: Interface) { while (true) { // eslint-disable-next-line no-await-in-loop - const mode = await askQuestion('What would you like to create? (module/tab)', rl); + const mode = await askQuestion( + 'What would you like to create? (module/tab)', + rl + ); if (mode !== 'module' && mode !== 'tab') { warn("Please answer with only 'module' or 'tab'."); } else { diff --git a/scripts/src/templates/module.ts b/scripts/src/templates/module.ts index 815ef1939..94a1df2fb 100644 --- a/scripts/src/templates/module.ts +++ b/scripts/src/templates/module.ts @@ -1,18 +1,21 @@ import fs from 'fs/promises'; import type { Interface } from 'readline/promises'; -import { type ModuleManifest, retrieveManifest } from '@src/manifest'; +import { retrieveManifest, type ModuleManifest } from '@src/manifest'; import { askQuestion, success, warn } from './print'; -import { type Options, isSnakeCase } from './utilities'; +import { isSnakeCase, type Options } from './utilities'; -export const check = (manifest: ModuleManifest, name: string) => Object.keys(manifest) - .includes(name); +export const check = (manifest: ModuleManifest, name: string) => + Object.keys(manifest).includes(name); async function askModuleName(manifest: ModuleManifest, rl: Interface) { while (true) { // eslint-disable-next-line no-await-in-loop - const name = await askQuestion('What is the name of your new module? (eg. binary_tree)', rl); + const name = await askQuestion( + 'What is the name of your new module? (eg. binary_tree)', + rl + ); if (isSnakeCase(name) === false) { warn('Module names must be in snake case. (eg. binary_tree)'); } else if (check(manifest, name)) { @@ -23,7 +26,10 @@ async function askModuleName(manifest: ModuleManifest, rl: Interface) { } } -export async function addNew({ srcDir, manifest: manifestFile }: Options, rl: Interface) { +export async function addNew( + { srcDir, manifest: manifestFile }: Options, + rl: Interface +) { const manifest = await retrieveManifest(manifestFile); const moduleName = await askModuleName(manifest, rl); @@ -35,10 +41,14 @@ export async function addNew({ srcDir, manifest: manifestFile }: Options, rl: In ); await fs.writeFile( manifestFile, - JSON.stringify({ - ...manifest, - [moduleName]: { tabs: [] } - }, null, 2) + JSON.stringify( + { + ...manifest, + [moduleName]: { tabs: [] } + }, + null, + 2 + ) ); success(`Bundle for module ${moduleName} created at ${bundleDestination}.`); } diff --git a/scripts/src/templates/print.ts b/scripts/src/templates/print.ts index f358a2fc6..e2f106b5b 100644 --- a/scripts/src/templates/print.ts +++ b/scripts/src/templates/print.ts @@ -1,10 +1,11 @@ -import { type Interface, createInterface } from 'readline/promises'; +import { createInterface, type Interface } from 'readline/promises'; import chalk from 'chalk'; -export const getRl = () => createInterface({ - input: process.stdin, - output: process.stdout -}); +export const getRl = () => + createInterface({ + input: process.stdin, + output: process.stdout + }); export function info(...args: any[]) { return console.log(...args.map(string => chalk.grey(string))); diff --git a/scripts/src/templates/tab.ts b/scripts/src/templates/tab.ts index 4bd1737d4..a34e1fb4f 100644 --- a/scripts/src/templates/tab.ts +++ b/scripts/src/templates/tab.ts @@ -2,11 +2,11 @@ import fs from 'fs/promises'; import type { Interface } from 'readline/promises'; -import { type ModuleManifest, retrieveManifest } from '@src/manifest'; +import { retrieveManifest, type ModuleManifest } from '@src/manifest'; import { check as _check } from './module'; import { askQuestion, success, warn } from './print'; -import { type Options, isPascalCase } from './utilities'; +import { isPascalCase, type Options } from './utilities'; export function check(manifest: ModuleManifest, tabName: string) { return Object.values(manifest) @@ -28,7 +28,8 @@ async function askModuleName(manifest: ModuleManifest, rl: Interface) { async function askTabName(manifest: ModuleManifest, rl: Interface) { while (true) { const name = await askQuestion( - 'What is the name of your new tab? (eg. BinaryTree)', rl + 'What is the name of your new tab? (eg. BinaryTree)', + rl ); if (check(manifest, name)) { warn('A tab with the same name already exists.'); @@ -40,7 +41,10 @@ async function askTabName(manifest: ModuleManifest, rl: Interface) { } } -export async function addNew({ manifest: manifestFile, srcDir }: Options, rl: Interface) { +export async function addNew( + { manifest: manifestFile, srcDir }: Options, + rl: Interface +) { const manifest = await retrieveManifest(manifestFile); const moduleName = await askModuleName(manifest, rl); diff --git a/scripts/src/templates/templates/__bundle__.ts b/scripts/src/templates/templates/__bundle__.ts index a3deb9ec0..cfd6f218f 100644 --- a/scripts/src/templates/templates/__bundle__.ts +++ b/scripts/src/templates/templates/__bundle__.ts @@ -12,7 +12,6 @@ To access things like the context or module state you can just import the context using the import below */ -import context from 'js-slang/context'; /** * Sample function. Increments a number by 1. diff --git a/scripts/src/templates/templates/__tab__.tsx b/scripts/src/templates/templates/__tab__.tsx index 34eb95b09..1eed1299b 100644 --- a/scripts/src/templates/templates/__tab__.tsx +++ b/scripts/src/templates/templates/__tab__.tsx @@ -29,7 +29,7 @@ class Repeat extends React.Component { constructor(props) { super(props); this.state = { - counter: 0, + counter: 0 }; } @@ -67,5 +67,5 @@ export default { * displayed in the side contents panel. * @see https://blueprintjs.com/docs/#icons */ - iconName: 'build', -}; \ No newline at end of file + iconName: 'build' +}; diff --git a/scripts/src/testing/__tests__/runner.test.ts b/scripts/src/testing/__tests__/runner.test.ts index e6d93c3cf..47f25beb6 100644 --- a/scripts/src/testing/__tests__/runner.test.ts +++ b/scripts/src/testing/__tests__/runner.test.ts @@ -2,27 +2,23 @@ import type { MockedFunction } from 'jest-mock'; import getTestCommand from '..'; import * as runner from '../runner'; -jest.spyOn(runner, 'runJest') - .mockImplementation(jest.fn()); +jest.spyOn(runner, 'runJest').mockImplementation(jest.fn()); -const runCommand = (...args: string[]) => getTestCommand() - .parseAsync(args, { from: 'user' }); +const runCommand = (...args: string[]) => + getTestCommand().parseAsync(args, { from: 'user' }); const mockRunJest = runner.runJest as MockedFunction; test('Check that the test command properly passes options to jest', async () => { await runCommand('-u', '-w', '--srcDir', 'gg', './src/folder'); const [call] = mockRunJest.mock.calls; - expect(call[0]) - .toEqual(['-u', '-w', './src/folder']); - expect(call[1]) - .toEqual('gg'); + expect(call[0]).toEqual(['-u', '-w', './src/folder']); + expect(call[1]).toEqual('gg'); }); test('Check that the test command handles windows paths as posix paths', async () => { await runCommand('.\\src\\folder'); const [call] = mockRunJest.mock.calls; - expect(call[0]) - .toEqual(['./src/folder']); + expect(call[0]).toEqual(['./src/folder']); }); diff --git a/scripts/src/testing/index.ts b/scripts/src/testing/index.ts index b6c606ee4..8592e9356 100644 --- a/scripts/src/testing/index.ts +++ b/scripts/src/testing/index.ts @@ -6,26 +6,32 @@ import { srcDirOption } from '@src/commandUtils'; import { runJest } from './runner'; export type TestCommandOptions = { - srcDir: string + srcDir: string; }; -const getTestCommand = () => new Command('test') - .description('Run jest') - .addOption(srcDirOption) - .allowUnknownOption() - .action(({ srcDir }, command) => { - const [args, filePatterns] = lodash.partition(command.args, arg => arg.startsWith('-')); +const getTestCommand = () => + new Command('test') + .description('Run jest') + .addOption(srcDirOption) + .allowUnknownOption() + .action(({ srcDir }, command) => { + const [args, filePatterns] = lodash.partition(command.args, arg => + arg.startsWith('-') + ); - // command.args automatically includes the source directory option - // which is not supported by Jest, so we need to remove it - const toRemove = args.findIndex(arg => arg.startsWith('--srcDir')); - if (toRemove !== -1) { - args.splice(toRemove, 1); - } + // command.args automatically includes the source directory option + // which is not supported by Jest, so we need to remove it + const toRemove = args.findIndex(arg => arg.startsWith('--srcDir')); + if (toRemove !== -1) { + args.splice(toRemove, 1); + } - const jestArgs = args.concat(filePatterns.map(pattern => pattern.split(pathlib.win32.sep) - .join(pathlib.posix.sep))); - return runJest(jestArgs, srcDir); - }); + const jestArgs = args.concat( + filePatterns.map(pattern => + pattern.split(pathlib.win32.sep).join(pathlib.posix.sep) + ) + ); + return runJest(jestArgs, srcDir); + }); export default getTestCommand; diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 897b90573..71e5118ef 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -11,4 +11,4 @@ }, "include": ["./src", "jest.setup.ts"], "exclude": ["./src/templates/templates/**"] -} \ No newline at end of file +} From 246ad1391dd6669a6433d6a2edcde5916198ef2d Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Tue, 26 Mar 2024 21:51:04 +0800 Subject: [PATCH 03/39] Reformat tabs with Prettier --- src/tabs/ArcadeTwod/index.tsx | 8 +- src/tabs/CopyGc/index.tsx | 126 +++++------ src/tabs/CopyGc/style.tsx | 2 +- src/tabs/Csg/canvas_holder.tsx | 195 +++++++++-------- src/tabs/Csg/hover_control_hint.tsx | 43 ++-- src/tabs/Csg/index.tsx | 12 +- src/tabs/Curve/__tests__/Curve.tsx | 30 ++- src/tabs/Curve/animation_canvas_3d_curve.tsx | 208 +++++++++--------- src/tabs/Curve/canvas_3d_curve.tsx | 109 +++++----- src/tabs/Curve/index.tsx | 49 +++-- src/tabs/Game/constants.ts | 2 +- src/tabs/MarkSweep/index.tsx | 72 +++--- src/tabs/MarkSweep/style.tsx | 2 +- src/tabs/Painter/index.tsx | 62 +++--- src/tabs/Pixnflix/index.tsx | 24 +- src/tabs/Plotly/index.tsx | 62 +++--- src/tabs/Repl/index.tsx | 118 ++++++---- src/tabs/Rune/__tests__/Rune.tsx | 8 +- src/tabs/Rune/index.tsx | 21 +- src/tabs/Sound/index.tsx | 11 +- src/tabs/SoundMatrix/index.tsx | 2 +- src/tabs/StereoSound/index.tsx | 16 +- src/tabs/UnityAcademy/index.tsx | 212 +++++++++++++----- src/tabs/common/AnimationCanvas.tsx | 217 ++++++++++--------- src/tabs/common/AutoLoopSwitch.tsx | 26 ++- src/tabs/common/ButtonComponent.tsx | 18 +- src/tabs/common/ModalDiv.tsx | 22 +- src/tabs/common/MultItemDisplay.tsx | 29 ++- src/tabs/common/PlayButton.tsx | 26 ++- src/tabs/common/WebglCanvas.tsx | 4 +- src/tabs/common/testUtils.ts | 15 +- src/tabs/physics_2d/DebugDrawCanvas.tsx | 40 ++-- src/tabs/physics_2d/index.tsx | 12 +- 33 files changed, 1018 insertions(+), 785 deletions(-) diff --git a/src/tabs/ArcadeTwod/index.tsx b/src/tabs/ArcadeTwod/index.tsx index b2e2e5103..24867c9e9 100644 --- a/src/tabs/ArcadeTwod/index.tsx +++ b/src/tabs/ArcadeTwod/index.tsx @@ -90,7 +90,11 @@ class GameTab extends React.Component { componentDidMount() { // Only mount the component when the Arcade2D tab is active - if (document.querySelector('[id="bp4-tab-panel_side-content-tabs_Arcade2D Tab"]')?.ariaHidden === 'true') { + if ( + document.querySelector( + '[id="bp4-tab-panel_side-content-tabs_Arcade2D Tab"]' + )?.ariaHidden === 'true' + ) { return; } @@ -136,7 +140,7 @@ class GameTab extends React.Component { }} >
- this.toggleGamePause(p)} /> + this.toggleGamePause(p)} />
); } diff --git a/src/tabs/CopyGc/index.tsx b/src/tabs/CopyGc/index.tsx index 0ed587331..78bde832e 100644 --- a/src/tabs/CopyGc/index.tsx +++ b/src/tabs/CopyGc/index.tsx @@ -1,4 +1,4 @@ -import { Slider, Icon } from '@blueprintjs/core'; +import { Icon, Slider } from '@blueprintjs/core'; import React from 'react'; import { COMMAND } from '../../bundles/copy_gc/types'; import { ThemeColor } from './style'; @@ -57,9 +57,9 @@ class CopyGC extends React.Component { componentDidMount() { const { debuggerContext } = this.props; if ( - debuggerContext - && debuggerContext.result - && debuggerContext.result.value + debuggerContext && + debuggerContext.result && + debuggerContext.result.value ) { this.initialize_state(); } @@ -187,12 +187,12 @@ class CopyGC extends React.Component { return commandHeap.length; }; - private isTag = (tag) => { + private isTag = tag => { const { tags } = this.state; return tags ? tags.includes(tag) : false; }; - private getMemoryColor = (indexValue) => { + private getMemoryColor = indexValue => { const { heap } = this.state; const value = heap ? heap[indexValue] : 0; @@ -209,7 +209,7 @@ class CopyGC extends React.Component { return color; }; - private getBackgroundColor = (indexValue) => { + private getBackgroundColor = indexValue => { const { firstChild } = this.state; const { lastChild } = this.state; const { commandHeap, value } = this.state; @@ -264,38 +264,34 @@ class CopyGC extends React.Component { marginTop: 10 }} > - {state.leftDesc - ? ( -
- - {state.leftDesc} -
- ) - : ( - false - )} - {state.rightDesc - ? ( -
- - {state.rightDesc} -
- ) - : ( - false - )} + {state.leftDesc ? ( +
+ + {state.leftDesc} +
+ ) : ( + false + )} + {state.rightDesc ? ( +
+ + {state.rightDesc} +
+ ) : ( + false + )}

@@ -323,17 +319,19 @@ class CopyGC extends React.Component {

{state.toSpace === 0 ? 'To Space' : 'From Space'}

- {toMemoryMatrix - && toMemoryMatrix.length > 0 - && toMemoryMatrix.map((item, row) => ( -
+ {toMemoryMatrix && + toMemoryMatrix.length > 0 && + toMemoryMatrix.map((item, row) => ( +
{row * state.column} - {item - && item.length > 0 - && item.map((content) => { + {item && + item.length > 0 && + item.map(content => { const color = this.getMemoryColor(content); const bgColor = this.getBackgroundColor(content); return ( @@ -360,18 +358,20 @@ class CopyGC extends React.Component {

{state.toSpace > 0 ? 'To Space' : 'From Space'}

- {fromMemoryMatrix - && fromMemoryMatrix.length > 0 - && fromMemoryMatrix.map((item, row) => ( -
+ {fromMemoryMatrix && + fromMemoryMatrix.length > 0 && + fromMemoryMatrix.map((item, row) => ( +
{row * state.column + state.memorySize / 2} {item && item.length > 0 - ? item.map((content) => { + ? item.map(content => { const color = this.getMemoryColor(content); const bgColor = this.getBackgroundColor(content); return ( @@ -397,11 +397,13 @@ class CopyGC extends React.Component {
-
+
{ - private readonly canvasReference: React.RefObject = React.createRef(); + private readonly canvasReference: React.RefObject = + React.createRef(); private statefulRenderer: StatefulRenderer | null = null; @@ -32,9 +39,8 @@ export default class CanvasHolder extends React.Component< const { current: canvas } = this.canvasReference; if (canvas === null) return; - const renderGroups: RenderGroup[] = Core - .getRenderGroupManager() - .getGroupsToRender(); + const renderGroups: RenderGroup[] = + Core.getRenderGroupManager().getGroupsToRender(); const lastRenderGroup: RenderGroup = renderGroups.at(-1) as RenderGroup; this.statefulRenderer = new StatefulRenderer( @@ -59,105 +65,108 @@ export default class CanvasHolder extends React.Component< // canvasReference via the ref attribute, for imperatively modifying the // canvas render() { - return <> -
+ return ( + <>
- - - - - -
- -
- + + + + + +
+ +
+ > + +
-
-
-

- WebGL Context Lost -

- -

- Your GPU is probably busy. Waiting for browser to re-establish connection... -

-
- ; +

+ WebGL Context Lost +

+ +

+ Your GPU is probably busy. Waiting for browser to re-establish + connection... +

+
+ + ); } } diff --git a/src/tabs/Csg/hover_control_hint.tsx b/src/tabs/Csg/hover_control_hint.tsx index 5fdf3c04b..180160858 100644 --- a/src/tabs/Csg/hover_control_hint.tsx +++ b/src/tabs/Csg/hover_control_hint.tsx @@ -1,32 +1,35 @@ /* [Imports] */ import { Icon, Tooltip } from '@blueprintjs/core'; import React from 'react'; -import { BP_ICON_COLOR, SA_TAB_BUTTON_WIDTH, SA_TAB_ICON_SIZE } from '../common/css_constants'; +import { + BP_ICON_COLOR, + SA_TAB_BUTTON_WIDTH, + SA_TAB_ICON_SIZE +} from '../common/css_constants'; import type { HintProps } from './types'; /* [Main] */ export default class HoverControlHint extends React.Component { render() { - return
- - - -
; + + + +
+ ); } } diff --git a/src/tabs/Csg/index.tsx b/src/tabs/Csg/index.tsx index 00a42cf4f..94ee2455f 100644 --- a/src/tabs/Csg/index.tsx +++ b/src/tabs/Csg/index.tsx @@ -10,21 +10,17 @@ import CanvasHolder from './canvas_holder'; export default { // Called by the frontend to decide whether to spawn the CSG tab toSpawn(debuggerContext: DebuggerContext): boolean { - const moduleState: CsgModuleState = debuggerContext.context.moduleContexts.csg.state; + const moduleState: CsgModuleState = + debuggerContext.context.moduleContexts.csg.state; // toSpawn() is checked before the frontend calls body() if needed, so we // initialise Core for the first time over on the tabs' end here Core.initialize(moduleState); - return Core.getRenderGroupManager() - .shouldRender(); + return Core.getRenderGroupManager().shouldRender(); }, // Called by the frontend to know what to render in the CSG tab body(_debuggerContext: DebuggerContext): ReactElement { - return ( - - ); + return ; }, // BlueprintJS icon name diff --git a/src/tabs/Curve/__tests__/Curve.tsx b/src/tabs/Curve/__tests__/Curve.tsx index d0d87089e..4358be5d7 100644 --- a/src/tabs/Curve/__tests__/Curve.tsx +++ b/src/tabs/Curve/__tests__/Curve.tsx @@ -1,18 +1,32 @@ import { CurveTab } from '..'; -import { animate_3D_curve, animate_curve, draw_3D_connected, draw_connected } from '../../../bundles/curve'; +import { + animate_3D_curve, + animate_curve, + draw_3D_connected, + draw_connected +} from '../../../bundles/curve'; import type { CurveModuleState } from '../../../bundles/curve/types'; import { mockDebuggerContext } from '../../common/testUtils'; test('Curve animations error gracefully', () => { const badAnimation = animate_curve(1, 60, draw_connected(200), t => 1 as any); - const mockContext = mockDebuggerContext({ drawnCurves: [badAnimation] }, 'curve'); - expect() - .toMatchSnapshot(); + const mockContext = mockDebuggerContext( + { drawnCurves: [badAnimation] }, + 'curve' + ); + expect().toMatchSnapshot(); }); test('Curve 3D animations error gracefully', () => { - const badAnimation = animate_3D_curve(1, 60, draw_3D_connected(200), t => 1 as any); - const mockContext = mockDebuggerContext({ drawnCurves: [badAnimation] }, 'curve'); - expect() - .toMatchSnapshot(); + const badAnimation = animate_3D_curve( + 1, + 60, + draw_3D_connected(200), + t => 1 as any + ); + const mockContext = mockDebuggerContext( + { drawnCurves: [badAnimation] }, + 'curve' + ); + expect().toMatchSnapshot(); }); diff --git a/src/tabs/Curve/animation_canvas_3d_curve.tsx b/src/tabs/Curve/animation_canvas_3d_curve.tsx index e993efaf9..ece620f0c 100644 --- a/src/tabs/Curve/animation_canvas_3d_curve.tsx +++ b/src/tabs/Curve/animation_canvas_3d_curve.tsx @@ -6,7 +6,11 @@ import AutoLoopSwitch from '../common/AutoLoopSwitch'; import ButtonComponent from '../common/ButtonComponent'; import PlayButton from '../common/PlayButton'; import WebGLCanvas from '../common/WebglCanvas'; -import { BP_TAB_BUTTON_MARGIN, BP_TEXT_MARGIN, CANVAS_MAX_WIDTH } from '../common/css_constants'; +import { + BP_TAB_BUTTON_MARGIN, + BP_TEXT_MARGIN, + CANVAS_MAX_WIDTH +} from '../common/css_constants'; type Props = { animation: AnimatedCurve; @@ -157,7 +161,7 @@ export default class AnimationCanvas3dCurve extends React.Component< // Animation hasn't ended, so just draw the next frame this.drawFrame(); this.setState( - (prev) => ({ + prev => ({ animTimestamp: prev.animTimestamp + currentFrame }), this.reqFrame @@ -192,17 +196,14 @@ export default class AnimationCanvas3dCurve extends React.Component< * Reset button click handler */ private onResetButtonClick = () => { - this.setState( - { animTimestamp: 0 }, - () => { - if (this.state.isPlaying) { - // Force stop - this.onPlayButtonClick(); - } - - this.drawFrame(); + this.setState({ animTimestamp: 0 }, () => { + if (this.state.isPlaying) { + // Force stop + this.onPlayButtonClick(); } - ); + + this.drawFrame(); + }); }; /** @@ -212,7 +213,7 @@ export default class AnimationCanvas3dCurve extends React.Component< private onTimeSliderChange = (newValue: number) => { this.callbackTimestamp = null; this.setState( - (prev) => ({ + prev => ({ wasPlaying: prev.isPlaying, isPlaying: false, animTimestamp: newValue @@ -226,7 +227,7 @@ export default class AnimationCanvas3dCurve extends React.Component< */ private onTimeSliderRelease = () => { this.setState( - (prev) => ({ + prev => ({ isPlaying: prev.wasPlaying }), () => { @@ -256,139 +257,142 @@ export default class AnimationCanvas3dCurve extends React.Component< * Auto loop switch onChange callback */ private onSwitchChange = () => { - this.setState((prev) => ({ + this.setState(prev => ({ isAutoLooping: !prev.isAutoLooping })); }; public render() { - return
+ return (
- - - - - -
- - + + + + +
- + + + +
+
-
-
-
- {this.state.errored - ? ( -
-
+ {this.state.errored ? ( +
- -
+
+ flexDirection: 'row', + alignItems: 'center' + }} + > + +

An error occurred while running your animation!

Here's the details:

- + {this.state.errored.toString()} -
) - : ( +
+ ) : ( { + ref={r => { this.canvas = r; }} /> )} +
-
; + ); } } diff --git a/src/tabs/Curve/canvas_3d_curve.tsx b/src/tabs/Curve/canvas_3d_curve.tsx index 318ace72c..603b3bd62 100644 --- a/src/tabs/Curve/canvas_3d_curve.tsx +++ b/src/tabs/Curve/canvas_3d_curve.tsx @@ -4,7 +4,11 @@ import type { CurveDrawn } from '../../bundles/curve/curves_webgl'; import { degreesToRadians } from '../../common/utilities'; import PlayButton from '../common/PlayButton'; import WebGLCanvas from '../common/WebglCanvas'; -import { BP_TAB_BUTTON_MARGIN, BP_TEXT_MARGIN, CANVAS_MAX_WIDTH } from '../common/css_constants'; +import { + BP_TAB_BUTTON_MARGIN, + BP_TEXT_MARGIN, + CANVAS_MAX_WIDTH +} from '../common/css_constants'; type State = { /** @@ -72,7 +76,7 @@ export default class Canvas3dCurve extends React.Component { if (!this.canvas) return; this.setState( - (prevState) => ({ + prevState => ({ isRotating: !prevState.isRotating }), () => { @@ -89,7 +93,7 @@ export default class Canvas3dCurve extends React.Component { private autoRotate = () => { if (this.canvas && this.state.isRotating) { this.setState( - (prevState) => ({ + prevState => ({ ...prevState, displayAngle: prevState.displayAngle >= 360 ? 0 : prevState.displayAngle + 2 @@ -102,7 +106,7 @@ export default class Canvas3dCurve extends React.Component { } }; - private onTextBoxChange = (event) => { + private onTextBoxChange = event => { const angle = parseFloat(event.target.value); this.setState( () => ({ displayAngle: angle }), @@ -122,68 +126,69 @@ export default class Canvas3dCurve extends React.Component { } public render() { - return
+ return (
+
+ + + +
+
+
- - - - { + this.canvas = r; }} - type="number" - value={ this.state.displayAngle } - min={ 0 } - max={ 360 } - step={ 1 } - disabled={ this.state.isRotating } - onChange={ this.onTextBoxChange } />
-
- { - this.canvas = r; - }} - /> -
-
; + ); } } diff --git a/src/tabs/Curve/index.tsx b/src/tabs/Curve/index.tsx index d37045dff..48c8b529d 100644 --- a/src/tabs/Curve/index.tsx +++ b/src/tabs/Curve/index.tsx @@ -1,6 +1,10 @@ import type { CurveModuleState } from '../../bundles/curve/types'; import { glAnimation } from '../../typings/anim_types'; -import { getModuleState, type DebuggerContext, type ModuleTab } from '../../typings/type_helpers'; +import { + getModuleState, + type DebuggerContext, + type ModuleTab +} from '../../typings/type_helpers'; import AnimationCanvas from '../common/AnimationCanvas'; import MultiItemDisplay from '../common/MultItemDisplay'; import WebGLCanvas from '../common/WebglCanvas'; @@ -13,29 +17,25 @@ export const CurveTab: ModuleTab = ({ context }) => { const elemKey = i.toString(); if (glAnimation.isAnimation(curve)) { - return curve.is3D - ? ( - - ) - : ( - - ); - } - return curve.is3D() - ? ( - - ) - : ( - { - if (r) { - curve.init(r); - curve.redraw(0); - } - }} - key={elemKey} - /> + return curve.is3D ? ( + + ) : ( + ); + } + return curve.is3D() ? ( + + ) : ( + { + if (r) { + curve.init(r); + curve.redraw(0); + } + }} + key={elemKey} + /> + ); }); return ; @@ -43,7 +43,8 @@ export const CurveTab: ModuleTab = ({ context }) => { export default { toSpawn(context: DebuggerContext) { - const drawnCurves = context.context?.moduleContexts?.curve?.state?.drawnCurves; + const drawnCurves = + context.context?.moduleContexts?.curve?.state?.drawnCurves; return drawnCurves.length > 0; }, body(context: DebuggerContext) { diff --git a/src/tabs/Game/constants.ts b/src/tabs/Game/constants.ts index a6afdeac5..b925b3ade 100644 --- a/src/tabs/Game/constants.ts +++ b/src/tabs/Game/constants.ts @@ -1,5 +1,5 @@ export enum Links { gameUserGuide = 'https://github.com/source-academy/modules/wiki/%5Bgame%5D-User-Guide', gameDeveloperDocumentation = 'https://github.com/source-academy/modules/wiki/%5Bgame%5D-Developer-Documentation', - gameAPIDocumentation = 'https://source-academy.github.io/modules/documentation/modules/game.html', + gameAPIDocumentation = 'https://source-academy.github.io/modules/documentation/modules/game.html' } diff --git a/src/tabs/MarkSweep/index.tsx b/src/tabs/MarkSweep/index.tsx index 4e4217146..2a06d1824 100644 --- a/src/tabs/MarkSweep/index.tsx +++ b/src/tabs/MarkSweep/index.tsx @@ -1,4 +1,4 @@ -import { Slider, Icon } from '@blueprintjs/core'; +import { Icon, Slider } from '@blueprintjs/core'; import React from 'react'; import { ThemeColor } from './style'; @@ -57,9 +57,9 @@ class MarkSweep extends React.Component { componentDidMount() { const { debuggerContext } = this.props; if ( - debuggerContext - && debuggerContext.result - && debuggerContext.result.value + debuggerContext && + debuggerContext.result && + debuggerContext.result.value ) { this.initialize_state(); } @@ -178,19 +178,19 @@ class MarkSweep extends React.Component { private getlengthFunction = () => { const { debuggerContext } = this.props; - const commandHeap - = debuggerContext && debuggerContext.result.value + const commandHeap = + debuggerContext && debuggerContext.result.value ? debuggerContext.result.value.get_command() : []; return commandHeap.length; }; - private isTag = (tag) => { + private isTag = tag => { const { tags } = this.state; return tags ? tags.includes(tag) : false; }; - private getMemoryColor = (indexValue) => { + private getMemoryColor = indexValue => { const { heap, marked, unmarked, command } = this.state; const { debuggerContext } = this.props; const roots = debuggerContext.result.value @@ -219,7 +219,7 @@ class MarkSweep extends React.Component { return color; }; - private getBackgroundColor = (indexValue) => { + private getBackgroundColor = indexValue => { const { firstChild } = this.state; const { lastChild } = this.state; const { commandHeap, value, command } = this.state; @@ -285,22 +285,20 @@ class MarkSweep extends React.Component { {state.leftDesc}
)} - {state.rightDesc - ? ( -
- - {state.rightDesc} -
- ) - : ( - false - )} + {state.rightDesc ? ( +
+ + {state.rightDesc} +
+ ) : ( + false + )}

@@ -327,17 +325,19 @@ class MarkSweep extends React.Component {

- {memoryMatrix - && memoryMatrix.length > 0 - && memoryMatrix.map((item, row) => ( -
+ {memoryMatrix && + memoryMatrix.length > 0 && + memoryMatrix.map((item, row) => ( +
{row * state.column} - {item - && item.length > 0 - && item.map((content) => { + {item && + item.length > 0 && + item.map(content => { const color = this.getMemoryColor(content); const bgColor = this.getBackgroundColor(content); return ( @@ -365,7 +365,7 @@ class MarkSweep extends React.Component {

Queue: [ - {state.queue.map((child) => ( + {state.queue.map(child => ( {child}, ))} ] diff --git a/src/tabs/MarkSweep/style.tsx b/src/tabs/MarkSweep/style.tsx index 8817fe1f3..c1fddd6f1 100644 --- a/src/tabs/MarkSweep/style.tsx +++ b/src/tabs/MarkSweep/style.tsx @@ -5,7 +5,7 @@ export enum ThemeColor { GREEN = '#42a870', YELLOW = '#f0d60e', RED = 'red', - BLACK = 'black', + BLACK = 'black' } export const FONT = { diff --git a/src/tabs/Painter/index.tsx b/src/tabs/Painter/index.tsx index 7e243bd1e..75bcf0a4f 100644 --- a/src/tabs/Painter/index.tsx +++ b/src/tabs/Painter/index.tsx @@ -4,14 +4,14 @@ import type { DebuggerContext } from '../../typings/type_helpers'; import Modal from '../common/ModalDiv'; type Props = { - children?: never - className?: string - debuggerContext: any + children?: never; + className?: string; + debuggerContext: any; }; type State = { - modalOpen: boolean - selectedPainter: any | null + modalOpen: boolean; + selectedPainter: any | null; }; class Painter extends React.Component { @@ -31,7 +31,15 @@ class Painter extends React.Component { }; public render() { - const { context: { moduleContexts: { painter: { state: { drawnPainters } } } } } = this.props.debuggerContext; + const { + context: { + moduleContexts: { + painter: { + state: { drawnPainters } + } + } + } + } = this.props.debuggerContext; return (
@@ -52,28 +60,25 @@ class Painter extends React.Component { height: '20rem', width: '20rem' }} - > -
+ >
- { - drawnPainters.map((drawnPainter: any, id:number) => { - const divId = `plotDiv${id}`; - return ( - <> -
this.handleOpen(drawnPainter)}>Click here to open Modal
-
{ - console.log(drawnPainter); - drawnPainter.draw(divId); - }} - > -
- - ); - }) - } - + {drawnPainters.map((drawnPainter: any, id: number) => { + const divId = `plotDiv${id}`; + return ( + <> +
this.handleOpen(drawnPainter)}> + Click here to open Modal +
+
{ + console.log(drawnPainter); + drawnPainter.draw(divId); + }} + >
+ + ); + })}
); } @@ -81,7 +86,8 @@ class Painter extends React.Component { export default { toSpawn(context: DebuggerContext) { - const drawnPainters = context.context?.moduleContexts?.painter.state.drawnPainters; + const drawnPainters = + context.context?.moduleContexts?.painter.state.drawnPainters; console.log(drawnPainters); return drawnPainters.length > 0; }, diff --git a/src/tabs/Pixnflix/index.tsx b/src/tabs/Pixnflix/index.tsx index 9ca938509..6daf33c4b 100644 --- a/src/tabs/Pixnflix/index.tsx +++ b/src/tabs/Pixnflix/index.tsx @@ -15,9 +15,9 @@ import { MIN_WIDTH } from '../../bundles/pix_n_flix/constants'; import { + InputFeed, type BundlePacket, type ErrorLogger, - InputFeed, type TabsPacket } from '../../bundles/pix_n_flix/types'; @@ -31,7 +31,7 @@ enum VideoMode { Video, Still, Accepting, - Image, + Image } type State = { @@ -195,10 +195,10 @@ class PixNFlix extends React.Component { public handleUpdateDimensions = (w: number, h: number) => { if ( - w >= MIN_WIDTH - && w <= MAX_WIDTH - && h >= MIN_HEIGHT - && h <= MAX_HEIGHT + w >= MIN_WIDTH && + w <= MAX_WIDTH && + h >= MIN_HEIGHT && + h <= MAX_HEIGHT ) { this.setState({ width: w, @@ -264,9 +264,9 @@ class PixNFlix extends React.Component { */ private isPixNFlix() { return ( - this.pixNFlix - && this.pixNFlix.toReplString - && this.pixNFlix.toReplString() === '[Pix N Flix]' + this.pixNFlix && + this.pixNFlix.toReplString && + this.pixNFlix.toReplString() === '[Pix N Flix]' ); } @@ -359,7 +359,7 @@ class PixNFlix extends React.Component {
{ + ref={r => { this.$image = r; }} width={DEFAULT_WIDTH} @@ -367,7 +367,7 @@ class PixNFlix extends React.Component { style={{ display: 'none' }} />
- { - drawnPlots.map((drawnPlot: any, id:number) => { - const divId = `plotDiv${id}`; - return ( -
{ + const divId = `plotDiv${id}`; + return ( +
-
this.handleOpen(drawnPlot)}>Click here to open Modal
-
{ - drawnPlot.draw(divId); - }} - > -
+ }} + key={divId} + > +
this.handleOpen(drawnPlot)}> + Click here to open Modal
- ); - }) - } - +
{ + drawnPlot.draw(divId); + }} + >
+
+ ); + })}
); } diff --git a/src/tabs/Repl/index.tsx b/src/tabs/Repl/index.tsx index 607472d0e..6ee9106f0 100644 --- a/src/tabs/Repl/index.tsx +++ b/src/tabs/Repl/index.tsx @@ -15,23 +15,23 @@ import type { DebuggerContext } from '../../typings/type_helpers'; // eslint-disable-next-line @typescript-eslint/no-var-requires const AceEditor = require('react-ace').default; +import 'ace-builds/src-noconflict/ext-language_tools'; import 'ace-builds/src-noconflict/mode-javascript'; import 'ace-builds/src-noconflict/theme-twilight'; -import 'ace-builds/src-noconflict/ext-language_tools'; type Props = { programmableReplInstance: ProgrammableRepl; }; type State = { - editorHeight: number, - isDraggingDragBar: boolean, + editorHeight: number; + isDraggingDragBar: boolean; }; const BOX_PADDING_VALUE = 4; class ProgrammableReplGUI extends React.Component { - public replInstance : ProgrammableRepl; + public replInstance: ProgrammableRepl; private editorAreaRect; private editorInstance; constructor(data: Props) { @@ -43,19 +43,22 @@ class ProgrammableReplGUI extends React.Component { isDraggingDragBar: false }; } - private dragBarOnMouseDown = (e) => { + private dragBarOnMouseDown = e => { e.preventDefault(); this.setState({ isDraggingDragBar: true }); }; - private onMouseMove = (e) => { + private onMouseMove = e => { if (this.state.isDraggingDragBar) { - const height = Math.max(e.clientY - this.editorAreaRect.top - BOX_PADDING_VALUE * 2, MINIMUM_EDITOR_HEIGHT); + const height = Math.max( + e.clientY - this.editorAreaRect.top - BOX_PADDING_VALUE * 2, + MINIMUM_EDITOR_HEIGHT + ); this.replInstance.editorHeight = height; this.setState({ editorHeight: height }); this.editorInstance.resize(); } }; - private onMouseUp = (_e) => { + private onMouseUp = _e => { this.setState({ isDraggingDragBar: false }); }; componentDidMount() { @@ -68,27 +71,42 @@ class ProgrammableReplGUI extends React.Component { } public render() { const { editorHeight } = this.state; - const outputDivs : React.JSX.Element[] = []; + const outputDivs: React.JSX.Element[] = []; const outputStringCount = this.replInstance.outputStrings.length; for (let i = 0; i < outputStringCount; i++) { const str = this.replInstance.outputStrings[i]; if (str.outputMethod === 'richtext') { if (str.color === '') { - outputDivs.push(
); + outputDivs.push( +
+ ); } else { - outputDivs.push(
); + outputDivs.push( +
+ ); } } else if (str.color === '') { - outputDivs.push(
{ str.content }
); + outputDivs.push(
{str.content}
); } else { - outputDivs.push(
{ str.content } -
); + outputDivs.push( +
+ {str.content} +
+ ); } } return ( @@ -97,50 +115,60 @@ class ProgrammableReplGUI extends React.Component { className="programmable-repl-button" icon={IconNames.PLAY} active={true} - onClick={() => this.replInstance.runCode()}// Note: Here if I directly use "this.replInstance.RunCode" instead using this lambda function, the "this" reference will become undefined and lead to a runtime error when user clicks the "Run" button + onClick={() => this.replInstance.runCode()} // Note: Here if I directly use "this.replInstance.RunCode" instead using this lambda function, the "this" reference will become undefined and lead to a runtime error when user clicks the "Run" button text="Run" />

The actual frame rate depends on your device's performance.

{highFPSWarning} -
-
Code Examples: Click Here
-
3D Prefab Information: Click Here{dimensionMode === '2d' && ' (You need 3D mode to use prefabs.)'}
-
-
Please note that before using Unity Academy and this module, you must agree to our User Agreement
-
- {getInstance() - .getUserAgreementStatus() === 'new_user_agreement' &&
The User Agreement has updated.
} - { - if (e !== null) { - e.checked = (getInstance() - .getUserAgreementStatus() === 'agreed'); - this.userAgreementCheckboxChecked = e.checked; - } - }} onChange={(event : React.ChangeEvent) => { - this.userAgreementCheckboxChecked = event.target.checked; - getInstance() - .setUserAgreementStatus(this.userAgreementCheckboxChecked); - }} /> +
+
+ Code Examples:{' '} + + Click Here + +
+
+ 3D Prefab Information:{' '} + + Click Here + + {dimensionMode === '2d' && ' (You need 3D mode to use prefabs.)'} +
+
+
+ Please note that before using Unity Academy and this module, you must + agree to our{' '} + + User Agreement + +
+
+ {getInstance().getUserAgreementStatus() === 'new_user_agreement' && ( +
+ The User Agreement has updated. +
+
+ )} + { + if (e !== null) { + e.checked = getInstance().getUserAgreementStatus() === 'agreed'; + this.userAgreementCheckboxChecked = e.checked; + } + }} + onChange={(event: React.ChangeEvent) => { + this.userAgreementCheckboxChecked = event.target.checked; + getInstance().setUserAgreementStatus( + this.userAgreementCheckboxChecked + ); + }} + />
); } - openUnityWindow(resolution : number) : void { + openUnityWindow(resolution: number): void { if (!this.userAgreementCheckboxChecked) { - alert('You must agree to the our User Agreement before using Unity Academy and this module!'); + alert( + 'You must agree to the our User Agreement before using Unity Academy and this module!' + ); return; } const INSTANCE = getInstance(); if (INSTANCE === undefined) { - alert('No running Unity application found. Please rerun your code and try again.'); + alert( + 'No running Unity application found. Please rerun your code and try again.' + ); return; } INSTANCE.setShowUnityComponent(resolution); diff --git a/src/tabs/common/AnimationCanvas.tsx b/src/tabs/common/AnimationCanvas.tsx index 8ff2dbe7a..1af2a9770 100644 --- a/src/tabs/common/AnimationCanvas.tsx +++ b/src/tabs/common/AnimationCanvas.tsx @@ -6,7 +6,11 @@ import AutoLoopSwitch from './AutoLoopSwitch'; import ButtonComponent from './ButtonComponent'; import PlayButton from './PlayButton'; import WebGLCanvas from './WebglCanvas'; -import { BP_TAB_BUTTON_MARGIN, BP_TEXT_MARGIN, CANVAS_MAX_WIDTH } from './css_constants'; +import { + BP_TAB_BUTTON_MARGIN, + BP_TEXT_MARGIN, + CANVAS_MAX_WIDTH +} from './css_constants'; type AnimCanvasProps = { animation: glAnimation; @@ -104,21 +108,23 @@ export default class AnimationCanvas extends React.Component< this.reqframeId = requestAnimationFrame(this.animationCallback); }; - private startAnimation = () => this.setState( - { - isPlaying: true - }, - this.reqFrame - ); - - private stopAnimation = () => this.setState( - { - isPlaying: false - }, - () => { - this.callbackTimestamp = null; - } - ); + private startAnimation = () => + this.setState( + { + isPlaying: true + }, + this.reqFrame + ); + + private stopAnimation = () => + this.setState( + { + isPlaying: false + }, + () => { + this.callbackTimestamp = null; + } + ); /** * Callback to use with `requestAnimationFrame` @@ -159,7 +165,7 @@ export default class AnimationCanvas extends React.Component< } else { // Animation hasn't ended, so just draw the next frame this.setState( - (prev) => ({ + prev => ({ animTimestamp: prev.animTimestamp + currentFrame }), () => { @@ -182,17 +188,14 @@ export default class AnimationCanvas extends React.Component< * Reset button click handler */ private onResetButtonClick = () => { - this.setState( - { animTimestamp: 0 }, - () => { - if (this.state.isPlaying) { - // Force stop - this.onPlayButtonClick(); - } - - this.drawFrame(); + this.setState({ animTimestamp: 0 }, () => { + if (this.state.isPlaying) { + // Force stop + this.onPlayButtonClick(); } - ); + + this.drawFrame(); + }); }; /** @@ -202,7 +205,7 @@ export default class AnimationCanvas extends React.Component< private onSliderChange = (newValue: number) => { this.callbackTimestamp = null; this.setState( - (prev) => ({ + prev => ({ wasPlaying: prev.isPlaying, isPlaying: false, animTimestamp: newValue @@ -216,7 +219,7 @@ export default class AnimationCanvas extends React.Component< */ private onSliderRelease = () => { this.setState( - (prev) => ({ + prev => ({ isPlaying: prev.wasPlaying }), () => { @@ -233,115 +236,121 @@ export default class AnimationCanvas extends React.Component< * Auto loop switch onChange callback */ private onSwitchChange = () => { - this.setState((prev) => ({ + this.setState(prev => ({ isAutoLooping: !prev.isAutoLooping })); }; public render() { - return
+ return (
- - - - - - - - + onClick={this.onPlayButtonClick} + /> + + + + + + + +
-
-
- {this.state.errored - ? ( -
-
+ {this.state.errored ? ( +
- -
+
+ flexDirection: 'row', + alignItems: 'center' + }} + > + +

An error occurred while running your animation!

Here's the details:

- + {this.state.errored.toString()} -
) - : ( +
+ ) : ( { + ref={r => { this.canvas = r; }} /> )} +
-
; + ); } } diff --git a/src/tabs/common/AutoLoopSwitch.tsx b/src/tabs/common/AutoLoopSwitch.tsx index 13cec0379..75cd8f92d 100644 --- a/src/tabs/common/AutoLoopSwitch.tsx +++ b/src/tabs/common/AutoLoopSwitch.tsx @@ -3,23 +3,25 @@ import { Switch, type SwitchProps } from '@blueprintjs/core'; import React from 'react'; export type AutoLoopSwitchProps = Omit & { - isAutoLooping: boolean, + isAutoLooping: boolean; }; /* [Main] */ export default class AutoLoopSwitch extends React.Component { render() { - return ; + // Prevent label from wrapping at every letter when width is small + whiteSpace: 'nowrap' + }} + label="Auto Loop" + checked={this.props.isAutoLooping} + {...this.props} + /> + ); } } diff --git a/src/tabs/common/ButtonComponent.tsx b/src/tabs/common/ButtonComponent.tsx index 4eb3d68ab..50af4759c 100644 --- a/src/tabs/common/ButtonComponent.tsx +++ b/src/tabs/common/ButtonComponent.tsx @@ -10,9 +10,9 @@ const defaultOptions = { }; type Props = { - onClick?: MouseEventHandler, - disabled?: boolean, - children?: ReactNode, + onClick?: MouseEventHandler; + disabled?: boolean; + children?: ReactNode; }; const ButtonComponent = (props: Props) => { @@ -20,12 +20,10 @@ const ButtonComponent = (props: Props) => { ...defaultOptions, ...props }; - return props.disabled - ? ( - - ) - : ( -
@@ -240,16 +225,12 @@ class Unity3DTab extends React.Component { openUnityWindow(resolution: number): void { if (!this.userAgreementCheckboxChecked) { - alert( - 'You must agree to the our User Agreement before using Unity Academy and this module!' - ); + alert('You must agree to the our User Agreement before using Unity Academy and this module!'); return; } const INSTANCE = getInstance(); if (INSTANCE === undefined) { - alert( - 'No running Unity application found. Please rerun your code and try again.' - ); + alert('No running Unity application found. Please rerun your code and try again.'); return; } INSTANCE.setShowUnityComponent(resolution); From 7c084cb8cc8a5636ed41d19006730fe3556994be Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:55:57 +0800 Subject: [PATCH 38/39] Add empty line to EOFs --- .vscode/settings.json | 2 +- modules.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index df07be32f..9ae728d45 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "eslint.experimental.useFlatConfig": true, "files.eol": "\r\n", -} \ No newline at end of file +} diff --git a/modules.json b/modules.json index 914bd7bc8..b3e413979 100644 --- a/modules.json +++ b/modules.json @@ -117,4 +117,4 @@ "Nbody" ] } -} \ No newline at end of file +} From f1d8961919f4efdf59cd8cecc52a225c3e964185 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:59:35 +0800 Subject: [PATCH 39/39] Update submodule to use relative path Allows for both SSH + HTTPS support --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index c1655131a..f8c2f2871 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "src/java/java-packages"] path = src/java/java-packages - url = https://github.com/source-academy/java-packages + url = ../../source-academy/java-packages.git