From 2ba144f171ba4853636b306c48ef1a50dd97d414 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 18 Jan 2024 23:13:32 +0200 Subject: [PATCH 01/44] feat(compilers): render React component if it is the default export --- src/livecodes/languages/jsx/jsx-runtime.ts | 11 +++++++++ .../languages/typescript/lang-typescript.ts | 14 +++++++---- src/livecodes/result/result-page.ts | 24 ++++++++++++++++--- 3 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 src/livecodes/languages/jsx/jsx-runtime.ts diff --git a/src/livecodes/languages/jsx/jsx-runtime.ts b/src/livecodes/languages/jsx/jsx-runtime.ts new file mode 100644 index 000000000..fdcf8130b --- /dev/null +++ b/src/livecodes/languages/jsx/jsx-runtime.ts @@ -0,0 +1,11 @@ +export const reactRuntime = ` +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from './script'; +const root = createRoot(document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div'))); +root.render(React.createElement(App, null)); +`; + +export const hasCustomJsxRuntime = (code: string) => new RegExp(/\/\*\*[\s\*]*@jsx\s/g).test(code); + +export const hasDefaultExport = (code: string) => new RegExp(/export\s*default\s/).test(code); diff --git a/src/livecodes/languages/typescript/lang-typescript.ts b/src/livecodes/languages/typescript/lang-typescript.ts index e6c826dcd..7a6464900 100644 --- a/src/livecodes/languages/typescript/lang-typescript.ts +++ b/src/livecodes/languages/typescript/lang-typescript.ts @@ -2,6 +2,8 @@ import type { LanguageSpecs } from '../../models'; import { typescriptUrl } from '../../vendors'; import { getLanguageCustomSettings } from '../../utils'; import { parserPlugins } from '../prettier'; +// eslint-disable-next-line import/no-internal-modules +import { hasCustomJsxRuntime } from '../jsx/jsx-runtime'; export const typescriptOptions = { target: 'es2015', @@ -22,12 +24,16 @@ export const typescript: LanguageSpecs = { url: typescriptUrl, factory: () => - async (code, { config, language }) => - (window as any).ts.transpile(code, { + async (code, { config }) => { + if (['jsx', 'tsx'].includes(config.script.language) && !hasCustomJsxRuntime(code)) { + code = `import React from 'react';\n${code}`; + } + return (window as any).ts.transpile(code, { ...typescriptOptions, ...getLanguageCustomSettings('typescript', config), - ...getLanguageCustomSettings(language, config), - }), + ...getLanguageCustomSettings(config.script.language, config), + }); + }, }, extensions: ['ts', 'typescript'], editor: 'script', diff --git a/src/livecodes/result/result-page.ts b/src/livecodes/result/result-page.ts index 5ca39a638..c13d70e76 100644 --- a/src/livecodes/result/result-page.ts +++ b/src/livecodes/result/result-page.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-internal-modules */ import { createImportMap, createCSSModulesImportMap, @@ -7,9 +8,9 @@ import { removeImports, } from '../compiler'; import { cssPresets, getLanguageCompiler, getLanguageExtension } from '../languages'; +import { hasCustomJsxRuntime, hasDefaultExport, reactRuntime } from '../languages/jsx/jsx-runtime'; import type { Cache, EditorId, Config, CompileInfo } from '../models'; import { getAppCDN, modulesService } from '../services'; -// eslint-disable-next-line import/no-internal-modules import { testImports } from '../toolspane/test-imports'; import { addAttrs, @@ -156,6 +157,12 @@ export const createResultPage = async ({ getImports(markup).includes('./script') || (runTests && !forExport && getImports(compiledTests).includes('./script')); + const shouldInsertReactJsxRuntime = + ['jsx', 'tsx'].includes(code.script.language) && + hasDefaultExport(code.script.compiled) && + !hasCustomJsxRuntime(code.script.content || '') && + !importFromScript; + let compilerImports = {}; for (const { language, compiled } of runtimeDependencies) { @@ -218,10 +225,13 @@ export const createResultPage = async ({ ...(hasImports(code.markup.compiled) ? createImportMap(code.markup.compiled, config) : {}), + ...(shouldInsertReactJsxRuntime ? createImportMap(reactRuntime, config) : {}), ...(runTests && !forExport && hasImports(compiledTests) ? createImportMap(compiledTests, config) : {}), - ...(importFromScript ? { './script': toDataUrl(code.script.compiled) } : {}), + ...(importFromScript || shouldInsertReactJsxRuntime + ? { './script': toDataUrl(code.script.compiled) } + : {}), ...createCSSModulesImportMap( code.script.compiled, code.style.compiled, @@ -263,7 +273,7 @@ export const createResultPage = async ({ dom.head.appendChild(externalScript); }); - if (!importFromScript) { + if (!importFromScript && !shouldInsertReactJsxRuntime) { // editor script const script = code.script.compiled; const scriptElement = dom.createElement('script'); @@ -288,6 +298,14 @@ export const createResultPage = async ({ } } + // React JSX runtime + if (shouldInsertReactJsxRuntime) { + const jsxRuntimeScript = dom.createElement('script'); + jsxRuntimeScript.type = 'module'; + jsxRuntimeScript.innerHTML = reactRuntime; + dom.body.appendChild(jsxRuntimeScript); + } + // spacing if (config.showSpacing && !forExport) { const spacingScript = dom.createElement('script'); From 87936277aae60e3d5d3d6ebb2f82d966eca5c709 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Fri, 19 Jan 2024 07:49:24 +0200 Subject: [PATCH 02/44] feat(compilers): set typescript option { jsx: 'react-jsx' } --- src/livecodes/languages/jsx/jsx-runtime.ts | 4 ++-- .../languages/typescript/lang-typescript.ts | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/livecodes/languages/jsx/jsx-runtime.ts b/src/livecodes/languages/jsx/jsx-runtime.ts index fdcf8130b..33cee2a7d 100644 --- a/src/livecodes/languages/jsx/jsx-runtime.ts +++ b/src/livecodes/languages/jsx/jsx-runtime.ts @@ -1,9 +1,9 @@ export const reactRuntime = ` -import React from "react"; +import { jsx as _jsx } from "react/jsx-runtime"; import { createRoot } from "react-dom/client"; import App from './script'; const root = createRoot(document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div'))); -root.render(React.createElement(App, null)); +root.render(_jsx(App, {})); `; export const hasCustomJsxRuntime = (code: string) => new RegExp(/\/\*\*[\s\*]*@jsx\s/g).test(code); diff --git a/src/livecodes/languages/typescript/lang-typescript.ts b/src/livecodes/languages/typescript/lang-typescript.ts index 7a6464900..bb8c578c6 100644 --- a/src/livecodes/languages/typescript/lang-typescript.ts +++ b/src/livecodes/languages/typescript/lang-typescript.ts @@ -7,7 +7,7 @@ import { hasCustomJsxRuntime } from '../jsx/jsx-runtime'; export const typescriptOptions = { target: 'es2015', - jsx: 'react', + jsx: 'react-jsx', allowUmdGlobalAccess: true, esModuleInterop: true, }; @@ -24,16 +24,13 @@ export const typescript: LanguageSpecs = { url: typescriptUrl, factory: () => - async (code, { config }) => { - if (['jsx', 'tsx'].includes(config.script.language) && !hasCustomJsxRuntime(code)) { - code = `import React from 'react';\n${code}`; - } - return (window as any).ts.transpile(code, { + async (code, { config }) => + (window as any).ts.transpile(code, { ...typescriptOptions, + ...(hasCustomJsxRuntime(code) ? { jsx: 'react' } : {}), ...getLanguageCustomSettings('typescript', config), ...getLanguageCustomSettings(config.script.language, config), - }); - }, + }), }, extensions: ['ts', 'typescript'], editor: 'script', From 4a287aeddc20fe70ce2ad1eefdc38a860c0dd57b Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Fri, 19 Jan 2024 08:31:02 +0200 Subject: [PATCH 03/44] only apply {jsx: 'react-jsx'} if language is JSX or TSX this avoids interfering with other languages (e.g. Vue with JSX) --- src/livecodes/languages/typescript/lang-typescript.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/livecodes/languages/typescript/lang-typescript.ts b/src/livecodes/languages/typescript/lang-typescript.ts index bb8c578c6..16ac76dda 100644 --- a/src/livecodes/languages/typescript/lang-typescript.ts +++ b/src/livecodes/languages/typescript/lang-typescript.ts @@ -7,7 +7,7 @@ import { hasCustomJsxRuntime } from '../jsx/jsx-runtime'; export const typescriptOptions = { target: 'es2015', - jsx: 'react-jsx', + jsx: 'react', allowUmdGlobalAccess: true, esModuleInterop: true, }; @@ -27,7 +27,9 @@ export const typescript: LanguageSpecs = { async (code, { config }) => (window as any).ts.transpile(code, { ...typescriptOptions, - ...(hasCustomJsxRuntime(code) ? { jsx: 'react' } : {}), + ...(['jsx', 'tsx'].includes(config.script.language) && !hasCustomJsxRuntime(code) + ? { jsx: 'react-jsx' } + : {}), ...getLanguageCustomSettings('typescript', config), ...getLanguageCustomSettings(config.script.language, config), }), From f9a7951ef7b908dbe4b994f0230d30b037e41502 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 20 Jan 2024 09:05:46 +0200 Subject: [PATCH 04/44] feat(Types): bundle types in the browser --- package-lock.json | 346 +++++++----- scripts/build.js | 1 + src/livecodes/compiler/import-map.ts | 5 + src/livecodes/core.ts | 2 +- src/livecodes/types/bundle-types.ts | 781 +++++++++++++++++++++++++++ src/livecodes/types/type-loader.ts | 19 +- src/livecodes/vendors.ts | 2 + vendor-licenses.md | 2 + 8 files changed, 1009 insertions(+), 149 deletions(-) create mode 100644 src/livecodes/types/bundle-types.ts diff --git a/package-lock.json b/package-lock.json index ff14eed12..e792b8a40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -123,12 +123,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -186,43 +187,52 @@ } }, "node_modules/@babel/generator": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", - "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.12.11", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", - "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/helper-get-function-arity": "^7.12.10", - "@babel/template": "^7.12.7", - "@babel/types": "^7.12.11" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.12.10", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", - "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.12.10" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { @@ -300,27 +310,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", - "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.12.11" + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -338,13 +351,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -352,9 +365,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz", - "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -541,41 +554,48 @@ } }, "node_modules/@babel/template": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", - "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.12.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", - "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.10", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.10", - "@babel/types": "^7.12.10", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -4418,6 +4438,20 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -4427,6 +4461,15 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -10118,9 +10161,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -20395,12 +20438,13 @@ "dev": true }, "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" } }, "@babel/core": { @@ -20441,42 +20485,40 @@ } }, "@babel/generator": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", - "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "requires": { - "@babel/types": "^7.12.11", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" } }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, "@babel/helper-function-name": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", - "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.12.10", - "@babel/template": "^7.12.7", - "@babel/types": "^7.12.11" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, - "@babel/helper-get-function-arity": { - "version": "7.12.10", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", - "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.12.10" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -20551,24 +20593,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", - "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.12.11" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helpers": { @@ -20583,20 +20625,20 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz", - "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -20726,41 +20768,42 @@ } }, "@babel/template": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", - "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.12.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", - "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.10", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.10", - "@babel/types": "^7.12.10", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -23762,12 +23805,29 @@ } } }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -28207,9 +28267,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true }, "for-in": { diff --git a/scripts/build.js b/scripts/build.js index 019bfd74d..5d35199fd 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -221,6 +221,7 @@ const esmBuild = () => 'languages/language-info.ts', 'export/export.ts', 'sync/sync.ts', + 'types/bundle-types.ts', 'UI/open.ts', 'UI/resources.ts', 'UI/assets.ts', diff --git a/src/livecodes/compiler/import-map.ts b/src/livecodes/compiler/import-map.ts index 6b1c7c1b2..ed07b62dc 100644 --- a/src/livecodes/compiler/import-map.ts +++ b/src/livecodes/compiler/import-map.ts @@ -90,6 +90,11 @@ export const hasImports = (code: string) => getImports(code).length > 0; export const hasExports = (code: string) => new RegExp(/(^export\s)|([\s|;]export\s)/).test(removeCommentsAndStrings(code)); +export const hasUrlImportsOrExports = (code: string) => + new RegExp( + /((?:import|export)\s+?(?:(?:(?:[\w*\s{},\$]*)\s+from\s+?)|))((?:"(?:\.|http|\/).*?")|(?:'(?:\.|http|\/).*?'))([\s]*?(?:;|$|))/, + ).test(removeComments(code)); + export const hasAwait = (code: string) => new RegExp(/(^await\s)|([\s|;]await\s)/).test(removeCommentsAndStrings(code)); diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index 1931b7a4e..de8233cbb 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -3859,7 +3859,7 @@ const basicHandlers = () => { notifications = createNotifications(); modal = createModal(); split = createSplitPanes(); - typeLoader = createTypeLoader(); + typeLoader = createTypeLoader(baseUrl); handleLogoLink(); handleResize(); diff --git a/src/livecodes/types/bundle-types.ts b/src/livecodes/types/bundle-types.ts new file mode 100644 index 000000000..641eb2f42 --- /dev/null +++ b/src/livecodes/types/bundle-types.ts @@ -0,0 +1,781 @@ +// based on dts-bundle + +import { pathBrowserifyUrl } from '../vendors'; + +// const dtsExp = /\.d\.ts$/; +const bomOptExp = /^\uFEFF?/; + +const externalExp = /^([ \t]*declare module )(['"])(.+?)(\2[ \t]*{?.*)$/; +const importExp = /^([ \t]*(?:export )?(?:import .+? )= require\()(['"])(.+?)(\2\);.*)$/; +const importEs6Exp = + /^([ \t]*(?:export|import) ?(?:(?:\* (?:as [^ ,]+)?)|.*)?,? ?(?:[^ ,]+ ?,?)(?:\{(?:[^ ,]+ ?,?)*\})? ?from )(['"])([^ ,]+)(\2;.*)$/; +const referenceTagExp = /^[ \t]*\/\/\/[ \t]*.*$/; +const identifierExp = /^\w+(?:[\.-]\w+)*$/; +const fileExp = /^([\./].*|.:.*)$/; +const privateExp = /^[ \t]*(?:static )?private (?:static )?/; +const publicExp = /^([ \t]*)(static |)(public |)(static |)(.*)/; + +export interface Options { + main: string; + name: string; + baseDir?: string; + newline?: string; + indent?: string; + prefix?: string; + separator?: string; + externals?: boolean; + exclude?: ((file: string) => boolean) | RegExp; + verbose?: boolean; + referenceExternals?: boolean; + emitOnIncludedFileNotFound?: boolean; + emitOnNoIncludedFileNotFound?: boolean; + headerText?: string; +} + +export interface ModLine { + original: string; + modified?: string; + skip?: boolean; +} + +export interface Result { + file: string; + name: string; + indent: string; + exp: string; + refs: string[]; + externalImports: string[]; + relativeImports: string[]; + exports: string[]; + lines: ModLine[]; + importLineRef: ModLine[]; + relativeRef: ModLine[]; + fileExists: boolean; +} + +export interface BundleResult { + fileMap: { [name: string]: Result }; + includeFilesNotFound: string[]; + noIncludeFilesNotFound: string[]; + emitted?: boolean; + options: Options; +} + +export async function bundle(options: Options): Promise { + const path = await import(pathBrowserifyUrl); + assert(typeof options === 'object' && options, 'options must be an object'); + + // option parsing & validation + const main = options.main; + const exportName = options.name; + const baseDir = optValue(options.baseDir, options.main.split('/').slice(0, -1).join('/')); + + const newline = optValue(options.newline, '\n'); + const indent = optValue(options.indent, ' ') || ' '; + const prefix = optValue(options.prefix, ''); + const separator = optValue(options.separator, '/') || '/'; + + const externals = optValue(options.externals, false); + const exclude = optValue(options.exclude, null); + const referenceExternals = optValue(options.referenceExternals, false); + const emitOnIncludedFileNotFound = optValue(options.emitOnIncludedFileNotFound, false); + const emitOnNoIncludedFileNotFound = optValue(options.emitOnNoIncludedFileNotFound, false); + const headerText = optValue(options.headerText, ''); + + // regular (non-jsdoc) comments are not actually supported by declaration compiler + const comments = false; + + const verbose = optValue(options.verbose, false); + + assert(main, 'option "main" must be defined'); + assert(exportName, 'option "name" must be defined'); + + assert(typeof newline === 'string', 'option "newline" must be a string'); + assert(typeof indent === 'string', 'option "indent" must be a string'); + assert(typeof prefix === 'string', 'option "prefix" must be a string'); + assert(separator.length > 0, 'option "separator" must have non-zero length'); + + // turn relative paths into absolute paths + const mainFile = main; + // const outFile = calcOutFilePath(out, baseDir); + + trace('### settings object passed ###'); + traceObject(options); + + trace('### settings ###'); + trace('main: %s', main); + trace('name: %s', exportName); + trace('baseDir: %s', baseDir); + trace('mainFile: %s', mainFile); + trace('externals: %s', externals ? 'yes' : 'no'); + trace('exclude: %s', exclude); + trace('comments: %s', comments ? 'yes' : 'no'); + trace('emitOnIncludedFileNotFound: %s', emitOnIncludedFileNotFound ? 'yes' : 'no'); + trace('emitOnNoIncludedFileNotFound: %s', emitOnNoIncludedFileNotFound ? 'yes' : 'no'); + trace('headerText %s', headerText); + + const headerData = headerText ? '/*' + headerText + '*/\n' : ''; + + let isExclude: (file: string, arg?: boolean) => boolean; + if (typeof exclude === 'function') { + isExclude = exclude; + } else if (exclude instanceof RegExp) { + isExclude = (file) => exclude.test(file); + } else { + isExclude = () => false; + } + + const [urlPart1, urlPart2] = main.split(exportName, 2); + const sourceRoot = urlPart1 + exportName + urlPart2.split('/')[0] + '/'; + + trace('\n### find typings ###'); + + const inSourceTypings = (file: string) => file.startsWith(sourceRoot); // if file reference is a directory assume commonjs index.d.ts + + trace('source typings (will be included in output if actually used)'); + + // sourceTypings.forEach(file => trace(' - %s ', file)); + + trace('excluded typings (will always be excluded from output)'); + + const fileMap: { [name: string]: Result } = Object.create(null); + const globalExternalImports: string[] = []; + let mainParse: Result | null = null; // will be parsed result of first parsed file + const externalTypings: string[] = []; + const inExternalTypings = (file: string) => externalTypings.indexOf(file) !== -1; + { + // recursively parse files, starting from main file, + // following all references and imports + trace('\n### parse files ###'); + + const queue: string[] = [mainFile]; + const queueSeen: { [name: string]: boolean } = Object.create(null); + + while (queue.length > 0) { + const target = queue.shift(); + if (!target) { + continue; + } + if (queueSeen[target]) { + continue; + } + queueSeen[target] = true; + + // parse the file + const parse = await parseFile(target); + if (!parse) { + continue; + } + if (!mainParse) { + mainParse = parse; + } + fileMap[parse.file] = parse; + pushUniqueArr(queue, parse.refs, parse.relativeImports); + } + } + + // map all exports to their file + trace('\n### map exports ###'); + + const exportMap = Object.create(null); + Object.keys(fileMap).forEach((file) => { + const parse = fileMap[file]; + parse.exports.forEach((name) => { + assert(!(name in exportMap), 'already got export for: ' + name); + exportMap[name] = parse; + trace('- %s -> %s', name, parse.file); + }); + }); + + // build list of typings to include in output later + trace('\n### determine typings to include ###'); + + const excludedTypings: string[] = []; + const usedTypings: Result[] = []; + const externalDependencies: string[] = []; // lists all source files that we omit due to !externals + { + const queue = [mainParse]; + const queueSeen: { [name: string]: boolean } = Object.create(null); + + trace('queue'); + trace(queue); + + while (queue.length > 0) { + const parse = queue.shift(); + if (!parse || queueSeen[parse.file]) { + continue; + } + queueSeen[parse.file] = true; + + trace('%s (%s)', parse.name, parse.file); + + usedTypings.push(parse); + + parse.externalImports.forEach((name) => { + const p = exportMap[name]; + if (!p) return; + if (!externals) { + trace(' - exclude external %s', name); + pushUnique(externalDependencies, !p ? name : p?.file); + return; + } + if (isExclude(path.relative(baseDir, p?.file), true)) { + trace(' - exclude external filter %s', name); + pushUnique(excludedTypings, p?.file); + return; + } + trace(' - include external %s', name); + assert(p, name); + queue.push(p); + }); + parse.relativeImports.forEach((file) => { + const p = fileMap[file]; + if (!p) return; + if (isExclude(path.relative(baseDir, p?.file), false)) { + trace(' - exclude internal filter %s', file); + pushUnique(excludedTypings, p?.file); + return; + } + trace(' - import relative %s', file); + assert(p, file); + queue.push(p); + }); + } + } + + // rewrite global external modules to a unique name + trace('\n### rewrite global external modules ###'); + + usedTypings.forEach((parse) => { + trace(parse.name); + + parse.relativeRef.forEach((line) => { + line.modified = replaceExternal(line.original, getLibName); + trace(' - %s ==> %s', line.original, line.modified); + }); + + parse.importLineRef.forEach((line) => { + if (importExp.test(line.original)) { + line.modified = replaceImportExport(line.original, getLibName); + } else { + line.modified = replaceImportExportEs6(line.original, getLibName); + } + trace(' - %s ==> %s', line.original, line.modified); + }); + }); + + // build collected content + trace('\n### build output ###'); + + let content = headerData; + if (externalDependencies.length > 0) { + content += '// Dependencies for this module:' + newline; + externalDependencies.forEach((file) => { + if (referenceExternals) { + content += formatReference(path.relative(baseDir, file).replace(/\\/g, '/')) + newline; + } else { + content += '// ' + path.relative(baseDir, file).replace(/\\/g, '/') + newline; + } + }); + } + + if (globalExternalImports.length > 0) { + content += newline; + content += globalExternalImports.join(newline) + newline; + } + + content += newline; + + // add wrapped modules to output + content += + usedTypings + .filter((parse: Result) => { + // Eliminate all the skipped lines + parse.lines = parse.lines.filter((line: ModLine) => true !== line.skip); + + // filters empty parse objects. + return parse.lines.length > 0; + }) + .map((parse: Result) => { + if (inSourceTypings(parse.file)) { + return formatModule( + parse.file, + parse.lines.map((line) => getIndenter(parse.indent, indent)(line)), + ); + } else { + return ( + parse.lines.map((line) => getIndenter(parse.indent, indent)(line)).join(newline) + + newline + ); + } + }) + .join(newline) + newline; + + const inUsed = (file: string): boolean => + usedTypings.filter((parse) => parse.file === file).length !== 0; + + const bundleResult: BundleResult = { + fileMap, + includeFilesNotFound: [], + noIncludeFilesNotFound: [], + options, + }; + + trace('## files not found ##'); + // eslint-disable-next-line guard-for-in + for (const p in fileMap) { + const parse = fileMap[p]; + if (!parse.fileExists) { + if (inUsed(parse.file)) { + bundleResult.includeFilesNotFound.push(parse.file); + warning(' X Included file NOT FOUND %s ', parse.file); + } else { + bundleResult.noIncludeFilesNotFound.push(parse.file); + trace(' X Not used file not found %s', parse.file); + } + } + } + + // write main file + trace('\n### write output ###'); + // write only if there aren't not found files or there are and option "emit file not found" is true. + if ( + (bundleResult.includeFilesNotFound.length === 0 || + (bundleResult.includeFilesNotFound.length > 0 && emitOnIncludedFileNotFound)) && + (bundleResult.noIncludeFilesNotFound.length === 0 || + (bundleResult.noIncludeFilesNotFound.length > 0 && emitOnNoIncludedFileNotFound)) + ) { + bundleResult.emitted = true; + } else { + warning(' XXX Not emit due to exist files not found.'); + trace( + 'See documentation for emitOnIncludedFileNotFound and emitOnNoIncludedFileNotFound options.', + ); + bundleResult.emitted = false; + } + + // print some debug info + if (verbose) { + trace('\n### statistics ###'); + + // trace('used sourceTypings'); + // sourceTypings.forEach(p => { + // if (inUsed(p)) { + // trace(' - %s', p); + // } + // }); + + // trace('unused sourceTypings'); + // sourceTypings.forEach(p => { + // if (!inUsed(p)) { + // trace(' - %s', p); + // } + // }); + + trace('excludedTypings'); + excludedTypings.forEach((p) => { + trace(' - %s', p); + }); + + trace('used external typings'); + externalTypings.forEach((p) => { + if (inUsed(p)) { + trace(' - %s', p); + } + }); + + trace('unused external typings'); + externalTypings.forEach((p) => { + if (!inUsed(p)) { + trace(' - %s', p); + } + }); + + trace('external dependencies'); + externalDependencies.forEach((p) => { + trace(' - %s', p); + }); + } + + trace('\n### done ###\n'); + return content; + + function assert(condition: any, msg?: string) { + if (!condition && verbose) { + // eslint-disable-next-line no-console + console.error(msg || 'assertion failed'); + } + } + + function traceObject(obj: any) { + if (verbose) { + // eslint-disable-next-line no-console + console.log(obj); + } + } + + function trace(...args: any[]) { + if (verbose) { + // eslint-disable-next-line no-console + console.log(...args); + } + } + + function warning(...args: any[]) { + if (verbose) { + // eslint-disable-next-line no-console + console.log(...args); + } + } + + function getModName(file: string) { + return path.relative( + baseDir, + path.dirname(file) + path.sep + path.basename(file).replace(/\.d\.ts$/, ''), + ); + } + + function getExpName(file: string) { + if (file === mainFile) { + return exportName; + } + return getExpNameRaw(file); + } + + function getExpNameRaw(file: string) { + return prefix + exportName + separator + cleanupName(getModName(file)); + } + + function getLibName(ref: string) { + return getExpNameRaw(mainFile) + separator + prefix + separator + ref; + } + + function cleanupName(name: string) { + return name.replace(/\.\./g, '--').replace(/[\\\/]/g, separator); + } + + function mergeModulesLines(lines: any) { + const i = indent; + return (lines.length === 0 ? '' : i + lines.join(newline + i)) + newline; + } + + function formatModule(file: string, lines: string[]) { + let out = ''; + out += "declare module '" + getExpName(file) + "' {" + newline; + out += mergeModulesLines(lines); + out += '}' + newline; + return out; + } + + // main info extractor + async function parseFile(file: string): Promise { + const name = getModName(file); + + trace('%s (%s)', name, file); + + const res: Result = { + file, + name, + indent, + exp: getExpName(file), + refs: [], // triple-slash references + externalImports: [], // import()'s like "events" + relativeImports: [], // import()'s like "./foo" + exports: [], + lines: [], + fileExists: true, + // the next two properties contain single-element arrays, which reference the same single-element in .lines, + // in order to be able to replace their contents later in the bundling process. + importLineRef: [], + relativeRef: [], + }; + let response = await fetch(file); + if (!response.ok) { + // if file is a directory then lets assume commonjs convention of an index file in the given folder + file = file + '/index.d.ts'; + response = await fetch(file); + if (!response.ok) { + trace(' X - File not found: %s', file); + res.fileExists = false; + return res; + } + } + + let code = (await response.text()).replace(bomOptExp, '').replace(/\s*$/, ''); + + if (code.includes(sourceRoot)) { + // if module is imported from same the package with absolute URL make it relative + const dir = file.substring(0, file.lastIndexOf('/')) + '/'; + code = code.replace(new RegExp(regexEscape(sourceRoot) + '(.*)', 'g'), (match) => + path.relative(dir, match), + ); + } + + res.indent = indent || ' '; + + // buffer multi-line comments, handle JSDoc + let multiComment: string[] = []; + let queuedJSDoc: string[] | null; + let inBlockComment = false; + const popBlock = () => { + if (multiComment.length > 0) { + // jsdoc + if (/^[ \t]*\/\*\*/.test(multiComment[0])) { + // flush but hold + queuedJSDoc = multiComment; + } else if (comments) { + // flush it + multiComment.forEach((line) => res.lines.push({ original: line })); + } + multiComment = []; + } + inBlockComment = false; + }; + const popJSDoc = () => { + if (queuedJSDoc) { + queuedJSDoc.forEach((line) => { + // fix shabby TS JSDoc output + const match = line.match(/^([ \t]*)(\*.*)/); + if (match) { + res.lines.push({ original: match[1] + ' ' + match[2] }); + } else { + res.lines.push({ original: line }); + } + }); + queuedJSDoc = null; + } + }; + + for (let line of code.split('\n')) { + let match: string[] | null; + + // block comment end + if (/^[((=====)(=*)) \t]*\*+\//.test(line)) { + multiComment.push(line); + popBlock(); + continue; + } + + // block comment start + if (/^[ \t]*\/\*/.test(line)) { + multiComment.push(line); + inBlockComment = true; + + // single line block comment + if (/\*+\/[ \t]*$/.test(line)) { + popBlock(); + } + continue; + } + + if (inBlockComment) { + multiComment.push(line); + continue; + } + + // blankline + if (/^\s*$/.test(line)) { + res.lines.push({ original: '' }); + continue; + } + + // reference tag + if (/^\/\/\//.test(line)) { + const ref = extractReference(line); + if (ref) { + const refPath = path.resolve(path.dirname(file), ref); + if (inSourceTypings(refPath)) { + trace(' - reference source typing %s (%s)', ref, refPath); + } else { + const relPath = path.relative(baseDir, refPath).replace(/\\/g, '/'); + + trace(' - reference external typing %s (%s) (relative: %s)', ref, refPath, relPath); + + if (!inExternalTypings(refPath)) { + externalTypings.push(refPath); + } + } + pushUnique(res.refs, refPath); + continue; + } + } + + // line comments + if (/^\/\//.test(line)) { + if (comments) { + res.lines.push({ original: line }); + } + continue; + } + + // private member + if (privateExp.test(line)) { + queuedJSDoc = null; + continue; + } + popJSDoc(); + + // import() statement or es6 import + if ( + (line.indexOf('from') >= 0 && (match = line.match(importEs6Exp))) || + (line.indexOf('require') >= 0 && (match = line.match(importExp))) + ) { + const [_, lead, quote, moduleName, trail] = match; + assert(moduleName); + + let impPath = path.resolve(path.dirname(file), moduleName); + if (impPath.startsWith('/')) { + impPath = impPath.replace('/https:/', 'https://').replace('.js', '.d.ts'); + } + + // filename (i.e. starts with a dot, slash or windows drive letter) + if (fileExp.test(moduleName) || moduleName.startsWith(sourceRoot)) { + // TODO: some module replacing is handled here, whereas the rest is + // done in the "rewrite global external modules" step. It may be + // more clear to do all of it in that step. + const modLine: ModLine = { + original: lead + quote + getExpName(impPath) + trail, + }; + res.lines.push(modLine); + + let full = impPath; + // If full is not an existing file, then let's assume the extension .d.ts + + const fullRes = await fetch(full); + if (!fullRes.ok) { + full += '.d.ts'; + } + + trace(' - import relative %s (%s)', moduleName, full); + + pushUnique(res.relativeImports, full); + res.importLineRef.push(modLine); + } + // identifier + else { + const modLine: ModLine = { + original: line, + }; + trace(' - import external %s', moduleName); + pushUnique(res.externalImports, moduleName); + if (externals) { + res.importLineRef.push(modLine); + } + res.lines.push(modLine); + } + } + // declaring an external module + // this triggers when we're e.g. parsing external module declarations, such as node.d.ts + else if ((match = line.match(externalExp))) { + const [_, _declareModule, _lead, moduleName, _trail] = match; + assert(moduleName); + + trace(' - declare %s', moduleName); + pushUnique(res.exports, moduleName); + const modLine: ModLine = { + original: line, + }; + res.relativeRef.push(modLine); // TODO + res.lines.push(modLine); + } + // clean regular lines + else { + // remove public keyword + if ((match = line.match(publicExp))) { + const [_, sp, static1, _pub, static2, ident] = match; + line = sp + static1 + static2 + ident; + } + if (inSourceTypings(file)) { + // for internal typings, remove the 'declare' keyword (but leave 'export' intact) + res.lines.push({ original: line.replace(/^(export )?declare /g, '$1') }); + } else { + res.lines.push({ original: line }); + } + } + } + + return res; + } +} + +function pushUnique(arr: T[], value: T) { + if (arr.indexOf(value) < 0) { + arr.push(value); + } + return arr; +} + +function pushUniqueArr(arr: T[], ...values: T[][]) { + values.forEach((vs) => vs.forEach((v) => pushUnique(arr, v))); + return arr; +} + +function formatReference(file: string) { + return '/// '; +} + +function extractReference(tag: string) { + const match = tag.match(referenceTagExp); + if (match) { + return match[2]; + } + return null; +} + +function replaceImportExport(line: string, replacer: (str: string) => string) { + const match = line.match(importExp); + if (match) { + // assert(match[4]); + if (identifierExp.test(match[3])) { + return match[1] + match[2] + replacer(match[3]) + match[4]; + } + } + return line; +} + +function replaceImportExportEs6(line: string, replacer: (str: string) => string) { + if (line.indexOf('from') < 0) { + return line; + } + const match = line.match(importEs6Exp); + if (match) { + // assert(match[4]); + if (identifierExp.test(match[3])) { + return match[1] + match[2] + replacer(match[3]) + match[4]; + } + } + return line; +} + +function replaceExternal(line: string, replacer: (str: string) => string) { + const match = line.match(externalExp); + if (match) { + const [_, declareModule, beforeIndent, moduleName, afterIdent] = match; + // assert(afterIdent); + if (identifierExp.test(moduleName)) { + return declareModule + beforeIndent + replacer(moduleName) + afterIdent; + } + } + return line; +} + +function getIndenter(_actual: string, _use: string): (line: ModLine) => string { + return (line) => line.modified || line.original; +} +// function getIndenter(actual: string, use: string): (line: ModLine) => string { +// if (actual === use || !actual) { +// return line => line.modified || line.original; +// } +// return line => (line.modified || line.original).replace(new RegExp('^' + actual + '+', 'g'), match => match.split(actual).join(use)); +// } + +function optValue(passed: T, def: T): T { + if (typeof passed === 'undefined') { + return def; + } + return passed; +} + +function regexEscape(s: string) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} diff --git a/src/livecodes/types/type-loader.ts b/src/livecodes/types/type-loader.ts index 9f4292184..92f123354 100644 --- a/src/livecodes/types/type-loader.ts +++ b/src/livecodes/types/type-loader.ts @@ -1,9 +1,10 @@ -import { getImports } from '../compiler'; +/* eslint-disable import/no-internal-modules */ import type { EditorLibrary, Types } from '../models'; -import { typesService } from '../services'; -import { objectFilter, safeName } from '../utils'; +import { getImports, hasUrlImportsOrExports } from '../compiler/import-map'; +import { typesService } from '../services/types'; +import { objectFilter, safeName } from '../utils/utils'; -export const createTypeLoader = () => { +export const createTypeLoader = (baseUrl: string) => { let loadedTypes: Types = {}; const getTypeContents = async (type: Types): Promise => { @@ -16,7 +17,15 @@ export const createTypeLoader = () => { try { const res = await fetch(url); if (!res.ok) throw new Error('Failed fetching: ' + url); - const dts = await res.text(); + let dts = await res.text(); + + if (hasUrlImportsOrExports(dts)) { + const dtsBundleModule: typeof import('./bundle-types') = await import( + baseUrl + '{{hash:bundle-types.js}}' + ); + dts = await dtsBundleModule.bundle({ name, main: url }); + } + const declareAsModule = !dts.includes('declare module') || (typeof value !== 'string' && value.declareAsModule === true); diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index dedd333d1..43d50ef5c 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -279,6 +279,8 @@ export const opalBaseUrl = /* @__PURE__ */ getUrl('https://cdn.opalrb.com/opal/1 export const parinferUrl = /* @__PURE__ */ getUrl('parinfer@3.13.1/parinfer.js'); +export const pathBrowserifyUrl = /* @__PURE__ */ getModuleUrl('path-browserify@1.0.1'); + export const pintoraUrl = /* @__PURE__ */ getUrl( '@pintora/standalone@0.6.2/lib/pintora-standalone.umd.js', ); diff --git a/vendor-licenses.md b/vendor-licenses.md index adbacd053..432a768ed 100644 --- a/vendor-licenses.md +++ b/vendor-licenses.md @@ -148,6 +148,8 @@ Opal: [MIT License](https://github.com/opal/opal/blob/631503c8957d1c6df60d158daf Parinfer.js: [MIT License](https://github.com/parinfer/parinfer.js/blob/cddc36ac3e7f9ecd328b977efe91c38ccfd0d94d/LICENSE.md) +path-browserify: [MIT License](https://github.com/browserify/path-browserify/blob/872fec31a8bac7b9b43be0e54ef3037e0202c5fb/LICENSE) + Perlito5: [Artistic License 2.0](https://github.com/fglock/Perlito/blob/f217cdac3771de31e009d4e099bac7013a619987/LICENSE.md) php-wasm: [Apache License 2.0](https://github.com/seanmorris/php-wasm/blob/094f4e00fa99b1271af077b6de7d0d38d475ecae/LICENSE) From 4418c639e68b5ec4707cbdbc46700fcdd1f05815 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 20 Jan 2024 09:15:16 +0200 Subject: [PATCH 05/44] add license --- vendor-licenses.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vendor-licenses.md b/vendor-licenses.md index 432a768ed..889e99e78 100644 --- a/vendor-licenses.md +++ b/vendor-licenses.md @@ -54,6 +54,8 @@ dot: [MIT License](https://github.com/olado/doT/blob/031d3bb7520eed6b93886df2b65 dart-sass: [MIT License](https://github.com/sass/dart-sass/blob/e3bf3eb3a3a8708877a86a08c7e3bee92160ac1f/LICENSE) +dts-bundle: [MIT License](https://github.com/TypeStrong/dts-bundle/blob/2ca1591e890dc4276efc4bb0893367e6ff32a039/LICENSE-MIT) + EasyQRCodeJS: [MIT License](https://github.com/ushelp/EasyQRCodeJS/blob/573373b110d706132a41cc8abb5b297016b80094/LICENSE) EJS: [Apache License 2.0](https://github.com/mde/ejs/blob/f47d7aedd51a983e4f73045f962b1209096b5800/LICENSE) From 994ed73a7e832c0ca2275a63ee690dbc2ae5c479 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 03:05:02 +0200 Subject: [PATCH 06/44] clean up type loading --- src/livecodes/UI/editor-settings.ts | 16 +++++++++--- src/livecodes/core.ts | 23 ++++++++++------- src/livecodes/languages/solid/lang-solid.ts | 6 ----- src/livecodes/types/type-loader.ts | 28 ++++++++++++++++++--- src/livecodes/vendors.ts | 2 -- src/sdk/models.ts | 1 + 6 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/livecodes/UI/editor-settings.ts b/src/livecodes/UI/editor-settings.ts index 199e4896d..b185b3bea 100644 --- a/src/livecodes/UI/editor-settings.ts +++ b/src/livecodes/UI/editor-settings.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-internal-modules */ import type { createEventsManager } from '../events'; import type { createModal } from '../modal'; -import type { Config, EditorOptions, FormatFn, UserConfig } from '../models'; +import type { Config, EditorLibrary, EditorOptions, FormatFn, UserConfig } from '../models'; import type { createEditor } from '../editor/create-editor'; import { editorSettingsScreen } from '../html'; import { getEditorConfig, getFormatterConfig } from '../config/config'; @@ -27,6 +27,7 @@ export const createEditorSettingsUI = async ({ deps: { getUserConfig: () => UserConfig; createEditor: typeof createEditor; + loadTypes: (code: string) => Promise; getFormatFn: () => Promise; changeSettings: (newConfig: Partial) => void; }; @@ -209,10 +210,10 @@ export const createEditorSettingsUI = async ({ baseUrl, container: previewContainer, editorId: 'editorSettings', - getLanguageExtension: () => 'jsx', + getLanguageExtension: () => 'tsx', isEmbed: false, isHeadless: false, - language: 'jsx', + language: 'tsx', mapLanguage: () => 'typescript', readonly: false, value: editorContent, @@ -225,6 +226,13 @@ export const createEditorSettingsUI = async ({ const initializeEditor = async (options: EditorOptions) => { const ed = await deps.createEditor(options); + if (typeof ed.addTypes === 'function') { + deps.loadTypes(editorContent).then((types) => { + types.forEach((type) => { + ed?.addTypes?.(type); + }); + }); + } deps.getFormatFn().then((fn) => { setTimeout(() => { ed.registerFormatter(fn); @@ -446,7 +454,7 @@ const editorContent = ` import React, { useState } from 'react'; import { createRoot } from "react-dom/client"; -function App(props) { +function App(props: { name: string }) { const [count, setCount] = useState(0); // increment on click! const onClick = () => setCount(count + 1); diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index de8233cbb..6bfdfc838 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -113,7 +113,6 @@ import * as UI from './UI/selectors'; import { createAuthService, getAppCDN, sandboxService, shareService } from './services'; import { cacheIsValid, getCache, getCachedCode, setCache, updateCache } from './cache'; import { - chaiTypesUrl, fscreenUrl, hintCssUrl, jestTypesUrl, @@ -3232,7 +3231,8 @@ const handleEditorSettings = () => { deps: { getUserConfig: () => getUserConfig(getConfig()), createEditor, - getFormatFn: () => formatter.getFormatFn('jsx'), + loadTypes: async (code: string) => typeLoader.load(code, {}), + getFormatFn: () => formatter.getFormatFn('tsx'), changeSettings, }, }); @@ -3616,15 +3616,20 @@ const handleTestEditor = () => { jest: { url: jestTypesUrl, autoload: true, - }, - chai: { - url: chaiTypesUrl, - autoload: true, + declareAsGlobal: true, }, }; - typeLoader.load('', testTypes, true).then((libs) => { - libs.forEach((lib) => testEditor?.addTypes?.(lib)); - }); + let forceLoadTypes = true; + const loadTestTypes = () => { + typeLoader.load(testEditor?.getValue() || '', testTypes, forceLoadTypes).then((libs) => { + libs.forEach((lib) => testEditor?.addTypes?.(lib)); + }); + forceLoadTypes = false; + }; + testEditor.onContentChanged( + debounce(loadTestTypes, () => getConfig().delay ?? defaultConfig.delay), + ); + loadTestTypes(); } eventsManager.addEventListener(UI.getLoadTestsButton(), 'click', async () => { diff --git a/src/livecodes/languages/solid/lang-solid.ts b/src/livecodes/languages/solid/lang-solid.ts index b6617c53a..b0c2210ca 100644 --- a/src/livecodes/languages/solid/lang-solid.ts +++ b/src/livecodes/languages/solid/lang-solid.ts @@ -16,12 +16,6 @@ export const solid: LanguageSpecs = { (self as any).importScripts(baseUrl + '{{hash:lang-solid-compiler.js}}'); return (self as any).createSolidCompiler(); }, - types: { - 'solid-js': { - url: vendorsBaseUrl + 'types/solid-js.d.ts', - declareAsModule: false, - }, - }, }, extensions: ['solid.jsx'], editor: 'script', diff --git a/src/livecodes/types/type-loader.ts b/src/livecodes/types/type-loader.ts index 92f123354..f84a25631 100644 --- a/src/livecodes/types/type-loader.ts +++ b/src/livecodes/types/type-loader.ts @@ -2,7 +2,7 @@ import type { EditorLibrary, Types } from '../models'; import { getImports, hasUrlImportsOrExports } from '../compiler/import-map'; import { typesService } from '../services/types'; -import { objectFilter, safeName } from '../utils/utils'; +import { objectFilter, removeDuplicates, safeName } from '../utils/utils'; export const createTypeLoader = (baseUrl: string) => { let loadedTypes: Types = {}; @@ -29,13 +29,24 @@ export const createTypeLoader = (baseUrl: string) => { const declareAsModule = !dts.includes('declare module') || (typeof value !== 'string' && value.declareAsModule === true); + const declareAsGlobal = typeof value !== 'string' && value.declareAsGlobal === true; - content = declareAsModule ? `declare module '${name}' {${dts}}` : dts; + content = declareAsModule && !declareAsGlobal ? `declare module '${name}' {${dts}}` : dts; } catch { content = `declare module '${name}': any`; } } - loadedTypes = { ...loadedTypes, ...type }; + // remove empty entries + const prevTypes = Object.keys(loadedTypes) + .filter((k) => loadedTypes[k] !== '') + .reduce( + (acc, k) => ({ + ...acc, + [k]: loadedTypes[k], + }), + {}, + ); + loadedTypes = { ...prevTypes, ...type }; return { filename: `file:///node_modules/${safeName(name)}/index.d.ts`, content, @@ -43,7 +54,9 @@ export const createTypeLoader = (baseUrl: string) => { }; const loadTypes = (types: Types) => - Promise.all(Object.keys(types).map((t) => getTypeContents({ [t]: types[t] }))); + Promise.all( + removeDuplicates(Object.keys(types)).map((t) => getTypeContents({ [t]: types[t] })), + ); const load = async (code: string, configTypes: Types, forceLoad = false) => { const imports = getImports(code); @@ -71,6 +84,13 @@ export const createTypeLoader = (baseUrl: string) => { }, {} as Types); const typesToGet = Object.keys(codeTypes).filter((key) => codeTypes[key] === ''); + + // mark as loaded to avoid re-fetching + loadedTypes = { + ...loadedTypes, + ...typesToGet.reduce((acc, cur) => ({ ...acc, [cur]: '' }), {}), + }; + const fetchedTypes = await typesService.getTypeUrls(typesToGet); const autoloadTypes: Types = objectFilter( diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 43d50ef5c..72b27deeb 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -42,8 +42,6 @@ export const brythonBaseUrl = /* @__PURE__ */ getUrl('brython@3.12.1/'); export const chaiUrl = /* @__PURE__ */ getModuleUrl('chai@5.0.0-alpha.2'); -export const chaiTypesUrl = /* @__PURE__ */ getUrl('@types/chai@4.3.11/index.d.ts'); - export const cherryCljsBaseUrl = /* @__PURE__ */ getUrl('cherry-cljs@0.0.4/'); export const cjs2esUrl = /* @__PURE__ */ getUrl('cjs2es@1.1.1/dist/cjs2es.browser.js'); diff --git a/src/sdk/models.ts b/src/sdk/models.ts index 3b83824fb..9db2a963f 100644 --- a/src/sdk/models.ts +++ b/src/sdk/models.ts @@ -372,6 +372,7 @@ export interface Types { | { url: string; declareAsModule?: boolean; + declareAsGlobal?: boolean; autoload?: boolean; }; } From 04d644e917e05c4483eafdb26942275fabf1d838 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 03:17:15 +0200 Subject: [PATCH 07/44] docs(Types): remove note about type bundling in docs which is no longer needed --- docs/docs/features/intellisense.md | 16 ---------------- docs/docs/features/module-resolution.md | 18 +++++++++--------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/docs/docs/features/intellisense.md b/docs/docs/features/intellisense.md index ee918a376..7613b29e0 100644 --- a/docs/docs/features/intellisense.md +++ b/docs/docs/features/intellisense.md @@ -20,14 +20,6 @@ These are examples for automatically loading React types with autocomplete and h ![LiveCodes Intellisense](../../static/img/screenshots/intellisense2.jpg) -:::info - -Automatically loading type definitions for npm modules uses a service provided for [https://livecodes.io](https://livecodes.io) and is not available for [self-hosted](./self-hosting.md) apps. You may want to use a [custom service](../advanced/services.md) instead. - -LiveCodes [sponsors](../sponsor.md) (Bronze sponsors and above) get access to managed custom services. - -::: - ## Custom Types If no type definitions are found, or if you want to provide your own (e.g. for a module that is not hosted on npm), custom type definition files can be used. @@ -75,14 +67,6 @@ Please note that the URLs used for `types` and `imports` properties may be full This can be of great use for library authors who want to provide playgrounds for documenting their libraries that are not (yet) published to npm. -:::info - -A single (bundled) data definition file should be used for each module. These files cannot import other files. - -You may want to use tools like [dts-bundle](https://www.npmjs.com/package/dts-bundle) or [dts-buddy](https://github.com/Rich-Harris/dts-buddy) to bundle declaration files. - -::: - ## Demo Let's assume we have this TypeScript module: diff --git a/docs/docs/features/module-resolution.md b/docs/docs/features/module-resolution.md index fd5bf97e7..da3684b38 100644 --- a/docs/docs/features/module-resolution.md +++ b/docs/docs/features/module-resolution.md @@ -22,10 +22,10 @@ If you run it directly in the browser, you get this error: Uncaught TypeError: Failed to resolve module specifier "uuid". Relative references must start with either "/", "./", or "../". ``` -However, in LiveCodes, bare module imports are transformed to full URLs that are imported from CDN (by default: [jspm.dev](https://jspm.dev/)) which provides ESM versions of NPM packages. +However, in LiveCodes, bare module imports are transformed to full URLs that are imported from CDN (by default: [esm.sh](https://esm.sh/)) which provides ESM versions of NPM packages. `import { v4 } from 'uuid';`
becomes
-`import { v4 } from 'https://jspm.dev/uuid';` +`import { v4 } from 'https://esm.sh/uuid';` This is made possible by using [import maps](https://github.com/WICG/import-maps). @@ -122,16 +122,14 @@ If you want to bundle (and transpile) any import URL, prefix it with `bundle:` ( ## CDN Providers -By default, npm modules are imported from [jspm.dev](https://jspm.dev/). You may choose another provider by using a CDN prefix. These are examples of importing the library `uuid`: +By default, npm modules are imported from [esm.sh](https://esm.sh/). You may choose another provider by using a CDN prefix. These are examples of importing the library `uuid`: -`uuid` → https://jspm.dev/uuid ([info](https://jspm.org)) +`uuid` → https://esm.sh/uuid ([info](https://esm.sh)) -`jspm:uuid` → https://jspm.dev/uuid ([info](https://jspm.org)) +`esm.sh:uuid` → https://esm.sh/uuid ([info](https://esm.sh/)) `skypack:uuid` → https://cdn.skypack.dev/uuid ([info](https://www.skypack.dev/)) -`esm.sh:uuid` → https://esm.sh/uuid ([info](https://esm.sh/)) - `jsdelivr:uuid` → https://cdn.jsdelivr.net/npm/uuid ([info](https://www.jsdelivr.com/)) `esm.run:uuid` → https://esm.run/uuid ([info](https://esm.run/)) @@ -146,9 +144,11 @@ By default, npm modules are imported from [jspm.dev](https://jspm.dev/). You may `deno:uuid` → https://deno.bundlejs.com/?file&q=https://deno.land/x/uuid/mod.ts ([info](https://bundlejs.com/)) -`npm:uuid` → https://jspm.dev/uuid ([info](https://jspm.org)) +`npm:uuid` → https://esm.sh/uuid ([info](https://esm.sh)) + +`node:uuid` → https://esm.sh/uuid ([info](https://esm.sh)) -`node:uuid` → https://jspm.dev/uuid ([info](https://jspm.org)) +`jspm:uuid` → https://jspm.dev/uuid ([info](https://jspm.org) - [DEPRECATED](https://jspm.org/jspm-dev-deprecation)) Example: From cc7761de876f6e343988453f6ad1147770cf5dd4 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 03:21:18 +0200 Subject: [PATCH 08/44] clean up --- docs/docs/advanced/services.md | 1 - src/livecodes/services/types.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/docs/advanced/services.md b/docs/docs/advanced/services.md index 4b064e3f2..362b72915 100644 --- a/docs/docs/advanced/services.md +++ b/docs/docs/advanced/services.md @@ -9,7 +9,6 @@ Some of the services are not supported on [self-hosted](../features/self-hosting Examples: - The [share](../features/share.md) service in [self-hosted](../features/self-hosting.md) apps uses [dpaste](https://dpaste.com/) for short URLs, which are [**deleted after 365 days**](https://dpaste.com/help). -- Automatically finding and loading TypeScript types for npm modules (for [editor intellisense](../features/intellisense.md)) are not available for [self-hosted](../features/self-hosting.md) apps. - [Firebase configuration](https://github.com/live-codes/livecodes/tree/develop/src/livecodes/services/firebase.ts) for authentication. :::info diff --git a/src/livecodes/services/types.ts b/src/livecodes/services/types.ts index 764bdca70..3c7e03db4 100644 --- a/src/livecodes/services/types.ts +++ b/src/livecodes/services/types.ts @@ -28,16 +28,16 @@ const removeCDNPrefix = (url: string) => { if (!url.startsWith('https://')) return url; const prefixes = [ - 'https://jspm.dev/', + 'https://esm.sh/', 'https://cdn.skypack.dev/', 'https://cdn.jsdelivr.net/npm/', 'https://fastly.jsdelivr.net/npm/', 'https://esm.run/', - 'https://esm.sh/', 'https://esbuild.vercel.app/', 'https://bundle.run/', 'https://unpkg.com/', 'https://deno.bundlejs.com/?file&q=', + 'https://jspm.dev/', ]; for (const prefix of prefixes) { From a85ba31b0c380c0d815533214615bab27b94cea4 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 17:09:47 +0200 Subject: [PATCH 09/44] fix(Types): fix race condition in loading types --- src/livecodes/core.ts | 14 ++++++++++---- src/livecodes/types/default-types.ts | 4 +--- src/livecodes/types/type-loader.ts | 24 ++++++++++++++++-------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index 6bfdfc838..27dcc3ae2 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -313,7 +313,12 @@ const createIframe = (container: HTMLElement, result = '', service = sandboxServ resultLanguages = getEditorLanguages(); }); -const loadModuleTypes = async (editors: Editors, config: Config, force = false) => { +const loadModuleTypes = async ( + editors: Editors, + config: Config, + loadAll = false, + force = false, +) => { if (typeof editors?.script?.addTypes !== 'function') return; const scriptLanguage = config.script.language; if (['typescript', 'javascript'].includes(mapLanguage(scriptLanguage)) || force) { @@ -327,6 +332,7 @@ const loadModuleTypes = async (editors: Editors, config: Config, force = false) const libs = await typeLoader.load( getConfig().script.content + '\n' + getConfig().markup.content, configTypes, + loadAll, force, ); libs.forEach((lib) => editors.script.addTypes?.(lib, force)); @@ -729,7 +735,7 @@ const changeLanguage = async (language: Language, value?: string, isUpdate = fal await setSavedStatus(); dispatchChangeEvent(); addConsoleInputCodeCompletion(); - loadModuleTypes(editors, getConfig()); + loadModuleTypes(editors, getConfig(), /* loadAll = */ true); await applyLanguageConfigs(language); }; @@ -3458,7 +3464,7 @@ const handleCustomSettings = () => { setCustomSettingsMark(); await setSavedStatus(); if (customSettings.types) { - loadModuleTypes(editors, getConfig(), /* force */ true); + loadModuleTypes(editors, getConfig(), /* loadAll = */ true, /* force */ true); } } customSettingsEditor?.destroy(); @@ -4149,7 +4155,7 @@ const bootstrap = async (reload = false) => { setExternalResourcesMark(); setCustomSettingsMark(); updateCompiledCode(); - loadModuleTypes(editors, getConfig()); + loadModuleTypes(editors, getConfig(), /* loadAll = */ true); compiler.load(Object.values(editorLanguages || {}), getConfig()).then(() => { if (!getConfig().autoupdate) { setLoading(false); diff --git a/src/livecodes/types/default-types.ts b/src/livecodes/types/default-types.ts index bd9c08bab..cf6fc21a4 100644 --- a/src/livecodes/types/default-types.ts +++ b/src/livecodes/types/default-types.ts @@ -3,7 +3,5 @@ import type { Types } from '../models'; import { modulesService } from '../services/modules'; export const getDefaultTypes = (): Types => ({ - react: modulesService.getUrl('@types/react/index.d.ts'), - 'react-dom': modulesService.getUrl('@types/react-dom/index.d.ts'), - 'react-dom/client': modulesService.getUrl('@types/react-dom/client.d.ts'), + livecodes: modulesService.getUrl('livecodes/livecodes.d.ts'), }); diff --git a/src/livecodes/types/type-loader.ts b/src/livecodes/types/type-loader.ts index f84a25631..fdd7b78dc 100644 --- a/src/livecodes/types/type-loader.ts +++ b/src/livecodes/types/type-loader.ts @@ -2,17 +2,20 @@ import type { EditorLibrary, Types } from '../models'; import { getImports, hasUrlImportsOrExports } from '../compiler/import-map'; import { typesService } from '../services/types'; -import { objectFilter, removeDuplicates, safeName } from '../utils/utils'; +import { objectFilter, safeName } from '../utils/utils'; export const createTypeLoader = (baseUrl: string) => { let loadedTypes: Types = {}; + const libs: EditorLibrary[] = []; const getTypeContents = async (type: Types): Promise => { let content = ''; const name = Object.keys(type)[0]; const value = Object.values(type)[0]; const url = typeof value === 'string' ? value : value.url; - + if (loadedTypes[name]) { + return { filename: '', content: '' }; + } if (url) { try { const res = await fetch(url); @@ -46,19 +49,23 @@ export const createTypeLoader = (baseUrl: string) => { }), {}, ); + if (content.trim() === '') { + loadedTypes = prevTypes; + return { filename: '', content: '' }; + } loadedTypes = { ...prevTypes, ...type }; - return { + const lib = { filename: `file:///node_modules/${safeName(name)}/index.d.ts`, content, }; + libs.push(lib); + return lib; }; const loadTypes = (types: Types) => - Promise.all( - removeDuplicates(Object.keys(types)).map((t) => getTypeContents({ [t]: types[t] })), - ); + Promise.all(Object.keys(types).map((t) => getTypeContents({ [t]: types[t] }))); - const load = async (code: string, configTypes: Types, forceLoad = false) => { + const load = async (code: string, configTypes: Types, loadAll = false, forceLoad = false) => { const imports = getImports(code); const codeTypes: Types = imports.reduce((accTypes, lib) => { @@ -101,7 +108,8 @@ export const createTypeLoader = (baseUrl: string) => { value.autoload === true, ); - return loadTypes({ ...codeTypes, ...fetchedTypes, ...autoloadTypes }); + const newLibs = await loadTypes({ ...codeTypes, ...fetchedTypes, ...autoloadTypes }); + return loadAll ? libs : newLibs; }; return { From 6daace7f4196e05b9704adfedadedae39cd69187 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 17:48:32 +0200 Subject: [PATCH 10/44] feat(compilers): render React component if it is the default export --- src/livecodes/languages/jsx/jsx-runtime.ts | 11 +++++++++ .../languages/typescript/lang-typescript.ts | 14 +++++++---- src/livecodes/result/result-page.ts | 24 ++++++++++++++++--- 3 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 src/livecodes/languages/jsx/jsx-runtime.ts diff --git a/src/livecodes/languages/jsx/jsx-runtime.ts b/src/livecodes/languages/jsx/jsx-runtime.ts new file mode 100644 index 000000000..fdcf8130b --- /dev/null +++ b/src/livecodes/languages/jsx/jsx-runtime.ts @@ -0,0 +1,11 @@ +export const reactRuntime = ` +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from './script'; +const root = createRoot(document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div'))); +root.render(React.createElement(App, null)); +`; + +export const hasCustomJsxRuntime = (code: string) => new RegExp(/\/\*\*[\s\*]*@jsx\s/g).test(code); + +export const hasDefaultExport = (code: string) => new RegExp(/export\s*default\s/).test(code); diff --git a/src/livecodes/languages/typescript/lang-typescript.ts b/src/livecodes/languages/typescript/lang-typescript.ts index e6c826dcd..7a6464900 100644 --- a/src/livecodes/languages/typescript/lang-typescript.ts +++ b/src/livecodes/languages/typescript/lang-typescript.ts @@ -2,6 +2,8 @@ import type { LanguageSpecs } from '../../models'; import { typescriptUrl } from '../../vendors'; import { getLanguageCustomSettings } from '../../utils'; import { parserPlugins } from '../prettier'; +// eslint-disable-next-line import/no-internal-modules +import { hasCustomJsxRuntime } from '../jsx/jsx-runtime'; export const typescriptOptions = { target: 'es2015', @@ -22,12 +24,16 @@ export const typescript: LanguageSpecs = { url: typescriptUrl, factory: () => - async (code, { config, language }) => - (window as any).ts.transpile(code, { + async (code, { config }) => { + if (['jsx', 'tsx'].includes(config.script.language) && !hasCustomJsxRuntime(code)) { + code = `import React from 'react';\n${code}`; + } + return (window as any).ts.transpile(code, { ...typescriptOptions, ...getLanguageCustomSettings('typescript', config), - ...getLanguageCustomSettings(language, config), - }), + ...getLanguageCustomSettings(config.script.language, config), + }); + }, }, extensions: ['ts', 'typescript'], editor: 'script', diff --git a/src/livecodes/result/result-page.ts b/src/livecodes/result/result-page.ts index 5ca39a638..c13d70e76 100644 --- a/src/livecodes/result/result-page.ts +++ b/src/livecodes/result/result-page.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-internal-modules */ import { createImportMap, createCSSModulesImportMap, @@ -7,9 +8,9 @@ import { removeImports, } from '../compiler'; import { cssPresets, getLanguageCompiler, getLanguageExtension } from '../languages'; +import { hasCustomJsxRuntime, hasDefaultExport, reactRuntime } from '../languages/jsx/jsx-runtime'; import type { Cache, EditorId, Config, CompileInfo } from '../models'; import { getAppCDN, modulesService } from '../services'; -// eslint-disable-next-line import/no-internal-modules import { testImports } from '../toolspane/test-imports'; import { addAttrs, @@ -156,6 +157,12 @@ export const createResultPage = async ({ getImports(markup).includes('./script') || (runTests && !forExport && getImports(compiledTests).includes('./script')); + const shouldInsertReactJsxRuntime = + ['jsx', 'tsx'].includes(code.script.language) && + hasDefaultExport(code.script.compiled) && + !hasCustomJsxRuntime(code.script.content || '') && + !importFromScript; + let compilerImports = {}; for (const { language, compiled } of runtimeDependencies) { @@ -218,10 +225,13 @@ export const createResultPage = async ({ ...(hasImports(code.markup.compiled) ? createImportMap(code.markup.compiled, config) : {}), + ...(shouldInsertReactJsxRuntime ? createImportMap(reactRuntime, config) : {}), ...(runTests && !forExport && hasImports(compiledTests) ? createImportMap(compiledTests, config) : {}), - ...(importFromScript ? { './script': toDataUrl(code.script.compiled) } : {}), + ...(importFromScript || shouldInsertReactJsxRuntime + ? { './script': toDataUrl(code.script.compiled) } + : {}), ...createCSSModulesImportMap( code.script.compiled, code.style.compiled, @@ -263,7 +273,7 @@ export const createResultPage = async ({ dom.head.appendChild(externalScript); }); - if (!importFromScript) { + if (!importFromScript && !shouldInsertReactJsxRuntime) { // editor script const script = code.script.compiled; const scriptElement = dom.createElement('script'); @@ -288,6 +298,14 @@ export const createResultPage = async ({ } } + // React JSX runtime + if (shouldInsertReactJsxRuntime) { + const jsxRuntimeScript = dom.createElement('script'); + jsxRuntimeScript.type = 'module'; + jsxRuntimeScript.innerHTML = reactRuntime; + dom.body.appendChild(jsxRuntimeScript); + } + // spacing if (config.showSpacing && !forExport) { const spacingScript = dom.createElement('script'); From e46450dd39919415e48896d27dd27ab66a69a417 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 17:48:32 +0200 Subject: [PATCH 11/44] feat(compilers): set typescript option { jsx: 'react-jsx' } --- src/livecodes/languages/jsx/jsx-runtime.ts | 4 ++-- .../languages/typescript/lang-typescript.ts | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/livecodes/languages/jsx/jsx-runtime.ts b/src/livecodes/languages/jsx/jsx-runtime.ts index fdcf8130b..33cee2a7d 100644 --- a/src/livecodes/languages/jsx/jsx-runtime.ts +++ b/src/livecodes/languages/jsx/jsx-runtime.ts @@ -1,9 +1,9 @@ export const reactRuntime = ` -import React from "react"; +import { jsx as _jsx } from "react/jsx-runtime"; import { createRoot } from "react-dom/client"; import App from './script'; const root = createRoot(document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div'))); -root.render(React.createElement(App, null)); +root.render(_jsx(App, {})); `; export const hasCustomJsxRuntime = (code: string) => new RegExp(/\/\*\*[\s\*]*@jsx\s/g).test(code); diff --git a/src/livecodes/languages/typescript/lang-typescript.ts b/src/livecodes/languages/typescript/lang-typescript.ts index 7a6464900..bb8c578c6 100644 --- a/src/livecodes/languages/typescript/lang-typescript.ts +++ b/src/livecodes/languages/typescript/lang-typescript.ts @@ -7,7 +7,7 @@ import { hasCustomJsxRuntime } from '../jsx/jsx-runtime'; export const typescriptOptions = { target: 'es2015', - jsx: 'react', + jsx: 'react-jsx', allowUmdGlobalAccess: true, esModuleInterop: true, }; @@ -24,16 +24,13 @@ export const typescript: LanguageSpecs = { url: typescriptUrl, factory: () => - async (code, { config }) => { - if (['jsx', 'tsx'].includes(config.script.language) && !hasCustomJsxRuntime(code)) { - code = `import React from 'react';\n${code}`; - } - return (window as any).ts.transpile(code, { + async (code, { config }) => + (window as any).ts.transpile(code, { ...typescriptOptions, + ...(hasCustomJsxRuntime(code) ? { jsx: 'react' } : {}), ...getLanguageCustomSettings('typescript', config), ...getLanguageCustomSettings(config.script.language, config), - }); - }, + }), }, extensions: ['ts', 'typescript'], editor: 'script', From d83c668f600c140cf04843beaea7b9727af78a72 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 17:48:32 +0200 Subject: [PATCH 12/44] only apply {jsx: 'react-jsx'} if language is JSX or TSX this avoids interfering with other languages (e.g. Vue with JSX) --- src/livecodes/languages/typescript/lang-typescript.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/livecodes/languages/typescript/lang-typescript.ts b/src/livecodes/languages/typescript/lang-typescript.ts index bb8c578c6..16ac76dda 100644 --- a/src/livecodes/languages/typescript/lang-typescript.ts +++ b/src/livecodes/languages/typescript/lang-typescript.ts @@ -7,7 +7,7 @@ import { hasCustomJsxRuntime } from '../jsx/jsx-runtime'; export const typescriptOptions = { target: 'es2015', - jsx: 'react-jsx', + jsx: 'react', allowUmdGlobalAccess: true, esModuleInterop: true, }; @@ -27,7 +27,9 @@ export const typescript: LanguageSpecs = { async (code, { config }) => (window as any).ts.transpile(code, { ...typescriptOptions, - ...(hasCustomJsxRuntime(code) ? { jsx: 'react' } : {}), + ...(['jsx', 'tsx'].includes(config.script.language) && !hasCustomJsxRuntime(code) + ? { jsx: 'react-jsx' } + : {}), ...getLanguageCustomSettings('typescript', config), ...getLanguageCustomSettings(config.script.language, config), }), From 2e5b9d1d2e8af0c150426d57b249c8112477d5dc Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 21 Jan 2024 20:22:09 +0200 Subject: [PATCH 13/44] feat(Templates): update react and jest-react starter templates to use the new jsx runtime --- .../templates/starter/jest-react-starter.ts | 14 ++++++-------- src/livecodes/templates/starter/react-starter.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/livecodes/templates/starter/jest-react-starter.ts b/src/livecodes/templates/starter/jest-react-starter.ts index 9eb5e9619..c82c70b23 100644 --- a/src/livecodes/templates/starter/jest-react-starter.ts +++ b/src/livecodes/templates/starter/jest-react-starter.ts @@ -8,9 +8,7 @@ export const jestReactStarter: Template = { autotest: true, markup: { language: 'html', - content: ` -
Loading...
-`.trimStart(), + content: '', }, style: { language: 'css', @@ -33,12 +31,11 @@ export const jestReactStarter: Template = { script: { language: 'jsx', content: ` -import React, { useState } from "react"; -import { createRoot } from "react-dom/client"; +import { useState } from "react"; export const increment = (count) => (count ?? 0) + 1; -export default function App(props) { +function Counter(props) { const [count, setCount] = useState(0); return (
@@ -51,8 +48,9 @@ export default function App(props) { ); } -const root = createRoot(document.querySelector("#app")); -root.render(); +export default function App() { + return ; +} `.trimStart(), }, tests: { diff --git a/src/livecodes/templates/starter/react-starter.ts b/src/livecodes/templates/starter/react-starter.ts index fd2b0fa2a..db497a1c3 100644 --- a/src/livecodes/templates/starter/react-starter.ts +++ b/src/livecodes/templates/starter/react-starter.ts @@ -7,7 +7,7 @@ export const reactStarter: Template = { activeEditor: 'script', markup: { language: 'html', - content: '
Loading...
\n', + content: '', }, style: { language: 'css', @@ -25,10 +25,9 @@ export const reactStarter: Template = { script: { language: 'jsx', content: ` -import React, { useState } from "react"; -import { createRoot } from "react-dom/client"; +import { useState } from "react"; -function App(props) { +function Counter(props) { const [count, setCount] = useState(0); return (
@@ -40,8 +39,9 @@ function App(props) { ); } -const root = createRoot(document.querySelector("#app")); -root.render(); +export default function App() { + return ; +} `.trimStart(), }, stylesheets: [], From 40a96c7148d5a41e029442e408142aa9b3d7772b Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Mon, 22 Jan 2024 01:14:04 +0200 Subject: [PATCH 14/44] only render jsx default export if it is a react component --- src/livecodes/languages/jsx/jsx-runtime.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/livecodes/languages/jsx/jsx-runtime.ts b/src/livecodes/languages/jsx/jsx-runtime.ts index 33cee2a7d..0287dd57c 100644 --- a/src/livecodes/languages/jsx/jsx-runtime.ts +++ b/src/livecodes/languages/jsx/jsx-runtime.ts @@ -1,9 +1,13 @@ export const reactRuntime = ` import { jsx as _jsx } from "react/jsx-runtime"; import { createRoot } from "react-dom/client"; -import App from './script'; -const root = createRoot(document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div'))); -root.render(_jsx(App, {})); +import App from "./script"; +(() => { + const isReactComponent = (c) => typeof c === "function" && (/return\\s+\\(?\\s*(_jsx|React\\.createElement)/g.test(String(c)) || Boolean(c.prototype.isReactComponent)); + if (!isReactComponent(App)) return; + const root = createRoot(document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement("div"))); + root.render(_jsx(App, {})); +})(); `; export const hasCustomJsxRuntime = (code: string) => new RegExp(/\/\*\*[\s\*]*@jsx\s/g).test(code); From 7db03f6e39a05beb56d51c8d1798564b0f52536f Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Tue, 23 Jan 2024 01:12:43 +0200 Subject: [PATCH 15/44] feat(compilers): render react-native component if it is the default export --- .../languages/react-native/jsx-runtime.ts | 11 ++++++++ .../react-native/lang-react-native-tsx.ts | 21 ++------------- .../react-native/lang-react-native.ts | 3 ++- src/livecodes/result/result-page.ts | 26 +++++++++++++------ .../templates/starter/react-native-starter.ts | 14 +++------- 5 files changed, 37 insertions(+), 38 deletions(-) create mode 100644 src/livecodes/languages/react-native/jsx-runtime.ts diff --git a/src/livecodes/languages/react-native/jsx-runtime.ts b/src/livecodes/languages/react-native/jsx-runtime.ts new file mode 100644 index 000000000..6f0794030 --- /dev/null +++ b/src/livecodes/languages/react-native/jsx-runtime.ts @@ -0,0 +1,11 @@ +export const reactNativeRuntime = ` +import { AppRegistry } from "react-native"; +import App from "./script"; +(() => { + const isReactComponent = (c) => typeof c === "function" && (/return\\s+\\(?\\s*(_jsx|React\\.createElement)/g.test(String(c)) || Boolean(c.prototype.isReactComponent)); + if (!isReactComponent(App)) return; + const rootTag = document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement("div")); + AppRegistry.registerComponent("App", () => App); + AppRegistry.runApplication("App", { rootTag }); +})(); +`; diff --git a/src/livecodes/languages/react-native/lang-react-native-tsx.ts b/src/livecodes/languages/react-native/lang-react-native-tsx.ts index fe008dbef..8e55b94df 100644 --- a/src/livecodes/languages/react-native/lang-react-native-tsx.ts +++ b/src/livecodes/languages/react-native/lang-react-native-tsx.ts @@ -1,32 +1,15 @@ import type { LanguageSpecs } from '../../models'; -import { typescriptOptions } from '../typescript'; -import { getLanguageCustomSettings } from '../utils'; import { parserPlugins } from '../prettier'; -import { reactNativeWebUrl } from './lang-react-native'; export const reactNativeTsx: LanguageSpecs = { name: 'react-native-tsx', title: 'RN (TSX)', longTitle: 'React Native (TSX)', parser: { - name: 'babel', + name: 'babel-ts', pluginUrls: [parserPlugins.babel, parserPlugins.html], }, - compiler: { - dependencies: ['typescript'], - factory: - () => - async (code, { config, language }) => - (window as any).ts.transpile(code, { - ...typescriptOptions, - ...getLanguageCustomSettings('typescript', config), - ...getLanguageCustomSettings(language, config), - }), - imports: { - react: reactNativeWebUrl, - 'react-native': reactNativeWebUrl, - }, - }, + compiler: 'react-native', extensions: ['react-native.tsx'], editor: 'script', editorLanguage: 'typescript', diff --git a/src/livecodes/languages/react-native/lang-react-native.ts b/src/livecodes/languages/react-native/lang-react-native.ts index 3ca25ff2b..c345b8820 100644 --- a/src/livecodes/languages/react-native/lang-react-native.ts +++ b/src/livecodes/languages/react-native/lang-react-native.ts @@ -4,7 +4,7 @@ import { typescriptOptions } from '../typescript'; import { getLanguageCustomSettings } from '../utils'; import { parserPlugins } from '../prettier'; -export const reactNativeWebUrl = vendorsBaseUrl + 'react-native-web/react-native-web.js'; +const reactNativeWebUrl = vendorsBaseUrl + 'react-native-web/react-native-web.js'; export const reactNative: LanguageSpecs = { name: 'react-native', @@ -21,6 +21,7 @@ export const reactNative: LanguageSpecs = { async (code, { config, language }) => (window as any).ts.transpile(code, { ...typescriptOptions, + ...{ jsx: 'react-jsx' }, ...getLanguageCustomSettings('typescript', config), ...getLanguageCustomSettings(language, config), }), diff --git a/src/livecodes/result/result-page.ts b/src/livecodes/result/result-page.ts index c13d70e76..bf559d4cc 100644 --- a/src/livecodes/result/result-page.ts +++ b/src/livecodes/result/result-page.ts @@ -9,7 +9,8 @@ import { } from '../compiler'; import { cssPresets, getLanguageCompiler, getLanguageExtension } from '../languages'; import { hasCustomJsxRuntime, hasDefaultExport, reactRuntime } from '../languages/jsx/jsx-runtime'; -import type { Cache, EditorId, Config, CompileInfo } from '../models'; +import { reactNativeRuntime } from '../languages/react-native/jsx-runtime'; +import type { Cache, EditorId, Config, CompileInfo, Language } from '../models'; import { getAppCDN, modulesService } from '../services'; import { testImports } from '../toolspane/test-imports'; import { @@ -157,8 +158,17 @@ export const createResultPage = async ({ getImports(markup).includes('./script') || (runTests && !forExport && getImports(compiledTests).includes('./script')); - const shouldInsertReactJsxRuntime = - ['jsx', 'tsx'].includes(code.script.language) && + const jsxRuntimes: Partial> = { + jsx: reactRuntime, + tsx: reactRuntime, + 'react-native': reactNativeRuntime, + 'react-native-tsx': reactNativeRuntime, + solid: '', + 'solid.tsx': '', + }; + const jsxRuntime = jsxRuntimes[code.script.language] || ''; + const shouldInsertJsxRuntime = + Object.keys(jsxRuntimes).includes(code.script.language) && hasDefaultExport(code.script.compiled) && !hasCustomJsxRuntime(code.script.content || '') && !importFromScript; @@ -225,11 +235,11 @@ export const createResultPage = async ({ ...(hasImports(code.markup.compiled) ? createImportMap(code.markup.compiled, config) : {}), - ...(shouldInsertReactJsxRuntime ? createImportMap(reactRuntime, config) : {}), + ...(shouldInsertJsxRuntime ? createImportMap(jsxRuntime, config) : {}), ...(runTests && !forExport && hasImports(compiledTests) ? createImportMap(compiledTests, config) : {}), - ...(importFromScript || shouldInsertReactJsxRuntime + ...(importFromScript || shouldInsertJsxRuntime ? { './script': toDataUrl(code.script.compiled) } : {}), ...createCSSModulesImportMap( @@ -273,7 +283,7 @@ export const createResultPage = async ({ dom.head.appendChild(externalScript); }); - if (!importFromScript && !shouldInsertReactJsxRuntime) { + if (!importFromScript && !shouldInsertJsxRuntime) { // editor script const script = code.script.compiled; const scriptElement = dom.createElement('script'); @@ -299,10 +309,10 @@ export const createResultPage = async ({ } // React JSX runtime - if (shouldInsertReactJsxRuntime) { + if (shouldInsertJsxRuntime) { const jsxRuntimeScript = dom.createElement('script'); jsxRuntimeScript.type = 'module'; - jsxRuntimeScript.innerHTML = reactRuntime; + jsxRuntimeScript.innerHTML = jsxRuntime; dom.body.appendChild(jsxRuntimeScript); } diff --git a/src/livecodes/templates/starter/react-native-starter.ts b/src/livecodes/templates/starter/react-native-starter.ts index f22e097d8..7d56f3d78 100644 --- a/src/livecodes/templates/starter/react-native-starter.ts +++ b/src/livecodes/templates/starter/react-native-starter.ts @@ -7,7 +7,7 @@ export const reactNativeStarter: Template = { activeEditor: 'script', markup: { language: 'html', - content: '
Loading...
\n', + content: '', }, style: { language: 'css', @@ -16,8 +16,8 @@ export const reactNativeStarter: Template = { script: { language: 'react-native', content: ` -import React, { useState } from "react"; -import { AppRegistry, Button, Image, StyleSheet, Text, View } from "react-native"; +import { useState } from "react"; +import { Button, Image, StyleSheet, Text, View } from "react-native"; const logoUri = \`data:image/svg+xml;utf8,\`; @@ -41,7 +41,7 @@ function Counter(props) { ); } -function App() { +export default function App() { return ( @@ -92,12 +92,6 @@ const styles = StyleSheet.create({ color: "#1B95E0", }, }); - -AppRegistry.registerComponent("App", () => App); - -AppRegistry.runApplication("App", { - rootTag: document.getElementById("app"), -}); `.trimStart(), }, stylesheets: [], From 4968f036b6062293d6580eab593f4a99cc83ea14 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Tue, 23 Jan 2024 02:04:34 +0200 Subject: [PATCH 16/44] feat(compilers): render Solid component if it is the default export --- src/livecodes/languages/solid/jsx-runtime.ts | 10 +++++++ .../languages/solid/lang-solid-tsx.ts | 2 +- src/livecodes/result/result-page.ts | 5 ++-- .../templates/starter/solid-starter.ts | 30 ++++++++++++------- 4 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 src/livecodes/languages/solid/jsx-runtime.ts diff --git a/src/livecodes/languages/solid/jsx-runtime.ts b/src/livecodes/languages/solid/jsx-runtime.ts new file mode 100644 index 000000000..05f5f7fa9 --- /dev/null +++ b/src/livecodes/languages/solid/jsx-runtime.ts @@ -0,0 +1,10 @@ +export const solidRuntime = ` +import { render, createComponent } from "solid-js/web"; +import App from "./script"; +(() => { + const isSolidComponent = (c) => typeof c === "function" && /return\\s+\\(?\\s*function\\s+\\(\\)\\s+{/g.test(String(c)); + if (!isSolidComponent(App)) return; + const root = document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement("div")); + render(() => createComponent(App, {}), root); +})(); +`; diff --git a/src/livecodes/languages/solid/lang-solid-tsx.ts b/src/livecodes/languages/solid/lang-solid-tsx.ts index d07af1c3d..bde8639ca 100644 --- a/src/livecodes/languages/solid/lang-solid-tsx.ts +++ b/src/livecodes/languages/solid/lang-solid-tsx.ts @@ -5,7 +5,7 @@ export const solidTsx: LanguageSpecs = { name: 'solid.tsx', title: 'Solid (TS)', parser: { - name: 'babel', + name: 'babel-ts', pluginUrls: [parserPlugins.babel, parserPlugins.html], }, compiler: 'solid', diff --git a/src/livecodes/result/result-page.ts b/src/livecodes/result/result-page.ts index bf559d4cc..4f163f5a8 100644 --- a/src/livecodes/result/result-page.ts +++ b/src/livecodes/result/result-page.ts @@ -10,6 +10,7 @@ import { import { cssPresets, getLanguageCompiler, getLanguageExtension } from '../languages'; import { hasCustomJsxRuntime, hasDefaultExport, reactRuntime } from '../languages/jsx/jsx-runtime'; import { reactNativeRuntime } from '../languages/react-native/jsx-runtime'; +import { solidRuntime } from '../languages/solid/jsx-runtime'; import type { Cache, EditorId, Config, CompileInfo, Language } from '../models'; import { getAppCDN, modulesService } from '../services'; import { testImports } from '../toolspane/test-imports'; @@ -163,8 +164,8 @@ export const createResultPage = async ({ tsx: reactRuntime, 'react-native': reactNativeRuntime, 'react-native-tsx': reactNativeRuntime, - solid: '', - 'solid.tsx': '', + solid: solidRuntime, + 'solid.tsx': solidRuntime, }; const jsxRuntime = jsxRuntimes[code.script.language] || ''; const shouldInsertJsxRuntime = diff --git a/src/livecodes/templates/starter/solid-starter.ts b/src/livecodes/templates/starter/solid-starter.ts index 257a3c369..d2f33e09a 100644 --- a/src/livecodes/templates/starter/solid-starter.ts +++ b/src/livecodes/templates/starter/solid-starter.ts @@ -7,7 +7,7 @@ export const solidStarter: Template = { activeEditor: 'script', markup: { language: 'html', - content: '
\n', + content: '', }, style: { language: 'css', @@ -25,28 +25,36 @@ export const solidStarter: Template = { script: { language: 'solid.tsx', content: ` -import { render } from "solid-js/web"; import { createSignal } from "solid-js"; -type Props = { - title: string; +function Greeting(props: { name: string }) { + return ( + <> +

Hello, {props.name}!

+ logo + + ); } -function App(props: Props) { +function Counter() { const [count, setCount] = createSignal(0); const increment = () => setCount(count() + 1); - return ( -
-

Hello, {props.title}!

- logo + <>

You clicked {count()} times.

-
+ ); } -render(() => , document.getElementById("app")); +export default function App() { + return ( +
+ + +
+ ); +} `.trimStart(), }, stylesheets: [], From 32270d801683dd556412e8396de6e56f19702431 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Tue, 23 Jan 2024 02:56:20 +0200 Subject: [PATCH 17/44] feat(compilers): allow using JSX fragments in Vue SFC --- src/livecodes/languages/vue/lang-vue-compiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/livecodes/languages/vue/lang-vue-compiler.ts b/src/livecodes/languages/vue/lang-vue-compiler.ts index a3aa970a6..b3dee4bd4 100644 --- a/src/livecodes/languages/vue/lang-vue-compiler.ts +++ b/src/livecodes/languages/vue/lang-vue-compiler.ts @@ -246,7 +246,7 @@ import { getLanguageByAlias } from '../utils'; attrs.toLowerCase().includes("'jsx'") || attrs.toLowerCase().includes("'tsx'") ) { - scriptContent = 'import { h } from "vue";\n' + scriptContent; + scriptContent = 'import { h, Fragment } from "vue";\n' + scriptContent; } return ``; }); @@ -254,6 +254,7 @@ import { getLanguageByAlias } from '../utils'; config.customSettings.typescript = { ...config.customSettings.typescript, jsxFactory: 'h', + jsxFragmentFactory: 'Fragment', }; content = await compileAllBlocks(content, config, { prepareFn }); From 6f630c2d723c2a9d9d09044cdba368820c7536f4 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Tue, 23 Jan 2024 02:56:53 +0200 Subject: [PATCH 18/44] edit starter templates --- .../templates/starter/mdx-starter.ts | 10 ++++----- .../templates/starter/react-starter.ts | 22 ++++++++++++++----- .../templates/starter/vue-sfc-starter.ts | 10 +++++++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/livecodes/templates/starter/mdx-starter.ts b/src/livecodes/templates/starter/mdx-starter.ts index 11b446d6f..cad5da47b 100644 --- a/src/livecodes/templates/starter/mdx-starter.ts +++ b/src/livecodes/templates/starter/mdx-starter.ts @@ -8,9 +8,9 @@ export const mdxStarter: Template = { markup: { language: 'mdx', content: ` -import { Hello, Counter } from './script'; +import { Greeting, Counter } from './script'; - + ![MDX Logo]({{ __livecodes_baseUrl__ }}assets/templates/mdx.svg) @@ -33,11 +33,11 @@ img { script: { language: 'jsx', content: ` -import React, { useState } from "react"; +import { useState } from "react"; -export const Hello = (props) =>

Hello, {props.title || "World"}!

; +export const Greeting = (props) =>

Hello, {props.name || "World"}!

; -export function Counter(props) { +export function Counter() { const [count, setCount] = useState(0); return (
diff --git a/src/livecodes/templates/starter/react-starter.ts b/src/livecodes/templates/starter/react-starter.ts index db497a1c3..43fc1bca1 100644 --- a/src/livecodes/templates/starter/react-starter.ts +++ b/src/livecodes/templates/starter/react-starter.ts @@ -27,20 +27,32 @@ export const reactStarter: Template = { content: ` import { useState } from "react"; -function Counter(props) { - const [count, setCount] = useState(0); +function Greeting(props) { return ( -
+ <>

Hello, {props.name}!

logo + + ); +} + +function Counter() { + const [count, setCount] = useState(0); + return ( + <>

You clicked {count} times.

-
+ ); } export default function App() { - return ; + return ( +
+ + +
+ ); } `.trimStart(), }, diff --git a/src/livecodes/templates/starter/vue-sfc-starter.ts b/src/livecodes/templates/starter/vue-sfc-starter.ts index 6dd83bdf6..caf750437 100644 --- a/src/livecodes/templates/starter/vue-sfc-starter.ts +++ b/src/livecodes/templates/starter/vue-sfc-starter.ts @@ -16,16 +16,22 @@ export const vueSfcStarter: Template = { script: { language: 'vue', content: ` -