diff --git a/package.json b/package.json index 8dda400d..1a790549 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "monaco-textmate": "^3.0.1", "onigasm": "^2.2.5", "prettier": "^2.7.1", + "resolve.exports": "^1.1.0", "rollup": "^2.78.1", "solid-dismiss": "^1.2.1", "solid-heroicons": "^2.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48bbe07d..87cf8dfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,7 @@ specifiers: onigasm: ^2.2.5 prettier: ^2.7.1 register-service-worker: ^1.7.2 + resolve.exports: ^1.1.0 rollup: ^2.78.1 rollup-plugin-delete: ^2.0.0 rollup-plugin-import-css: ^3.0.3 @@ -65,9 +66,10 @@ dependencies: monaco-textmate: 3.0.1_onigasm@2.2.5 onigasm: 2.2.5 prettier: 2.7.1 + resolve.exports: 1.1.0 rollup: 2.78.1 solid-dismiss: 1.2.1_solid-js@1.4.8 - solid-heroicons: 2.0.3 + solid-heroicons: 2.0.3_solid-js@1.4.8 solid-js: 1.4.8 devDependencies: @@ -3864,6 +3866,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /resolve.exports/1.1.0: + resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} + engines: {node: '>=10'} + dev: false + /resolve/1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -4040,8 +4047,10 @@ packages: solid-js: 1.4.8 dev: false - /solid-heroicons/2.0.3: + /solid-heroicons/2.0.3_solid-js@1.4.8: resolution: {integrity: sha512-sHlEaCaFFD8s/RDWTlmfIC/5dUPOtRB/UqOTpXQLq/BUZ94jWS58CANwONBclxwKFFGu6iasaP5zN4iYqORlVg==} + peerDependencies: + solid-js: '>= ^1.2.5' dependencies: solid-js: 1.4.8 dev: false diff --git a/src/components/editor/index.tsx b/src/components/editor/index.tsx index e833ea7d..da261b8e 100644 --- a/src/components/editor/index.tsx +++ b/src/components/editor/index.tsx @@ -1,8 +1,9 @@ import { Component, createEffect, onMount, onCleanup } from 'solid-js'; import { Uri, languages, editor as mEditor, KeyMod, KeyCode } from 'monaco-editor'; -import { liftOff } from './setupSolid'; import { useZoom } from '../../hooks/useZoom'; import type { Repl } from 'solid-repl/lib/repl'; +import loadDefinitions from './loadDefinitions'; +import setupMonaco from './setupMonaco'; const Editor: Component<{ url: string; @@ -70,13 +71,14 @@ const Editor: Component<{ editor.onDidChangeModelContent(() => { props.onDocChange?.(editor.getValue()); + loadDefinitions(editor.getValue()); }); }); onCleanup(() => editor?.dispose()); createEffect(() => { editor.setModel(model()); - liftOff(); + setupMonaco(); }); createEffect(() => { diff --git a/src/components/editor/TypeScriptReact.tmLanguage.json b/src/components/editor/languages/TypeScriptReact.tmLanguage.json similarity index 99% rename from src/components/editor/TypeScriptReact.tmLanguage.json rename to src/components/editor/languages/TypeScriptReact.tmLanguage.json index 325fa06d..1636bd17 100644 --- a/src/components/editor/TypeScriptReact.tmLanguage.json +++ b/src/components/editor/languages/TypeScriptReact.tmLanguage.json @@ -3055,4 +3055,4 @@ }, "jsx-tag-attributes-illegal": { "name": "invalid.illegal.attribute.tsx", "match": "\\S+" } } -} +} \ No newline at end of file diff --git a/src/components/editor/css.tmLanguage.json b/src/components/editor/languages/css.tmLanguage.json similarity index 99% rename from src/components/editor/css.tmLanguage.json rename to src/components/editor/languages/css.tmLanguage.json index 7283f885..0cc23d7b 100644 --- a/src/components/editor/css.tmLanguage.json +++ b/src/components/editor/languages/css.tmLanguage.json @@ -1075,4 +1075,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/components/editor/loadDefinitions.ts b/src/components/editor/loadDefinitions.ts new file mode 100644 index 00000000..fcf2bb9d --- /dev/null +++ b/src/components/editor/loadDefinitions.ts @@ -0,0 +1,169 @@ +import { editor, languages, Uri } from 'monaco-editor'; +import { resolve } from 'resolve.exports'; + +const UNPKG = 'https://unpkg.com'; + +interface PackageJSON { + types?: string; + typings?: string; +} + +const GLOBAL_CACHE = new Set(); + +function matchURLS(str: string): string[] { + // Find all "from" expression + const fromMatches = str.match(/from ((".*")|('.*'))/g) ?? []; + // Find all "dynamic import" expression + const importMatches = str.match(/import\(((".*")|('.*'))\)/g) ?? []; + + const matches = [ + ...fromMatches.map((item) => item.replace('from ', '')), + ...importMatches + .map((item) => item.replace('import', '')) + .map((item) => item.substring(1, item.length - 1)), + ].map((item) => item.substring(1, item.length - 1)); + + return matches; +} + +function getPackageName(source: string) { + const pathname = source.split('/'); + if (source.startsWith('@')) { + return `${pathname[0]}/${pathname[1]}`; + } + return pathname[0]; +} + +function getTypes(packageName: string) { + // TODO consider namespaced packages + return `@types/${packageName}`; +} + +function resolveTypings(pkg: PackageJSON, entry: string, isSubpackage = false) { + if ('exports' in pkg) { + const result = resolve(pkg, entry, { + unsafe: true, + conditions: ['types'], + }) ?? resolve(pkg, entry, { + unsafe: true, + conditions: ['typings'], + }); + if (result) { + return result; + } + } + if (!isSubpackage) { + return pkg.types ?? pkg.typings + } + return undefined; +} + +function addDefinition( + // Content of the file + content: string, + // Path to file + uri: string, + // File type + type: string, +) { + languages.typescript.typescriptDefaults.addExtraLib( + content, + uri, + ); + + editor.createModel( + content, + type, + Uri.parse(uri), + ); +} + +const DTS_CACHE = new Set(); + +class DefLoader { + static async loadTSFile(source: string) { + // this.loadDTS(`${source}.ts`); + this.loadDTS(`${source}.d.ts`); + } + + static async loadDTS( + source: string, + ) { + if (DTS_CACHE.has(source)) { + return; + } + DTS_CACHE.add(source); + const targetPath = new URL(source, UNPKG); + const response = await fetch(targetPath); + if (response.ok) { + const dts = await response.text(); + + addDefinition(dts, `file:///node_modules/${source}`, 'typescript'); + + const imports = matchURLS(dts) ?? []; + + const splitPath = source.split('/'); + const directory = splitPath.slice(0, -1).join('/'); + + await Promise.all(imports.map((item) => { + if (item) { + if (item.startsWith('./') || item.startsWith('../')) { + const clean = item.endsWith('.js') ? item.substring(0, item.length - 3) : item; + const resolved = new URL(`${directory}/${clean}`, 'file://').pathname.substring(1); + this.loadTSFile(resolved); + this.loadTSFile(`${resolved}/index`); + } else { + this.loadPackage(item); + } + } + })); + } + } + + static async loadPackage( + // The import URL + source: string, + // referral URL + original = source, + ) { + if (GLOBAL_CACHE.has(source)) { + return; + } + GLOBAL_CACHE.add(source); + const packageName = getPackageName(source); + // Get the package.json + const targetUnpkg = new URL(packageName, UNPKG); + const response = await fetch(`${targetUnpkg}/package.json`); + const pkg = await response.json() as PackageJSON; + if (packageName !== source) { + // Attempt to resolve types + const typeDeclarations = resolveTypings(pkg, source, true); + if (typeDeclarations) { + await this.loadDTS(`${packageName}/${typeDeclarations}`); + } else { + this.loadPackage(packageName); + } + } else { + // Check for `types` or `typings` + const typeDeclarations = resolveTypings(pkg, packageName); + if (typeDeclarations) { + addDefinition(JSON.stringify(pkg), `file:///node_modules/${original}/package.json`, 'json'); + await this.loadDTS(`${packageName}/${typeDeclarations}`); + return; + } + await this.loadPackage(getTypes(packageName), original); + } + } +} + +export default async function loadDefinitions( + source: string, +): Promise { + const imports = matchURLS(source) ?? []; + + await Promise.all(imports.map((item) => { + if (item && !item.startsWith('.')) { + DefLoader.loadPackage(item); + } + })); +} diff --git a/src/components/editor/setupLanguages.ts b/src/components/editor/setupLanguages.ts new file mode 100644 index 00000000..f764c8fa --- /dev/null +++ b/src/components/editor/setupLanguages.ts @@ -0,0 +1,65 @@ +import { Registry } from 'monaco-textmate'; +import { wireTmGrammars } from 'monaco-editor-textmate'; +import * as monaco from 'monaco-editor'; +import cssDefinition from './languages/css.tmLanguage.json?url'; +import tsxDefinition from './languages/TypeScriptReact.tmLanguage.json?url'; + +const grammars = new Map(); +grammars.set('css', 'source.css'); +// grammars.set('javascript', 'source.js'); +grammars.set('javascript', 'source.js.jsx'); +// grammars.set('jsx', 'source.js.jsx'); +// grammars.set('tsx', 'source.tsx'); +// grammars.set('typescript', 'source.ts'); +grammars.set('typescript', 'source.tsx'); + +const inverseGrammars: Record = { + 'source.css': 'css', + 'source.js': 'jsx', + // 'source.js': 'javascript', + 'source.js.jsx': 'jsx', + 'source.tsx': 'tsx', + // 'source.ts': 'typescript', +}; + + +function createRegistry(): Registry { + return new Registry({ + getGrammarDefinition: async (scopeName) => { + console.log(scopeName); + switch (inverseGrammars[scopeName]) { + case 'css': + return { + format: 'json', + content: await (await fetch(cssDefinition)).text(), + }; + case 'jsx': + case 'typescript': + case 'javascript': + case 'tsx': + default: + return { + format: 'json', + content: await (await fetch(tsxDefinition)).text(), + }; + } + }, + }); +} + +let LOADED = false; + +export default async function setupLanguages( + editor: monaco.editor.ICodeEditor, +): Promise { + if (LOADED) { + return; + } + LOADED = true; + await wireTmGrammars( + monaco, + createRegistry(), + grammars, + editor, + ); +} \ No newline at end of file diff --git a/src/components/editor/setupMonaco.ts b/src/components/editor/setupMonaco.ts new file mode 100644 index 00000000..4e464d86 --- /dev/null +++ b/src/components/editor/setupMonaco.ts @@ -0,0 +1,14 @@ + +import { loadWASM } from 'onigasm'; +import onigasm from 'onigasm/lib/onigasm.wasm?url'; +import './setupThemes'; +import './setupTypescript'; + +let LOADED = false; + +export default async function setupMonaco(): Promise { + if (!LOADED) { + LOADED = true; + await loadWASM(onigasm); + } +} \ No newline at end of file diff --git a/src/components/editor/setupSolid.ts b/src/components/editor/setupSolid.ts index 825506e4..b4a8023f 100644 --- a/src/components/editor/setupSolid.ts +++ b/src/components/editor/setupSolid.ts @@ -1,113 +1,9 @@ -import { languages, editor } from 'monaco-editor'; -import vsDark from './vs_dark_good.json'; -import vsLight from './vs_light_good.json'; -import { loadWASM } from 'onigasm'; -import { Registry } from 'monaco-textmate'; -import { wireTmGrammars } from 'monaco-editor-textmate'; -import typescriptReactTM from './TypeScriptReact.tmLanguage.json'; -import cssTM from './css.tmLanguage.json'; -import sPackageJson from '/node_modules/solid-js/package.json?raw'; -import sWebPackageJson from '/node_modules/solid-js/web/package.json?raw'; -import sJsxRuntime from '/node_modules/solid-js/jsx-runtime.d.ts?raw'; -import sIndex from '/node_modules/solid-js/types/index.d.ts?raw'; -import sJsx from '/node_modules/solid-js/types/jsx.d.ts?raw'; -import sArray from '/node_modules/solid-js/types/reactive/array.d.ts?raw'; -import sObservable from '/node_modules/solid-js/types/reactive/observable.d.ts?raw'; -import sScheduler from '/node_modules/solid-js/types/reactive/scheduler.d.ts?raw'; -import sSignal from '/node_modules/solid-js/types/reactive/signal.d.ts?raw'; -import sComponent from '/node_modules/solid-js/types/render/component.d.ts?raw'; -import sFlow from '/node_modules/solid-js/types/render/flow.d.ts?raw'; -import sHydration from '/node_modules/solid-js/types/render/hydration.d.ts?raw'; -import sRenderIndex from '/node_modules/solid-js/types/render/index.d.ts?raw'; -import sSuspense from '/node_modules/solid-js/types/render/Suspense.d.ts?raw'; -import sClient from '/node_modules/solid-js/web/types/client.d.ts?raw'; -import sCore from '/node_modules/solid-js/web/types/core.d.ts?raw'; -import sWebIndex from '/node_modules/solid-js/web/types/index.d.ts?raw'; -import sWebJsx from '/node_modules/solid-js/web/types/jsx.d.ts?raw'; -import sServerMock from '/node_modules/solid-js/web/types/server-mock.d.ts?raw'; -import sStoreIndex from '/node_modules/solid-js/store/types/index.d.ts?raw'; -import sStateModifier from '/node_modules/solid-js/store/types/modifiers.d.ts?raw'; -import sMutable from '/node_modules/solid-js/store/types/mutable.d.ts?raw'; -import sServer from '/node_modules/solid-js/store/types/server.d.ts?raw'; -import sStore from '/node_modules/solid-js/store/types/store.d.ts?raw'; - -// Tell monaco about the file from solid-js -function cm(source: string, path: string) { - languages.typescript.typescriptDefaults.addExtraLib(source, `file:///node_modules/solid-js/${path}`); - languages.typescript.javascriptDefaults.addExtraLib(source, `file:///node_modules/solid-js/${path}`); -} - -cm(sPackageJson, 'package.json'); -cm(sWebPackageJson, 'web/package.json'); -cm(sJsxRuntime, 'jsx-runtime.d.ts'); -cm(sIndex, 'types/index.d.ts'); -cm(sJsx, 'types/jsx.d.ts'); -cm(sArray, 'types/reactive/array.d.ts'); -cm(sObservable, 'types/reactive/mutable.d.ts'); -cm(sScheduler, 'types/reactive/scheduler.d.ts'); -cm(sSignal, 'types/reactive/signal.d.ts'); -cm(sComponent, 'types/render/component.d.ts'); -cm(sFlow, 'types/render/flow.d.ts'); -cm(sHydration, 'types/render/hydration.d.ts'); -cm(sRenderIndex, 'types/render/index.d.ts'); -cm(sSuspense, 'types/render/Suspense.d.ts'); -cm(sClient, 'web/types/client.d.ts'); -cm(sCore, 'web/types/core.d.ts'); -cm(sWebIndex, 'web/types/index.d.ts'); -cm(sWebJsx, 'web/types/jsx.d.ts'); -cm(sServerMock, 'web/types/server-mock.d.ts'); -cm(sStoreIndex, 'store/types/index.d.ts'); -cm(sStateModifier, 'store/types/modifiers.d.ts'); -cm(sMutable, 'store/types/mutable.d.ts'); -cm(sServer, 'store/types/server.d.ts'); -cm(sStore, 'store/types/store.d.ts'); - -const compilerOptions: languages.typescript.CompilerOptions = { - strict: true, - target: languages.typescript.ScriptTarget.ESNext, - module: languages.typescript.ModuleKind.ESNext, - moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs, - jsx: languages.typescript.JsxEmit.Preserve, - jsxImportSource: 'solid-js', - allowNonTsExtensions: true, -}; - -languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions); -languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions); - -let loadingWasm: Promise; - -const registry = new Registry({ - async getGrammarDefinition(scopeName) { - return { - format: 'json', - content: scopeName === 'source.tsx' ? typescriptReactTM : cssTM, - }; - }, -}); - -const grammars = new Map(); -grammars.set('typescript', 'source.tsx'); -grammars.set('javascript', 'source.tsx'); -grammars.set('css', 'source.css'); - -// monaco's built-in themes aren't powereful enough to handle TM tokens -// https://github.com/Nishkalkashyap/monaco-vscode-textmate-theme-converter#monaco-vscode-textmate-theme-converter -editor.defineTheme('vs-dark-plus', vsDark as editor.IStandaloneThemeData); -editor.defineTheme('vs-light-plus', vsLight as editor.IStandaloneThemeData); +import { languages } from 'monaco-editor'; +import setupMonaco from './setupMonaco'; const hookLanguages = languages.setLanguageConfiguration; languages.setLanguageConfiguration = (languageId: string, configuration: languages.LanguageConfiguration) => { - liftOff(); + setupMonaco(); return hookLanguages(languageId, configuration); }; - -export async function liftOff(): Promise { - if (!loadingWasm) loadingWasm = loadWASM(window.MonacoEnvironment.onigasm); - await loadingWasm; - - // wireTmGrammars only cares about the language part, but asks for all of monaco - // we fool it by just passing in an object with languages - await wireTmGrammars({ languages } as any, registry, grammars); -} diff --git a/src/components/editor/setupThemes.ts b/src/components/editor/setupThemes.ts new file mode 100644 index 00000000..113b4fa2 --- /dev/null +++ b/src/components/editor/setupThemes.ts @@ -0,0 +1,6 @@ +import * as monaco from 'monaco-editor'; +import vsDark from './themes/vs_dark_good.json'; +import vsLight from './themes/vs_light_good.json'; + +monaco.editor.defineTheme('vs-dark-plus', vsDark as monaco.editor.IStandaloneThemeData); +monaco.editor.defineTheme('vs-light-plus', vsLight as monaco.editor.IStandaloneThemeData); diff --git a/src/components/editor/setupTypescript.ts b/src/components/editor/setupTypescript.ts new file mode 100644 index 00000000..8f020709 --- /dev/null +++ b/src/components/editor/setupTypescript.ts @@ -0,0 +1,21 @@ +import { languages } from 'monaco-editor'; + +const compilerOptions: languages.typescript.CompilerOptions = { + target: languages.typescript.ScriptTarget.ESNext, + allowNonTsExtensions: true, + jsx: languages.typescript.JsxEmit.Preserve, + noEmit: true, + module: languages.typescript.ModuleKind.ESNext, + moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs, + strict: true, + noUnusedLocals: true, + noUnusedParameters: true, + noImplicitReturns: true, + noFallthroughCasesInSwitch: true, + importHelpers: true, + esModuleInterop: true, + jsxImportSource: 'solid-js', +}; + +languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions); +languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions); diff --git a/src/components/editor/vs_dark_good.json b/src/components/editor/themes/vs_dark_good.json similarity index 100% rename from src/components/editor/vs_dark_good.json rename to src/components/editor/themes/vs_dark_good.json diff --git a/src/components/editor/vs_light_good.json b/src/components/editor/themes/vs_light_good.json similarity index 100% rename from src/components/editor/vs_light_good.json rename to src/components/editor/themes/vs_light_good.json