From bc94651ea31b26ef8d356016ad78aa103f0ca876 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 9 Jul 2025 13:11:26 +0300 Subject: [PATCH 01/94] upgrade node version --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index d78bf0a56c..e35b986d37 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.20.4 +v24.1.0 From 0b1c2ebdf48cdccdfc22a4b8e9b50862f6f3d1df Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 10 Jul 2025 01:07:28 +0300 Subject: [PATCH 02/94] refactor vendors.ts to use `getUrl` & file extensions --- src/livecodes/languages/utils.ts | 5 ++- src/livecodes/types/bundle-types.ts | 4 +- src/livecodes/vendors.ts | 60 +++++++++++++++++------------ 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/livecodes/languages/utils.ts b/src/livecodes/languages/utils.ts index 903ca3aa17..beafe93f45 100644 --- a/src/livecodes/languages/utils.ts +++ b/src/livecodes/languages/utils.ts @@ -1,6 +1,6 @@ import type { Compiler, Config, CustomSettings, Language, Processor } from '../models'; import { getLanguageCustomSettings } from '../utils/utils'; -import { highlightjsUrl } from '../vendors'; +import { vendorsBaseUrl } from '../vendors'; export const getLanguageByAlias = (alias: string = ''): Language | undefined => { if (!alias) return; @@ -97,7 +97,8 @@ export const getCustomSettings = ( export const detectLanguage = async (code: string, languages: Language[]) => { (window as any).HighlightJS = - (window as any).HighlightJS || (await import(highlightjsUrl)).default; + (window as any).HighlightJS || + (await import(vendorsBaseUrl + 'highlight.js/highlight.js')).default; const result = (window as any).HighlightJS.highlightAuto(code, languages); return { language: result.language as Language, diff --git a/src/livecodes/types/bundle-types.ts b/src/livecodes/types/bundle-types.ts index 89da0f9284..bf03eaaa38 100644 --- a/src/livecodes/types/bundle-types.ts +++ b/src/livecodes/types/bundle-types.ts @@ -1,6 +1,6 @@ // based on dts-bundle -import { pathBrowserifyUrl } from '../vendors'; +import { vendorsBaseUrl } from '../vendors'; // const dtsExp = /\.d\.ts$/; const bomOptExp = /^\uFEFF?/; @@ -62,7 +62,7 @@ export interface BundleResult { } export async function bundle(options: Options): Promise { - const path = await import(pathBrowserifyUrl); + const path = (await import(vendorsBaseUrl + 'path-browserify/path-browserify.js')).default; assert(typeof options === 'object' && options, 'options must be an object'); // option parsing & validation diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 3ee6afdcfc..276ee64150 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -1,9 +1,15 @@ import { modulesService } from './services/modules'; -const { getUrl, getModuleUrl } = modulesService; +// - only use `getUrl` or full URL (not `getModuleUrl`) +// - always add full version and file extension +// - minimize usage of baseUrls if possible +// - if es module imports others, use baseUrl instead +// - after `vendorBaseUrl` the file is sorted alphabetically -export const vendorsBaseUrl = // 'http://127.0.0.1:8081/'; - /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.3/dist/'); +const { getUrl } = modulesService; + +export const vendorsBaseUrl = 'http://127.0.0.1:8081/'; +// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.3/dist/'); export const acornUrl = /* @__PURE__ */ getUrl('acorn@8.12.1/dist/acorn.js'); @@ -41,7 +47,7 @@ export const browserJestUrl = /* @__PURE__ */ getUrl( export const brythonBaseUrl = /* @__PURE__ */ getUrl('brython@3.12.4/'); -export const chaiUrl = /* @__PURE__ */ getModuleUrl('chai@5.1.2'); +export const chaiUrl = /* @__PURE__ */ getUrl('chai@5.2.1/chai.js'); export const cherryCljsBaseUrl = /* @__PURE__ */ getUrl('cherry-cljs@0.2.19/'); @@ -142,7 +148,7 @@ export const fontCascadiaCodeUrl = /* @__PURE__ */ getUrl( ); export const fontCodeNewRomanUrl = /* @__PURE__ */ getUrl( - 'https://fonts.cdnfonts.com/css/code-new-roman-2', + 'https://fonts.cdnfonts.com/css/code-new-roman-2?style.css', ); export const fontComicMonoUrl = /* @__PURE__ */ getUrl('comic-mono@0.0.1/index.css'); @@ -152,7 +158,7 @@ export const fontCourierPrimeUrl = /* @__PURE__ */ getUrl( ); export const fontDECTerminalModernUrl = /* @__PURE__ */ getUrl( - 'https://fonts.cdnfonts.com/css/dec-terminal-modern', + 'https://fonts.cdnfonts.com/css/dec-terminal-modern?style.css', ); export const fontDejaVuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/dejavu-mono@4.5.4/index.css'); @@ -163,22 +169,24 @@ export const fontFantasqueUrl = /* @__PURE__ */ getUrl( export const fontFiraCodeUrl = /* @__PURE__ */ getUrl('firacode@6.2.0/distr/fira_code.css'); -export const fontFixedsysUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/fixedsys-62'); +export const fontFixedsysUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/fixedsys-62?style.css', +); export const fontHackUrl = /* @__PURE__ */ getUrl('hack-font@3.3.0/build/web/hack.css'); export const fontHermitUrl = /* @__PURE__ */ getUrl('typeface-hermit@0.0.44/index.css'); export const fontIBMPlexMonoUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap', + 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap&style.css', ); export const fontInconsolataUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=Inconsolata&display=swap', + 'https://fonts.googleapis.com/css2?family=Inconsolata&display=swap&style.css', ); export const fontInterUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css?family=Inter:300,400,500', + 'https://fonts.googleapis.com/css?family=Inter:300,400,500&style.css', ); export const fontIosevkaUrl = /* @__PURE__ */ getUrl('@fontsource/iosevka@4.5.4/index.css'); @@ -188,23 +196,27 @@ export const fontJetbrainsMonoUrl = /* @__PURE__ */ getUrl( ); export const fontMaterialIconsUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css?family=Material+Icons&display=swap', + 'https://fonts.googleapis.com/css?family=Material+Icons&display=swap&style.css', ); -export const fontMenloUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/menlo'); +export const fontMenloUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/menlo?style.css', +); export const fontMonaspaceBaseUrl = /* @__PURE__ */ getUrl('monaspace-font@0.0.2/'); -export const fontMonofurUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/monofur'); +export const fontMonofurUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/monofur?style.css', +); export const fontMonoidUrl = /* @__PURE__ */ getUrl('@typopro/web-monoid@3.7.5/TypoPRO-Monoid.css'); export const fontNotoUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap', + 'https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap&style.css', ); export const fontNovaMonoUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=Nova+Mono&display=swap', + 'https://fonts.googleapis.com/css2?family=Nova+Mono&display=swap&style.css', ); export const fontOpenDyslexicUrl = /* @__PURE__ */ getUrl( @@ -212,12 +224,14 @@ export const fontOpenDyslexicUrl = /* @__PURE__ */ getUrl( ); export const fontProFontWindowsUrl = /* @__PURE__ */ getUrl( - 'https://fonts.cdnfonts.com/css/profontwindows', + 'https://fonts.cdnfonts.com/css/profontwindows?style.css', ); export const fontRobotoMonoUrl = /* @__PURE__ */ getUrl('@fontsource/roboto-mono@4.5.8/index.css'); -export const fontSFMonoUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/sf-mono'); +export const fontSFMonoUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/sf-mono?style.css', +); export const fontSourceCodeProUrl = /* @__PURE__ */ getUrl( '@fontsource/source-code-pro@4.5.12/index.css', @@ -225,7 +239,9 @@ export const fontSourceCodeProUrl = /* @__PURE__ */ getUrl( export const fontSpaceMonoUrl = /* @__PURE__ */ getUrl('@fontsource/space-mono@4.5.10/index.css'); -export const fontSudoVarUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/sudo-var'); +export const fontSudoVarUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/sudo-var?style.css', +); export const fontUbuntuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/ubuntu-mono@4.5.11/index.css'); @@ -245,8 +261,6 @@ export const graphreCdnUrl = /* @__PURE__ */ getUrl('graphre@0.1.3/dist/graphre. export const handlebarsBaseUrl = /* @__PURE__ */ getUrl('handlebars@4.7.8/dist/'); -export const highlightjsUrl = /* @__PURE__ */ getModuleUrl('highlight.js@11.11.1'); - export const hpccJsCdnUrl = /* @__PURE__ */ getUrl('@hpcc-js/wasm@2.13.0/dist/index.js'); export const htmlToImageUrl = /* @__PURE__ */ getUrl('html-to-image@1.11.11/dist/html-to-image.js'); @@ -319,8 +333,6 @@ 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 pgliteUrl = /* @__PURE__ */ getUrl('@electric-sql/pglite@0.1.5/dist/index.js'); export const pintoraUrl = /* @__PURE__ */ getUrl( @@ -445,9 +457,9 @@ export const vegaCdnUrl = /* @__PURE__ */ getUrl('vega@5.25.0/build/vega.js'); export const vegaLiteCdnUrl = /* @__PURE__ */ getUrl('vega-lite@5.9.3/build/vega-lite.js'); -export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3'); +export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3.5.17/dist/vue.global.js'); -export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2'); +export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2.7.16/dist/vue.js'); export const vueRuntimeUrl = /* @__PURE__ */ getUrl('vue@3/dist/vue.runtime.esm-browser.prod.js'); From 70c7587fc2211489db0b9c7e0d21e4c17e648b97 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 10 Jul 2025 12:42:58 +0300 Subject: [PATCH 03/94] optimize vendor urls for download --- .../diagrams/lang-diagrams-compiler-esm.ts | 6 ++-- .../lang-postgresql-compiler-esm.ts | 4 +-- .../rescript/lang-rescript-compiler-esm.ts | 12 +++---- .../rescript/lang-rescript-formatter.ts | 4 +-- src/livecodes/vendors.ts | 33 ++++++++++++------- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts b/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts index ff45f7120b..d27d3da78d 100644 --- a/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts +++ b/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts @@ -16,7 +16,7 @@ import { cytoscapeUrl, elkjsBaseUrl, graphreCdnUrl, - hpccJsCdnUrl, + hpccJsCdnBaseUrl, mermaidCdnUrl, nomnomlCdnUrl, pintoraUrl, @@ -180,7 +180,7 @@ const compileGnuplot = async (code: string) => { const compileMermaid = async (code: string) => { let mermaid: any; const load = async () => { - mermaid = (await import(mermaidCdnUrl)).default; + mermaid = await loadScript(mermaidCdnUrl, 'mermaid'); mermaid.initialize({ startOnLoad: false, }); @@ -201,7 +201,7 @@ const compileMermaid = async (code: string) => { const compileGraphviz = async (code: string) => { let graphviz: any; const load = async () => { - const hpccWasm = await import(hpccJsCdnUrl); + const hpccWasm = await import(hpccJsCdnBaseUrl + 'index.js'); graphviz = await hpccWasm.Graphviz.load(); }; const render = (src: string, script: HTMLScriptElement) => { diff --git a/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts b/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts index aacc74f089..baa85b387b 100644 --- a/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts +++ b/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts @@ -1,6 +1,6 @@ import type { CompilerFunction } from '../../models'; import { getLanguageCustomSettings, safeName } from '../../utils/utils'; -import { pgliteUrl } from '../../vendors'; +import { pgliteBaseUrl } from '../../vendors'; declare global { interface Window { @@ -11,7 +11,7 @@ declare global { export const pgSqlCompiler: CompilerFunction = async (code, { config }) => { if (!code.trim()) return '{ data: [] }'; - window.PGlite = window.PGlite || (await import(pgliteUrl)).PGlite; + window.PGlite = window.PGlite || (await import(pgliteBaseUrl + 'index.js')).PGlite; const options = getLanguageCustomSettings('pgsql', config); const { dbName, scriptURLs, ...pgliteOptions } = options; diff --git a/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts b/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts index c12bf5243d..53170e41de 100644 --- a/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts +++ b/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts @@ -6,7 +6,10 @@ import { reasonReactUrl, reasonStdLibBaseUrl, requireUrl, - rescriptCdnBaseUrl, + rescriptCdnUrl1, + rescriptCdnUrl2, + rescriptCdnUrl3, + rescriptCdnUrl4, rescriptStdLibBaseUrl, } from '../../vendors'; @@ -57,12 +60,7 @@ const loadCompiler = async (language: Language) => { ); } else { window.require( - [ - rescriptCdnBaseUrl + 'compiler.js', - rescriptCdnBaseUrl + 'compiler-builtins/cmij.js', - rescriptCdnBaseUrl + '%40rescript/react/cmij.js', - rescriptCdnBaseUrl + '%40rescript/core/cmij.js', - ], + [rescriptCdnUrl1, rescriptCdnUrl2, rescriptCdnUrl3, rescriptCdnUrl4], () => { window.rescript_ocaml_compiler = window.rescript_compiler; window.rescript_compiler = undefined; diff --git a/src/livecodes/languages/rescript/lang-rescript-formatter.ts b/src/livecodes/languages/rescript/lang-rescript-formatter.ts index 6851d6cb7d..4036a7d642 100644 --- a/src/livecodes/languages/rescript/lang-rescript-formatter.ts +++ b/src/livecodes/languages/rescript/lang-rescript-formatter.ts @@ -1,12 +1,12 @@ import type { LanguageFormatter } from '../../models'; import { getAbsoluteUrl } from '../../utils'; -import { rescriptCdnBaseUrl } from '../../vendors'; +import { rescriptCdnUrl1 } from '../../vendors'; declare const importScripts: any; const createRescriptFormatter: LanguageFormatter['factory'] = (baseUrl, language) => { if (!(self as any).rescript_compiler) { - importScripts(getAbsoluteUrl(rescriptCdnBaseUrl + 'compiler.js', baseUrl)); + importScripts(getAbsoluteUrl(rescriptCdnUrl1, baseUrl)); } const compiler = (self as any).rescript_compiler.make(); compiler.setModuleSystem('es6'); diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 276ee64150..ff18070b82 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -9,7 +9,7 @@ import { modulesService } from './services/modules'; const { getUrl } = modulesService; export const vendorsBaseUrl = 'http://127.0.0.1:8081/'; -// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.3/dist/'); +// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.4/dist/'); export const acornUrl = /* @__PURE__ */ getUrl('acorn@8.12.1/dist/acorn.js'); @@ -261,7 +261,7 @@ export const graphreCdnUrl = /* @__PURE__ */ getUrl('graphre@0.1.3/dist/graphre. export const handlebarsBaseUrl = /* @__PURE__ */ getUrl('handlebars@4.7.8/dist/'); -export const hpccJsCdnUrl = /* @__PURE__ */ getUrl('@hpcc-js/wasm@2.13.0/dist/index.js'); +export const hpccJsCdnBaseUrl = /* @__PURE__ */ getUrl('@hpcc-js/wasm@2.13.0/dist/'); export const htmlToImageUrl = /* @__PURE__ */ getUrl('html-to-image@1.11.11/dist/html-to-image.js'); @@ -297,11 +297,11 @@ export const lunaObjViewerStylesUrl = /* @__PURE__ */ getUrl( 'luna-object-viewer@0.2.4/luna-object-viewer.css', ); -export const malinaBaseUrl = /* @__PURE__ */ getUrl(`malinajs@0.7.19/`); +export const malinaBaseUrl = /* @__PURE__ */ getUrl('malinajs@0.7.19/'); export const markedUrl = /* @__PURE__ */ getUrl('marked@13.0.2/marked.min.js'); -export const mermaidCdnUrl = /* @__PURE__ */ getUrl('mermaid@10.2.2/dist/mermaid.esm.mjs'); +export const mermaidCdnUrl = /* @__PURE__ */ getUrl('mermaid@10.2.2/dist/mermaid.min.js'); export const metaPngUrl = /* @__PURE__ */ getUrl('meta-png@1.0.6/dist/meta-png.umd.js'); @@ -333,7 +333,7 @@ export const opalBaseUrl = /* @__PURE__ */ getUrl('https://cdn.opalrb.com/opal/1 export const parinferUrl = /* @__PURE__ */ getUrl('parinfer@3.13.1/parinfer.js'); -export const pgliteUrl = /* @__PURE__ */ getUrl('@electric-sql/pglite@0.1.5/dist/index.js'); +export const pgliteBaseUrl = /* @__PURE__ */ getUrl('@electric-sql/pglite@0.1.5/dist/'); export const pintoraUrl = /* @__PURE__ */ getUrl( '@pintora/standalone@0.6.2/lib/pintora-standalone.umd.js', @@ -393,7 +393,18 @@ export const reasonReactUrl = /* @__PURE__ */ getUrl( export const reasonStdLibBaseUrl = /* @__PURE__ */ getUrl('@rescript/std@9.1.3/lib/es6/'); -export const rescriptCdnBaseUrl = /* @__PURE__ */ getUrl('https://cdn.rescript-lang.org/v11.1.2/'); +export const rescriptCdnUrl1 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/compiler.js', +); +export const rescriptCdnUrl2 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/compiler-builtins/cmij.js', +); +export const rescriptCdnUrl3 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/%40rescript/react/cmij.js', +); +export const rescriptCdnUrl4 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/%40rescript/core/cmij.js', +); export const rescriptStdLibBaseUrl = /* @__PURE__ */ getUrl('@rescript/std@11.1.2/lib/es6/'); @@ -447,19 +458,19 @@ export const tesseractUrl = /* @__PURE__ */ getUrl('tesseract.js@6.0.1/dist/tess export const twigUrl = /* @__PURE__ */ getUrl('twig@1.17.1/twig.min.js'); -export const typescriptUrl = /* @__PURE__ */ getUrl(`typescript@5.6.2/lib/typescript.js`); +export const typescriptUrl = /* @__PURE__ */ getUrl('typescript@5.6.2/lib/typescript.js'); export const typescriptVfsUrl = /* @__PURE__ */ getUrl('@typescript/vfs@1.5.3/dist/vfs.esm.js'); export const uniterUrl = /* @__PURE__ */ getUrl('uniter@2.18.0/dist/uniter.js'); -export const vegaCdnUrl = /* @__PURE__ */ getUrl('vega@5.25.0/build/vega.js'); +export const vegaCdnUrl = /* @__PURE__ */ getUrl('vega@5.25.0/build/vega.min.js'); -export const vegaLiteCdnUrl = /* @__PURE__ */ getUrl('vega-lite@5.9.3/build/vega-lite.js'); +export const vegaLiteCdnUrl = /* @__PURE__ */ getUrl('vega-lite@5.9.3/build/vega-lite.min.js'); -export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3.5.17/dist/vue.global.js'); +export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3.5.17/dist/vue.global.prod.js'); -export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2.7.16/dist/vue.js'); +export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2.7.16/dist/vue.min.js'); export const vueRuntimeUrl = /* @__PURE__ */ getUrl('vue@3/dist/vue.runtime.esm-browser.prod.js'); From 17558882f4454ef9266f5fd36d3233a17102bad7 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 10 Jul 2025 13:24:02 +0300 Subject: [PATCH 04/94] change fonts to use BaseUrl --- src/livecodes/editor/fonts.ts | 76 +++++++++---------- .../python-wasm/lang-python-wasm-script.ts | 4 +- src/livecodes/vendors.ts | 56 ++++++-------- 3 files changed, 64 insertions(+), 72 deletions(-) diff --git a/src/livecodes/editor/fonts.ts b/src/livecodes/editor/fonts.ts index d5bdd7f612..d161468621 100644 --- a/src/livecodes/editor/fonts.ts +++ b/src/livecodes/editor/fonts.ts @@ -1,36 +1,36 @@ import { - fontAnonymousProUrl, - fontAstigmataUrl, - fontCascadiaCodeUrl, + fontAnonymousProBaseUrl, + fontAstigmataBaseUrl, + fontCascadiaCodeBaseUrl, fontCodeNewRomanUrl, - fontComicMonoUrl, - fontCourierPrimeUrl, + fontComicMonoBaseUrl, + fontCourierPrimeBaseUrl, fontDECTerminalModernUrl, - fontDejaVuMonoUrl, - fontFantasqueUrl, - fontFiraCodeUrl, + fontDejaVuMonoBaseUrl, + fontFantasqueBaseUrl, + fontFiraCodeBaseUrl, fontFixedsysUrl, - fontHackUrl, - fontHermitUrl, + fontHackBaseUrl, + fontHermitBaseUrl, fontIBMPlexMonoUrl, fontInconsolataUrl, - fontIosevkaUrl, - fontJetbrainsMonoUrl, + fontIosevkaBaseUrl, + fontJetbrainsMonoBaseUrl, fontMenloUrl, fontMonaspaceBaseUrl, fontMonofurUrl, - fontMonoidUrl, + fontMonoidBaseUrl, fontNotoUrl, fontNovaMonoUrl, - fontOpenDyslexicUrl, + fontOpenDyslexicBaseUrl, fontProFontWindowsUrl, - fontRobotoMonoUrl, + fontRobotoMonoBaseUrl, fontSFMonoUrl, - fontSourceCodeProUrl, - fontSpaceMonoUrl, + fontSourceCodeProBaseUrl, + fontSpaceMonoBaseUrl, fontSudoVarUrl, - fontUbuntuMonoUrl, - fontVictorMonoUrl, + fontUbuntuMonoBaseUrl, + fontVictorMonoBaseUrl, } from '../vendors'; export interface Font { @@ -44,17 +44,17 @@ export const fonts: Font[] = [ { id: 'anonymous-pro', name: 'Anonymous Pro', - url: fontAnonymousProUrl, + url: fontAnonymousProBaseUrl + 'index.css', }, { id: 'astigmata', name: 'Astigmata', - url: fontAstigmataUrl, + url: fontAstigmataBaseUrl + 'index.css', }, { id: 'cascadia-code', name: 'Cascadia Code', - url: fontCascadiaCodeUrl, + url: fontCascadiaCodeBaseUrl + 'index.css', }, { id: 'comic-mono', @@ -64,12 +64,12 @@ export const fonts: Font[] = [ { id: 'comic-mono', name: 'Comic Mono', - url: fontComicMonoUrl, + url: fontComicMonoBaseUrl + 'index.css', }, { id: 'courier-prime', name: 'Courier Prime', - url: fontCourierPrimeUrl, + url: fontCourierPrimeBaseUrl + 'index.css', }, { id: 'dec-terminal-modern', @@ -79,18 +79,18 @@ export const fonts: Font[] = [ { id: 'dejavu-mono', name: 'DejaVu Mono', - url: fontDejaVuMonoUrl, + url: fontDejaVuMonoBaseUrl + 'index.css', }, { id: 'fantasque-sans-mono', name: 'TypoPRO Fantasque Sans Mono', label: 'Fantasque Sans Mono', - url: fontFantasqueUrl, + url: fontFantasqueBaseUrl + 'TypoPRO-FantasqueSansMono.css', }, { id: 'fira-code', name: 'Fira Code', - url: fontFiraCodeUrl, + url: fontFiraCodeBaseUrl + 'fira_code.css', }, { id: 'fixedsys', @@ -101,12 +101,12 @@ export const fonts: Font[] = [ { id: 'hack', name: 'Hack', - url: fontHackUrl, + url: fontHackBaseUrl + 'hack.css', }, { id: 'hermit', name: 'Hermit', - url: fontHermitUrl, + url: fontHermitBaseUrl + 'index.css', }, { id: 'ibm-plex-mono', @@ -121,12 +121,12 @@ export const fonts: Font[] = [ { id: 'iosevka', name: 'Iosevka', - url: fontIosevkaUrl, + url: fontIosevkaBaseUrl + 'index.css', }, { id: 'jetbrains-mono', name: 'JetBrains Mono', - url: fontJetbrainsMonoUrl, + url: fontJetbrainsMonoBaseUrl + 'index.css', }, { id: 'menlo', @@ -167,7 +167,7 @@ export const fonts: Font[] = [ id: 'monoid', name: 'TypoPRO Monoid', label: 'Monoid', - url: fontMonoidUrl, + url: fontMonoidBaseUrl + 'TypoPRO-Monoid.css', }, { id: 'noto-sans-mono', @@ -182,7 +182,7 @@ export const fonts: Font[] = [ { id: 'opendyslexic', name: 'OpenDyslexic', - url: fontOpenDyslexicUrl, + url: fontOpenDyslexicBaseUrl + 'index.css', }, { id: 'profontwindows', @@ -193,7 +193,7 @@ export const fonts: Font[] = [ { id: 'roboto-mono', name: 'Roboto Mono', - url: fontRobotoMonoUrl, + url: fontRobotoMonoBaseUrl + 'index.css', }, { id: 'sf-mono', @@ -203,12 +203,12 @@ export const fonts: Font[] = [ { id: 'source-code-pro', name: 'Source Code Pro', - url: fontSourceCodeProUrl, + url: fontSourceCodeProBaseUrl + 'index.css', }, { id: 'space-mono', name: 'Space Mono', - url: fontSpaceMonoUrl, + url: fontSpaceMonoBaseUrl + 'index.css', }, { id: 'sudo-var', @@ -218,12 +218,12 @@ export const fonts: Font[] = [ { id: 'ubuntu-mono', name: 'Ubuntu Mono', - url: fontUbuntuMonoUrl, + url: fontUbuntuMonoBaseUrl + 'index.css', }, { id: 'victor-mono', name: 'Victor Mono', - url: fontVictorMonoUrl, + url: fontVictorMonoBaseUrl + 'index.css', }, ]; diff --git a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts index e0ec66b066..1c21931348 100644 --- a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts +++ b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { fontAwesomeUrl, pyodideBaseUrl } from '../../vendors'; +import { fontAwesomeBaseUrl, pyodideBaseUrl } from '../../vendors'; declare const loadPyodide: any; @@ -53,7 +53,7 @@ window.addEventListener('load', async () => { // needed for matplotlib icons const stylesheet = document.createElement('link'); stylesheet.rel = 'stylesheet'; - stylesheet.href = fontAwesomeUrl; + stylesheet.href = fontAwesomeBaseUrl + 'css/font-awesome.min.css'; document.head.append(stylesheet); await pyodideReady; diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index ff18070b82..e5f58a0e98 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -133,49 +133,43 @@ export const fflateUrl = /* @__PURE__ */ getUrl('fflate@0.8.1/esm/browser.js'); export const flexSearchUrl = /* @__PURE__ */ getUrl('flexsearch@0.7.21/dist/flexsearch.bundle.js'); -export const fontAnonymousProUrl = /* @__PURE__ */ getUrl( - '@fontsource/anonymous-pro@4.5.9/index.css', -); +export const fontAnonymousProBaseUrl = /* @__PURE__ */ getUrl('@fontsource/anonymous-pro@4.5.9/'); -export const fontAstigmataUrl = /* @__PURE__ */ getUrl( - 'gh:hatemhosny/astigmata-font@6d0ee00a07fb1932902f0b81a504d075d47bd52f/index.css', +export const fontAstigmataBaseUrl = /* @__PURE__ */ getUrl( + 'gh:hatemhosny/astigmata-font@6d0ee00a07fb1932902f0b81a504d075d47bd52f/', ); -export const fontAwesomeUrl = /* @__PURE__ */ getUrl('font-awesome@4.7.0/css/font-awesome.min.css'); +export const fontAwesomeBaseUrl = /* @__PURE__ */ getUrl('font-awesome@4.7.0/'); -export const fontCascadiaCodeUrl = /* @__PURE__ */ getUrl( - '@fontsource/cascadia-code@4.2.1/index.css', -); +export const fontCascadiaCodeBaseUrl = /* @__PURE__ */ getUrl('@fontsource/cascadia-code@4.2.1/'); export const fontCodeNewRomanUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/code-new-roman-2?style.css', ); -export const fontComicMonoUrl = /* @__PURE__ */ getUrl('comic-mono@0.0.1/index.css'); +export const fontComicMonoBaseUrl = /* @__PURE__ */ getUrl('comic-mono@0.0.1/'); -export const fontCourierPrimeUrl = /* @__PURE__ */ getUrl( - '@fontsource/courier-prime@4.5.9/index.css', -); +export const fontCourierPrimeBaseUrl = /* @__PURE__ */ getUrl('@fontsource/courier-prime@4.5.9/'); export const fontDECTerminalModernUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/dec-terminal-modern?style.css', ); -export const fontDejaVuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/dejavu-mono@4.5.4/index.css'); +export const fontDejaVuMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/dejavu-mono@4.5.4/'); -export const fontFantasqueUrl = /* @__PURE__ */ getUrl( - '@typopro/web-fantasque-sans-mono@3.7.5/TypoPRO-FantasqueSansMono.css', +export const fontFantasqueBaseUrl = /* @__PURE__ */ getUrl( + '@typopro/web-fantasque-sans-mono@3.7.5/', ); -export const fontFiraCodeUrl = /* @__PURE__ */ getUrl('firacode@6.2.0/distr/fira_code.css'); +export const fontFiraCodeBaseUrl = /* @__PURE__ */ getUrl('firacode@6.2.0/distr/'); export const fontFixedsysUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/fixedsys-62?style.css', ); -export const fontHackUrl = /* @__PURE__ */ getUrl('hack-font@3.3.0/build/web/hack.css'); +export const fontHackBaseUrl = /* @__PURE__ */ getUrl('hack-font@3.3.0/build/web/'); -export const fontHermitUrl = /* @__PURE__ */ getUrl('typeface-hermit@0.0.44/index.css'); +export const fontHermitBaseUrl = /* @__PURE__ */ getUrl('typeface-hermit@0.0.44/'); export const fontIBMPlexMonoUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap&style.css', @@ -189,10 +183,10 @@ export const fontInterUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css?family=Inter:300,400,500&style.css', ); -export const fontIosevkaUrl = /* @__PURE__ */ getUrl('@fontsource/iosevka@4.5.4/index.css'); +export const fontIosevkaBaseUrl = /* @__PURE__ */ getUrl('@fontsource/iosevka@4.5.4/'); -export const fontJetbrainsMonoUrl = /* @__PURE__ */ getUrl( - '@fontsource/jetbrains-mono@4.5.11/index.css', +export const fontJetbrainsMonoBaseUrl = /* @__PURE__ */ getUrl( + '@fontsource/jetbrains-mono@4.5.11/', ); export const fontMaterialIconsUrl = /* @__PURE__ */ getUrl( @@ -209,7 +203,7 @@ export const fontMonofurUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/monofur?style.css', ); -export const fontMonoidUrl = /* @__PURE__ */ getUrl('@typopro/web-monoid@3.7.5/TypoPRO-Monoid.css'); +export const fontMonoidBaseUrl = /* @__PURE__ */ getUrl('@typopro/web-monoid@3.7.5/'); export const fontNotoUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap&style.css', @@ -219,33 +213,31 @@ export const fontNovaMonoUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css2?family=Nova+Mono&display=swap&style.css', ); -export const fontOpenDyslexicUrl = /* @__PURE__ */ getUrl( - '@fontsource/opendyslexic@4.5.4/index.css', -); +export const fontOpenDyslexicBaseUrl = /* @__PURE__ */ getUrl('@fontsource/opendyslexic@4.5.4/'); export const fontProFontWindowsUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/profontwindows?style.css', ); -export const fontRobotoMonoUrl = /* @__PURE__ */ getUrl('@fontsource/roboto-mono@4.5.8/index.css'); +export const fontRobotoMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/roboto-mono@4.5.8/'); export const fontSFMonoUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/sf-mono?style.css', ); -export const fontSourceCodeProUrl = /* @__PURE__ */ getUrl( - '@fontsource/source-code-pro@4.5.12/index.css', +export const fontSourceCodeProBaseUrl = /* @__PURE__ */ getUrl( + '@fontsource/source-code-pro@4.5.12/', ); -export const fontSpaceMonoUrl = /* @__PURE__ */ getUrl('@fontsource/space-mono@4.5.10/index.css'); +export const fontSpaceMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/space-mono@4.5.10/'); export const fontSudoVarUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/sudo-var?style.css', ); -export const fontUbuntuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/ubuntu-mono@4.5.11/index.css'); +export const fontUbuntuMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/ubuntu-mono@4.5.11/'); -export const fontVictorMonoUrl = /* @__PURE__ */ getUrl('victormono@1.5.4/dist/index.css'); +export const fontVictorMonoBaseUrl = /* @__PURE__ */ getUrl('victormono@1.5.4/dist/'); export const fscreenUrl = /* @__PURE__ */ getUrl('fscreen@1.2.0/dist/fscreen.esm.js'); From 30d7f4e467f61505df2578825264594220104dbc Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 10 Jul 2025 13:45:55 +0300 Subject: [PATCH 05/94] upgrade pyodide to v0.28.0 --- docs/docs/languages/python-wasm.mdx | 2 +- .../languages/python-wasm/lang-python-wasm-script.ts | 8 +------- src/livecodes/vendors.ts | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/docs/languages/python-wasm.mdx b/docs/docs/languages/python-wasm.mdx index a174a2b301..e98f819e3b 100644 --- a/docs/docs/languages/python-wasm.mdx +++ b/docs/docs/languages/python-wasm.mdx @@ -101,7 +101,7 @@ Check the [starter template](#starter-template) for an example. ### Version -Pyodide v0.25.1, running Python v3.11.3 +Pyodide v0.28.0, running Python v3.13.2 ## Code Formatting diff --git a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts index 1c21931348..b4a378516b 100644 --- a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts +++ b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { fontAwesomeBaseUrl, pyodideBaseUrl } from '../../vendors'; +import { pyodideBaseUrl } from '../../vendors'; declare const loadPyodide: any; @@ -50,12 +50,6 @@ window.addEventListener('load', async () => { } async function prepareEnv() { - // needed for matplotlib icons - const stylesheet = document.createElement('link'); - stylesheet.rel = 'stylesheet'; - stylesheet.href = fontAwesomeBaseUrl + 'css/font-awesome.min.css'; - document.head.append(stylesheet); - await pyodideReady; const patchInput = ` from js import prompt diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index e5f58a0e98..68f51ca905 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -356,7 +356,7 @@ export const prismThemesLaserWaveUrl = /* @__PURE__ */ getUrl( ); export const pyodideBaseUrl = /* @__PURE__ */ getUrl( - 'https://cdn.jsdelivr.net/pyodide/v0.25.1/full/', + 'https://cdn.jsdelivr.net/pyodide/v0.28.0/full/', ); export const qrcodeUrl = /* @__PURE__ */ getUrl('easyqrcodejs@4.6.1/dist/easy.qrcode.min.js'); From 333fc73e6d878119f4b7567d3586fe88a3f97939 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 10 Jul 2025 13:59:01 +0300 Subject: [PATCH 06/94] update opal to use gh CDN --- src/livecodes/vendors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 68f51ca905..e60abb6d8a 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -321,7 +321,7 @@ export const normalizeCssUrl = /* @__PURE__ */ getUrl('normalize.css@8.0.1/norma export const nunjucksBaseUrl = /* @__PURE__ */ getUrl('nunjucks@3.2.4/browser/'); -export const opalBaseUrl = /* @__PURE__ */ getUrl('https://cdn.opalrb.com/opal/1.8.2/'); +export const opalBaseUrl = /* @__PURE__ */ getUrl('gh:opal/opal-cdn@v1.8.2/opal/1.8.2/'); export const parinferUrl = /* @__PURE__ */ getUrl('parinfer@3.13.1/parinfer.js'); From fb278507c1573a4421b8f970bed25d19db989fce Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 10 Jul 2025 18:23:08 +0300 Subject: [PATCH 07/94] build(self-hosting): allow downloading modules during build --- eslint.config.mjs | 1 + package.json | 1 + scripts/build.js | 5 ++ scripts/download-modules.js | 143 ++++++++++++++++++++++++++++++++++++ src/livecodes/vendors.ts | 7 +- 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 scripts/download-modules.js diff --git a/eslint.config.mjs b/eslint.config.mjs index aab8c872fb..1b84e6be5e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,6 +31,7 @@ export default [ '**/.docusaurus', '**/.jest', '**/.storybook', + '**/.cache', 'functions/vendors', ], }, diff --git a/package.json b/package.json index 32edc9a344..588f6ffa5a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "build:docs": "cd docs && npm run build", "build:storybook": "cd storybook && npm run build", "copy:assets": "recursive-delete build/livecodes/assets && mkdirp build/livecodes/assets && recursive-copy src/livecodes/assets build/livecodes/assets", + "download-modules": "node ./scripts/download-modules.js", "typedocs": "run-s typedocs:*", "typedocs:livecodes": "typedoc src/livecodes/main.ts src/livecodes/app.ts src/livecodes/embed.ts src/livecodes/_modules.ts --out build/typedocs/livecodes --exclude **/*.spec.ts --excludeExternals", "typedocs:sdk": "typedoc src/sdk/livecodes.ts --out build/typedocs/sdk --exclude **/*.spec.ts --excludeExternals", diff --git a/scripts/build.js b/scripts/build.js index 13058760d2..222ea61e39 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -3,6 +3,7 @@ const { minify: minifyHTML, default: minifyHTMLPlugin } = require('esbuild-plugi const fs = require('fs'); const path = require('path'); +const { downloadModules } = require('./download-modules'); const { applyHash } = require('./hash'); const { injectCss } = require('./inject-css'); const { buildStyles } = require('./styles'); @@ -11,6 +12,7 @@ const { arrToObj, mkdir, uint8arrayToString, iife, getFileNames, getEnvVars } = const args = process.argv.slice(2); const devMode = args.includes('--dev'); +const localModules = args.includes('--download-modules') || process.env.LOCAL_MODULES === 'true'; const root = path.resolve(__dirname + '/..'); const outDir = path.resolve(root, 'build'); @@ -316,6 +318,9 @@ const functionsBuild = () => const stylesBuild = () => buildStyles(devMode); prepareDir().then(async () => { + if (localModules) { + downloadModules(); + } await buildLocalePathLoader(); Promise.all([ esmBuild(), diff --git a/scripts/download-modules.js b/scripts/download-modules.js new file mode 100644 index 0000000000..d4cafce4ac --- /dev/null +++ b/scripts/download-modules.js @@ -0,0 +1,143 @@ +const fs = require('fs'); +const path = require('path'); +const sdkPkg = require('../src/sdk/package.sdk.json'); + +const downloadModules = async ({ dryRun = false } = {}) => { + const vendorsModule = 'src/livecodes/vendors.ts'; + const tempDir = '.cache/'; + const modulesDir = tempDir + '/modules/'; + const outputDir = 'build/modules/'; + + /** @type {string[]} */ + const modules = []; + /** @type {string[]} */ + const baseUrls = []; + /** @type {Array<{module: string; url: string}>} */ + const moduleUrls = []; + + fs.mkdirSync(modulesDir, { recursive: true }); + + const verdorModulesContent = + 'const modulesService = { getUrl: (mod) => mod };\n' + + fs + .readFileSync(vendorsModule, 'utf8') + .replace('import', '// import') + .replace('process.env.SDK_VERSION', `"${sdkPkg.version}"`); + fs.writeFileSync(tempDir + 'vendors.js', verdorModulesContent, 'utf8'); + + const vendorUrls = require('../' + tempDir + 'vendors.js'); + + // modules vs baseUrls + for (const [key, value] of Object.entries(vendorUrls)) { + if (key.includes('BaseUrl')) { + baseUrls.push(value); + } else { + modules.push(value); + } + } + + // get modules from baseUrls + for (const baseUrl of baseUrls) { + // https://unpkg.com/@seth0x41/doppio@1.0.0/ + if (baseUrl.startsWith('https://unpkg.com/')) { + baseUrl.replace('https://unpkg.com/', ''); + } + if (baseUrl.startsWith('https://')) continue; + const mod = getModuleName(baseUrl); + const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; + const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; + const modInfo = await fetch(modInfoUrl).then((res) => res.json()); + const files = modInfo.files; + if (!Array.isArray(files)) continue; + for (const file of files) { + if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { + modules.push(mod + file.name); + } + } + } + + // get moduleUrls + for (const module of modules) { + if (module.startsWith('http')) { + moduleUrls.push({ module, url: module }); + } else if (module.startsWith('gh:')) { + moduleUrls.push({ + module, + url: `https://cdn.jsdelivr.net/gh/${module.replace('gh:', '')}`, + }); + } else { + // use unpkg - no restriction on file types (e.g. jar) + moduleUrls.push({ module, url: `https://unpkg.com/${module}` }); + } + // TODO: handle modules hosted elsewhere: + // - https://cdn.jsdelivr.net/pyodide/v0.25.1/full/ -> https://pyodide.org/en/stable/usage/downloading-and-deploying.html#github-releases + // TODO: handle font absolute urls in css (in font CDNs) + } + + // download modules + for (const { module, url } of moduleUrls) { + const fullPath = + modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); + const dirPath = path.dirname(fullPath); + if (fs.existsSync(fullPath)) continue; + + let text = ''; + if (dryRun) { + text = url; + } else { + const res = await fetch(url); + if (!res.ok) { + console.warn(`Failed to fetch ${module}: ${res.statusText}`); + continue; + } + text = await res.text(); + } + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(fullPath, text); + } + + // copy to build directory + fs.mkdirSync(outputDir, { recursive: true }); + fs.promises.cp(modulesDir, outputDir, { recursive: true }); + + // cleanup + fs.rmSync(tempDir + 'vendors.js'); + + // utils + /** + * @param {string} module + */ + function getModuleName(module) { + if (module.startsWith('gh:')) { + return module.replace('gh:', ''); + } + const parts = module.split('/'); + if (parts[0].startsWith('@')) { + return parts[0] + '/' + parts[1]; + } else { + return parts[0]; + } + } + /** + * @param {string} module + */ + function shouldExclude(module) { + const includePackages = ['@live-codes/browser-compilers']; + const excludeExtensions = ['.map', '.md', '.txt', '.d.ts', 'package.json', 'package-lock.json']; + for (const pkg of includePackages) { + if (module.includes(pkg)) return false; + } + for (const extension of excludeExtensions) { + if (module.endsWith(extension)) { + return true; + } + } + return false; + } +}; + +module.exports = { downloadModules }; + +if (require.main === module) { + downloadModules({ dryRun: process.argv.includes('--dry-run') }); +} diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index e60abb6d8a..b664b3a77c 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -4,12 +4,13 @@ import { modulesService } from './services/modules'; // - always add full version and file extension // - minimize usage of baseUrls if possible // - if es module imports others, use baseUrl instead -// - after `vendorBaseUrl` the file is sorted alphabetically +// - excluding `vendorsBaseUrl`, the file is sorted alphabetically +// see scripts/download-modules.js const { getUrl } = modulesService; -export const vendorsBaseUrl = 'http://127.0.0.1:8081/'; -// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.4/dist/'); +export const vendorsBaseUrl = // 'http://127.0.0.1:8081/'; + /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.4/dist/'); export const acornUrl = /* @__PURE__ */ getUrl('acorn@8.12.1/dist/acorn.js'); From 37ad7fa044b3383272dcf5c85adb1ea3fc140fd7 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 13 Jul 2025 02:22:37 +0300 Subject: [PATCH 08/94] fix --- scripts/download-modules.js | 2 +- src/livecodes/languages/clojurescript/lang-clojurescript.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index d4cafce4ac..866b3bdc16 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -123,7 +123,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { */ function shouldExclude(module) { const includePackages = ['@live-codes/browser-compilers']; - const excludeExtensions = ['.map', '.md', '.txt', '.d.ts', 'package.json', 'package-lock.json']; + const excludeExtensions = ['.map', '.md', '.d.ts', 'package.json', 'package-lock.json']; for (const pkg of includePackages) { if (module.includes(pkg)) return false; } diff --git a/src/livecodes/languages/clojurescript/lang-clojurescript.ts b/src/livecodes/languages/clojurescript/lang-clojurescript.ts index 60199edeee..6b438e8c5e 100644 --- a/src/livecodes/languages/clojurescript/lang-clojurescript.ts +++ b/src/livecodes/languages/clojurescript/lang-clojurescript.ts @@ -23,9 +23,9 @@ export const clojurescript: LanguageSpecs = { imports: { 'cherry-cljs': cherryCljsBaseUrl + 'index.js', 'cherry-cljs/cljs.core.js': cherryCljsBaseUrl + 'cljs.core.js', - 'cherry-cljs/lib/clojure.string.js': 'lib/clojure.string.js', - 'cherry-cljs/lib/clojure.set.js': 'lib/clojure.set.js', - 'cherry-cljs/lib/clojure.walk.js': 'lib/clojure.walk.js', + 'cherry-cljs/lib/clojure.string.js': cherryCljsBaseUrl + 'lib/clojure.string.js', + 'cherry-cljs/lib/clojure.set.js': cherryCljsBaseUrl + 'lib/clojure.set.js', + 'cherry-cljs/lib/clojure.walk.js': cherryCljsBaseUrl + 'lib/clojure.walk.js', 'squint-cljs': squintCljsBaseUrl + 'index.js', 'squint-cljs/core.js': squintCljsBaseUrl + 'core.js', 'squint-cljs/string.js': squintCljsBaseUrl + 'string.js', From ef0f8586fcaee4b16e40f03c130f366c716ba928 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 13 Jul 2025 22:59:06 +0300 Subject: [PATCH 09/94] retry downloading failed modules --- scripts/download-modules.js | 60 ++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 866b3bdc16..437f5b9e99 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -3,6 +3,8 @@ const path = require('path'); const sdkPkg = require('../src/sdk/package.sdk.json'); const downloadModules = async ({ dryRun = false } = {}) => { + console.log(`Downloading modules...`); + const vendorsModule = 'src/livecodes/vendors.ts'; const tempDir = '.cache/'; const modulesDir = tempDir + '/modules/'; @@ -75,25 +77,43 @@ const downloadModules = async ({ dryRun = false } = {}) => { } // download modules - for (const { module, url } of moduleUrls) { - const fullPath = - modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); - const dirPath = path.dirname(fullPath); - if (fs.existsSync(fullPath)) continue; - - let text = ''; - if (dryRun) { - text = url; - } else { - const res = await fetch(url); - if (!res.ok) { - console.warn(`Failed to fetch ${module}: ${res.statusText}`); - continue; + const download = async (/** @type {Array<{module: string; url: string}>} */ moduleUrls) => { + /** @type {Array<{module: string; url: string; error: string}>} */ + const failedModuleUrls = []; + + for (const { module, url } of moduleUrls) { + const fullPath = + modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); + const dirPath = path.dirname(fullPath); + if (fs.existsSync(fullPath)) continue; + + let text = ''; + if (dryRun) { + text = url; + } else { + const res = await fetch(url); + if (!res.ok) { + failedModuleUrls.push({ module, url, error: res.statusText }); + continue; + } + text = await res.text(); + } + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(fullPath, text); + } + + return failedModuleUrls; + }; + + let failedModuleUrls = await download(moduleUrls); + if (failedModuleUrls.length) { + // retry + failedModuleUrls = await download(failedModuleUrls); + if (failedModuleUrls.length) { + for (const { module, error } of failedModuleUrls) { + console.error(`Failed to download module (${module}): ${error}`); } - text = await res.text(); } - fs.mkdirSync(dirPath, { recursive: true }); - fs.writeFileSync(fullPath, text); } // copy to build directory @@ -103,6 +123,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { // cleanup fs.rmSync(tempDir + 'vendors.js'); + // log + console.log(`Downloaded ${moduleUrls.length - failedModuleUrls.length} modules.`); + if (failedModuleUrls.length) { + console.log(`Failed to download ${failedModuleUrls.length} modules.`); + } + // utils /** * @param {string} module From 94f3fad75d5973a920d2b66b346e4d0cc641a629 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Mon, 14 Jul 2025 13:05:29 +0300 Subject: [PATCH 10/94] build(self-hosting): download modules --- package-lock.json | 1120 +++++++++++++++++++++++++++++++---- package.json | 29 +- scripts/download-modules.js | 105 +++- src/livecodes/vendors.ts | 3 +- 4 files changed, 1139 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74f1dc6bee..52fe28cb88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,8 @@ "comlink": "4.4.1", "conventional-changelog": "3.1.25", "cross-env": "7.0.3", + "decompress": "4.2.1", + "decompress-tarbz2": "4.1.1", "dts-bundle": "0.7.3", "esbuild": "0.20.2", "esbuild-plugin-minify-html": "0.1.2", @@ -5360,6 +5362,27 @@ "node": ">=0.10.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -5403,6 +5426,17 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5494,6 +5528,66 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -5800,16 +5894,47 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -7146,6 +7271,196 @@ "node": ">=0.10" } }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -7484,6 +7799,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -7534,6 +7864,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -7616,13 +7956,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -7643,10 +7981,11 @@ "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -8929,6 +9268,16 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -8956,6 +9305,16 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -9158,12 +9517,19 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/for-in": { @@ -9328,6 +9694,13 @@ ], "peer": true }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -9415,16 +9788,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -9472,6 +9851,20 @@ "node": ">=10" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stdin": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", @@ -9808,12 +10201,13 @@ "dev": true }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9918,10 +10312,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -10186,6 +10581,27 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -10571,6 +10987,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -10737,12 +11160,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -13809,6 +14233,16 @@ "node": ">= 12" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -15550,6 +15984,13 @@ "through": "~2.3" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -17017,6 +17458,20 @@ "node": ">=v12.22.7" } }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, "node_modules/selenium-webdriver": { "version": "4.0.0-rc-1", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz", @@ -17924,6 +18379,16 @@ "node": ">=4" } }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -18605,6 +19070,25 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/teeny-request": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz", @@ -18692,6 +19176,49 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -19009,14 +19536,15 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -19201,6 +19729,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -19604,15 +20143,18 @@ "peer": true }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -19792,6 +20334,17 @@ "node": ">=10" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yjs": { "version": "13.5.40", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.40.tgz", @@ -23760,6 +24313,12 @@ } } }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -23797,6 +24356,16 @@ "file-uri-to-path": "1.0.0" } }, + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -23861,6 +24430,44 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -24092,16 +24699,35 @@ } }, "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "requires": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "callsites": { @@ -25128,6 +25754,148 @@ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, + "decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, "dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -25382,6 +26150,17 @@ } } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -25423,6 +26202,15 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -25493,13 +26281,10 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true }, "es-errors": { "version": "1.3.0", @@ -25514,9 +26299,9 @@ "dev": true }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "requires": { "es-errors": "^1.3.0" @@ -26485,6 +27270,15 @@ "bser": "2.1.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -26503,6 +27297,12 @@ "flat-cache": "^3.0.4" } }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -26662,12 +27462,12 @@ "dev": true }, "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "requires": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" } }, "for-in": { @@ -26780,6 +27580,12 @@ "dev": true, "peer": true }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -26839,16 +27645,21 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { @@ -26880,6 +27691,16 @@ } } }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stdin": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", @@ -27125,13 +27946,10 @@ "dev": true }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true }, "graceful-fs": { "version": "4.2.11", @@ -27200,9 +28018,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true }, "has-tostringtag": { @@ -27396,6 +28214,12 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==" }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, "ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -27673,6 +28497,12 @@ "is-extglob": "^2.1.1" } }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true + }, "is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -27779,12 +28609,12 @@ } }, "is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "requires": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" } }, "is-typedarray": { @@ -30131,6 +30961,12 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, "mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -31435,6 +32271,12 @@ "through": "~2.3" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -32480,6 +33322,15 @@ "xmlchars": "^2.2.0" } }, + "seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "requires": { + "commander": "^2.8.1" + } + }, "selenium-webdriver": { "version": "4.0.0-rc-1", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz", @@ -33217,6 +34068,15 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -33736,6 +34596,21 @@ } } }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, "teeny-request": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz", @@ -33810,6 +34685,31 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "requires": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -34021,14 +34921,14 @@ "dev": true }, "typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" } }, "typed-array-byte-length": { @@ -34158,6 +35058,16 @@ "which-boxed-primitive": "^1.0.2" } }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -34477,15 +35387,17 @@ "peer": true }, "which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "requires": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, @@ -34608,6 +35520,16 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yjs": { "version": "13.5.40", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.40.tgz", diff --git a/package.json b/package.json index 588f6ffa5a..4b6c3ab1a1 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,8 @@ "comlink": "4.4.1", "conventional-changelog": "3.1.25", "cross-env": "7.0.3", + "decompress": "4.2.1", + "decompress-tarbz2": "4.1.1", "dts-bundle": "0.7.3", "esbuild": "0.20.2", "esbuild-plugin-minify-html": "0.1.2", @@ -158,15 +160,32 @@ "singleQuote": true, "trailingComma": "all", "printWidth": 100, - "plugins": ["prettier-plugin-organize-imports"] + "plugins": [ + "prettier-plugin-organize-imports" + ] }, "jest": { "preset": "ts-jest", "testEnvironment": "jsdom", - "setupFiles": ["/.jest/setup.ts"], - "testPathIgnorePatterns": ["/node_modules/", "/build/", "/src/modules/"], - "collectCoverageFrom": ["src/**/*.ts", "!**/build/**", "!**/vendor/**", "!src/modules/**"], - "coverageReporters": ["json", "html", "lcov"], + "setupFiles": [ + "/.jest/setup.ts" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/build/", + "/src/modules/" + ], + "collectCoverageFrom": [ + "src/**/*.ts", + "!**/build/**", + "!**/vendor/**", + "!src/modules/**" + ], + "coverageReporters": [ + "json", + "html", + "lcov" + ], "resolveJsonModule": true } } diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 437f5b9e99..797c9c2b7e 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -1,5 +1,8 @@ +const decompress = require('decompress'); +const decompressTarbz = require('decompress-tarbz2'); const fs = require('fs'); const path = require('path'); +const stream = require('stream'); const sdkPkg = require('../src/sdk/package.sdk.json'); const downloadModules = async ({ dryRun = false } = {}) => { @@ -16,6 +19,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { const baseUrls = []; /** @type {Array<{module: string; url: string}>} */ const moduleUrls = []; + let pyodideBaseUrl = ''; fs.mkdirSync(modulesDir, { recursive: true }); @@ -39,21 +43,40 @@ const downloadModules = async ({ dryRun = false } = {}) => { } // get modules from baseUrls - for (const baseUrl of baseUrls) { - // https://unpkg.com/@seth0x41/doppio@1.0.0/ - if (baseUrl.startsWith('https://unpkg.com/')) { - baseUrl.replace('https://unpkg.com/', ''); + for (let baseUrl of baseUrls) { + if (baseUrl.includes('@seth0x41/doppio')) { + baseUrl = baseUrl.replace('https://unpkg.com/', '').replace('unpkg:', ''); + } + if (baseUrl.includes('pyodide')) { + pyodideBaseUrl = baseUrl; + } + if (baseUrl.startsWith('https://')) { + continue; } - if (baseUrl.startsWith('https://')) continue; const mod = getModuleName(baseUrl); const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; const modInfo = await fetch(modInfoUrl).then((res) => res.json()); const files = modInfo.files; - if (!Array.isArray(files)) continue; - for (const file of files) { - if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { - modules.push(mod + file.name); + if (Array.isArray(files)) { + for (const file of files) { + if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { + modules.push(mod + file.name); + } + } + } else if (type === 'gh') { + // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB. + const [repo, version] = mod.split('@'); + const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; + const repoInfo = await fetch(filesUrl).then((res) => res.json()); + const files = repoInfo.tree; + if (Array.isArray(files)) { + const basePath = baseUrl.split(mod + '/')[1]; + for (const file of files) { + if (file.path.includes(basePath) && !shouldExclude(mod + '/' + file.path)) { + modules.push('gh:' + mod + '/' + file.path); + } + } } } } @@ -71,8 +94,6 @@ const downloadModules = async ({ dryRun = false } = {}) => { // use unpkg - no restriction on file types (e.g. jar) moduleUrls.push({ module, url: `https://unpkg.com/${module}` }); } - // TODO: handle modules hosted elsewhere: - // - https://cdn.jsdelivr.net/pyodide/v0.25.1/full/ -> https://pyodide.org/en/stable/usage/downloading-and-deploying.html#github-releases // TODO: handle font absolute urls in css (in font CDNs) } @@ -116,6 +137,36 @@ const downloadModules = async ({ dryRun = false } = {}) => { } } + // download Pyodide + if (pyodideBaseUrl) { + const pyodideVersion = pyodideBaseUrl.split('/v')[1].split('/')[0] || '0.28.0'; + const pyodideFiles = [ + `pyodide-${pyodideVersion}.tar.bz2`, + `pyodide-core-${pyodideVersion}.tar.bz2`, + `static-libraries-${pyodideVersion}.tar.bz2`, + `xbuildenv-${pyodideVersion}.tar.bz2`, + ]; + fs.mkdirSync(`${tempDir}pyodide/v${pyodideVersion}`, { recursive: true }); + await Promise.all( + pyodideFiles.map((file) => + (async () => { + const downloadPath = `${tempDir}pyodide/v${pyodideVersion}/${file}`; + const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; + if (!fs.existsSync(downloadPath)) { + await fetchAndSaveFile(url, downloadPath); + } + await decompress(downloadPath, `${outputDir}pyodide/v${pyodideVersion}/full`, { + plugins: [decompressTarbz()], + map: (file) => { + file.path = file.path.split('/').slice(1).join('/'); + return file; + }, + }); + })(), + ), + ); + } + // copy to build directory fs.mkdirSync(outputDir, { recursive: true }); fs.promises.cp(modulesDir, outputDir, { recursive: true }); @@ -124,7 +175,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.rmSync(tempDir + 'vendors.js'); // log - console.log(`Downloaded ${moduleUrls.length - failedModuleUrls.length} modules.`); + console.log(`Modules downloaded to: ${outputDir}`); if (failedModuleUrls.length) { console.log(`Failed to download ${failedModuleUrls.length} modules.`); } @@ -135,7 +186,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { */ function getModuleName(module) { if (module.startsWith('gh:')) { - return module.replace('gh:', ''); + return module.replace('gh:', '').split('/').slice(0, 2).join('/'); } const parts = module.split('/'); if (parts[0].startsWith('@')) { @@ -160,6 +211,34 @@ const downloadModules = async ({ dryRun = false } = {}) => { } return false; } + + /** + * @param {string | URL | Request} url + * @param {any} filePath + */ + async function fetchAndSaveFile(url, filePath) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + if (!response.body) { + throw new Error('Response body is empty.'); + } + const writer = fs.createWriteStream(filePath); + // @ts-ignore + const readableStream = stream.Readable.fromWeb(response.body); + readableStream.pipe(writer); + return /** @type {Promise} */ ( + new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }) + ); + } catch (error) { + console.error(`Error fetching or saving file: ${error.message}`); + } + } }; module.exports = { downloadModules }; diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index b664b3a77c..018491acfa 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -1,6 +1,7 @@ import { modulesService } from './services/modules'; // - only use `getUrl` or full URL (not `getModuleUrl`) +// - only use `gh:` or `unpkg: prefixes if required // - always add full version and file extension // - minimize usage of baseUrls if possible // - if es module imports others, use baseUrl instead @@ -116,7 +117,7 @@ export const ddietrCmThemesBaseUrl = /* @__PURE__ */ getUrl( '@ddietr/codemirror-themes@1.4.2/dist/theme/', ); -export const doppioJvmBaseUrl = 'https://unpkg.com/@seth0x41/doppio@1.0.0/'; +export const doppioJvmBaseUrl = /* @__PURE__ */ getUrl('unpkg:@seth0x41/doppio@1.0.0/'); export const dotUrl = /* @__PURE__ */ getUrl('dot@1.1.3/doT.js'); From 0f6c8fe6fe873be6ee12f372fd044ee550c39e17 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 16 Jul 2025 09:36:53 +0300 Subject: [PATCH 11/94] handle appCDN when using local modules --- .dockerignore | 1 - Dockerfile | 2 + docker-compose.yml | 2 + scripts/download-modules.js | 58 +++++++++++++++---- scripts/utils.js | 52 ++++++++++++++++- server/src/app.ts | 6 +- server/src/cache.ts | 13 +++++ src/_headers | 4 +- src/livecodes/compiler/compile.worker.ts | 5 +- src/livecodes/editor/codemirror/codemirror.ts | 5 +- .../editor/codemirror/editor-languages.ts | 4 +- src/livecodes/html/app.html | 5 +- src/livecodes/main.ts | 15 ++++- src/livecodes/services/modules.ts | 28 +++++++-- src/livecodes/vendors.ts | 4 +- 15 files changed, 171 insertions(+), 33 deletions(-) create mode 100644 server/src/cache.ts diff --git a/.dockerignore b/.dockerignore index 9a9e18fcdd..7b37e79d44 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,6 @@ docs/.docusaurus .jest build dist -.cache **/*.log .env docs/docs/api diff --git a/Dockerfile b/Dockerfile index 65402a492c..be4116e214 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ ARG SANDBOX_HOST_NAME ARG SANDBOX_PORT ARG FIREBASE_CONFIG ARG DOCS_BASE_URL +ARG LOCAL_MODULES RUN if [ "$DOCS_BASE_URL" == "null" ]; \ then npm run build:app; \ @@ -42,6 +43,7 @@ COPY server/package*.json ./ RUN npm ci +COPY --from=builder /app/.cache/ tmp/ COPY --from=builder /app/build/ build/ COPY functions/ functions/ diff --git a/docker-compose.yml b/docker-compose.yml index e8bb4ca09c..678950c2a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - SANDBOX_PORT=${SANDBOX_PORT:-8090} - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} + - LOCAL_MODULES=${LOCAL_MODULES:-true} restart: unless-stopped environment: - SELF_HOSTED=true @@ -28,6 +29,7 @@ services: - VALKEY_PORT=6379 volumes: - ./assets:/srv/build/assets + - ./.cache:/srv/.cache depends_on: - valkey diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 797c9c2b7e..43ac4f5379 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -17,6 +17,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { const modules = []; /** @type {string[]} */ const baseUrls = []; + /** @type {string[]} */ + const fontStylSheets = []; /** @type {Array<{module: string; url: string}>} */ const moduleUrls = []; let pyodideBaseUrl = ''; @@ -35,11 +37,17 @@ const downloadModules = async ({ dryRun = false } = {}) => { // modules vs baseUrls for (const [key, value] of Object.entries(vendorUrls)) { - if (key.includes('BaseUrl')) { + if (key.includes('BaseUrl') || key.includes('codeMirrorBasePath')) { baseUrls.push(value); } else { modules.push(value); } + if ( + value.includes('https://fonts.googleapis.com/') || + value.includes('https://fonts.cdnfonts.com/css') + ) { + fontStylSheets.push(value); + } } // get modules from baseUrls @@ -65,7 +73,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { } } } else if (type === 'gh') { - // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB. + // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). const [repo, version] = mod.split('@'); const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; const repoInfo = await fetch(filesUrl).then((res) => res.json()); @@ -73,7 +81,11 @@ const downloadModules = async ({ dryRun = false } = {}) => { if (Array.isArray(files)) { const basePath = baseUrl.split(mod + '/')[1]; for (const file of files) { - if (file.path.includes(basePath) && !shouldExclude(mod + '/' + file.path)) { + if ( + file.type === 'blob' && + file.path.includes(basePath) && + !shouldExclude(mod + '/' + file.path) + ) { modules.push('gh:' + mod + '/' + file.path); } } @@ -111,18 +123,38 @@ const downloadModules = async ({ dryRun = false } = {}) => { let text = ''; if (dryRun) { text = url; + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(fullPath, text); } else { - const res = await fetch(url); - if (!res.ok) { - failedModuleUrls.push({ module, url, error: res.statusText }); + const result = await fetchAndSaveFile(url, fullPath); + if (result instanceof Error) { + failedModuleUrls.push({ module, url, error: result.message }); continue; } - text = await res.text(); + const urlPattern = /https:\/\/[^'"\)]*/g; + + if (fullPath.includes('fonts.googleapis.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll('https://fonts.gstatic.com/', '../fonts.gstatic.com/'); + fs.writeFileSync(fullPath, patched); + } + if (fullPath.includes('fonts.cdnfonts.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll('https://fonts.cdnfonts.com/', '../'); + fs.writeFileSync(fullPath, patched); + } } - fs.mkdirSync(dirPath, { recursive: true }); - fs.writeFileSync(fullPath, text); } - return failedModuleUrls; }; @@ -169,7 +201,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { // copy to build directory fs.mkdirSync(outputDir, { recursive: true }); - fs.promises.cp(modulesDir, outputDir, { recursive: true }); + await fs.promises.cp(modulesDir, outputDir, { recursive: true }); // cleanup fs.rmSync(tempDir + 'vendors.js'); @@ -225,6 +257,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { if (!response.body) { throw new Error('Response body is empty.'); } + fs.mkdirSync(path.dirname(filePath), { recursive: true }); const writer = fs.createWriteStream(filePath); // @ts-ignore const readableStream = stream.Readable.fromWeb(response.body); @@ -236,7 +269,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { }) ); } catch (error) { - console.error(`Error fetching or saving file: ${error.message}`); + console.error(`Error downloading file (${url}): ${error.message}`); + return error; } } }; diff --git a/scripts/utils.js b/scripts/utils.js index 52fa646cf8..e58f0b1457 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -72,6 +72,7 @@ const getVars = (/** @type {boolean} */ devMode) => { const selfHostedSandboxHostName = process.env.SANDBOX_HOST_NAME || 'localhost'; const selfHostedSandboxPort = Number(process.env.SANDBOX_PORT) || 8090; const firebaseConfig = process.env.FIREBASE_CONFIG || 'null'; + const localModules = String(process.env.LOCAL_MODULES) === 'true'; return { appVersion, sdkVersion, @@ -86,6 +87,7 @@ const getVars = (/** @type {boolean} */ devMode) => { firebaseConfig, selfHostedSandboxHostName, selfHostedSandboxPort, + localModules, }; }; @@ -104,6 +106,7 @@ const getEnvVars = (/** @type {boolean} */ devMode) => { firebaseConfig, selfHostedSandboxHostName, selfHostedSandboxPort, + localModules, } = getVars(devMode); return { 'process.env.VERSION': `"${appVersion || ''}"`, @@ -119,8 +122,55 @@ const getEnvVars = (/** @type {boolean} */ devMode) => { 'process.env.SANDBOX_HOST_NAME': `"${selfHostedSandboxHostName}"`, 'process.env.SANDBOX_PORT': `"${selfHostedSandboxPort}"`, 'process.env.FIREBASE_CONFIG': `"${firebaseConfig}"`, + 'process.env.LOCAL_MODULES': `"${localModules}"`, define: 'undefined', // prevent using AMD (e.g. in lz-string), }; }; -module.exports = { arrToObj, mkdir, uint8arrayToString, iife, getFileNames, getEnvVars }; +const createAsyncQueue = (concurrency = 1) => { + /** @typedef {(() => Promise | void) | Promise} Task */ + + /** @type {Task[]} */ + const queue = []; + let running = 0; + + const add = (/** @type {Task} */ task) => { + queue.push(task); + processQueue(); + }; + + const processQueue = async () => { + if (running >= concurrency || queue.length === 0) { + return; + } + + running++; + const task = queue.shift(); + + try { + if (typeof task === 'function') { + await task(); + } else if (typeof task === 'object' && 'then' in task) { + await task; + } + } catch (error) { + console.error('Task failed:', error); + } finally { + running--; + processQueue(); + } + }; + return { + add, + }; +}; + +module.exports = { + arrToObj, + mkdir, + uint8arrayToString, + iife, + getFileNames, + getEnvVars, + createAsyncQueue, +}; diff --git a/server/src/app.ts b/server/src/app.ts index ba6c6b8f94..2aae023a8f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,6 +5,7 @@ import path from 'node:path'; import { onRequest as index } from '../../functions/index.ts'; import { onRequest as oembed } from '../../functions/oembed.ts'; import { broadcast } from './broadcast/index.ts'; +import { saveCache } from './cache.ts'; import { corsProxy } from './cors.ts'; import { sandbox } from './sandbox.ts'; import { share } from './share.ts'; @@ -40,7 +41,7 @@ app.use( setHeaders(res) { // match headers in: src/_headers const reqPath = res.req.path; - if (reqPath.startsWith('/assets/')) { + if (reqPath.startsWith('/modules/')) { res.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000, immutable'); } if (reqPath.startsWith('/livecodes/')) { @@ -79,3 +80,6 @@ if (process.env.SELF_HOSTED_BROADCAST === 'true') { userTokens: process.env.BROADCAST_TOKENS || '', }); } + +// save local modules cache to host +saveCache(); diff --git a/server/src/cache.ts b/server/src/cache.ts new file mode 100644 index 0000000000..fabc3aab18 --- /dev/null +++ b/server/src/cache.ts @@ -0,0 +1,13 @@ +import fs from 'fs'; +import path from 'path'; +import { dirname } from './utils.ts'; + +export const saveCache = async () => { + const srcDir = path.resolve(dirname, '../../tmp/'); + const dstDir = path.resolve(dirname, '../../.cache/'); + + if (fs.existsSync(srcDir)) { + fs.mkdirSync(dstDir, { recursive: true }); + await fs.promises.cp(srcDir, dstDir, { recursive: true }); + } +}; diff --git a/src/_headers b/src/_headers index d466dc3a5d..48b82f66a0 100644 --- a/src/_headers +++ b/src/_headers @@ -1,7 +1,7 @@ -/assets/* +/livecodes/* cache-control: public, max-age=31536000, s-maxage=31536000, immutable -/livecodes/* +/modules/* cache-control: public, max-age=31536000, s-maxage=31536000, immutable /livecodes/:file.map diff --git a/src/livecodes/compiler/compile.worker.ts b/src/livecodes/compiler/compile.worker.ts index 6dc87c0e64..3686bc5a48 100644 --- a/src/livecodes/compiler/compile.worker.ts +++ b/src/livecodes/compiler/compile.worker.ts @@ -2,8 +2,9 @@ import type TS from 'typescript'; import { getCompilerOptions } from '../editor/ts-compiler-options'; import { languages, processors } from '../languages'; import type { CompileOptions, Compilers, Config, EditorLibrary, Language } from '../models'; +import { getAppCDN, modulesService } from '../services'; import { doOnce, objectFilter } from '../utils/utils'; -import { codeMirrorBaseUrl, comlinkBaseUrl, vendorsBaseUrl } from '../vendors'; +import { codeMirrorBasePath, comlinkBaseUrl, vendorsBaseUrl } from '../vendors'; import { getAllCompilers } from './get-all-compilers'; import type { CompilerMessage, CompilerMessageEvent, LanguageOrProcessor } from './models'; declare const importScripts: (...args: string[]) => void; @@ -266,7 +267,7 @@ const initCodemirrorTS = doOnce(async () => { await loadTypeScript(); importScripts(comlinkBaseUrl + 'umd/comlink.js'); importScripts(typescriptVfsUrl); - importScripts(codeMirrorBaseUrl + 'codemirror-ts.worker.js'); + importScripts(modulesService.getUrl(codeMirrorBasePath, getAppCDN()) + 'codemirror-ts.worker.js'); const { createWorker } = worker.CodemirrorTsWorker; const { createDefaultMapFromCDN, createSystem, createVirtualTypeScriptEnvironment } = worker.typescriptVFS; diff --git a/src/livecodes/editor/codemirror/codemirror.ts b/src/livecodes/editor/codemirror/codemirror.ts index 199efa8e0b..2a525f7721 100644 --- a/src/livecodes/editor/codemirror/codemirror.ts +++ b/src/livecodes/editor/codemirror/codemirror.ts @@ -41,12 +41,15 @@ import type { Language, Theme, } from '../../models'; +import { getAppCDN, modulesService } from '../../services'; import { ctrl, debounce, getRandomString } from '../../utils/utils'; -import { codeMirrorBaseUrl, comlinkBaseUrl } from '../../vendors'; +import { codeMirrorBasePath, comlinkBaseUrl } from '../../vendors'; import { getEditorTheme } from '../themes'; import { codemirrorThemes, customThemes } from './codemirror-themes'; import { editorLanguages } from './editor-languages'; +const codeMirrorBaseUrl = modulesService.getUrl(codeMirrorBasePath, getAppCDN()); + export type CodeiumEditor = Pick & { editorId: EditorOptions['editorId']; }; diff --git a/src/livecodes/editor/codemirror/editor-languages.ts b/src/livecodes/editor/codemirror/editor-languages.ts index 1b7e807037..6640fd453c 100644 --- a/src/livecodes/editor/codemirror/editor-languages.ts +++ b/src/livecodes/editor/codemirror/editor-languages.ts @@ -12,11 +12,13 @@ import { javascript } from '@codemirror/lang-javascript'; import { json } from '@codemirror/lang-json'; import type { Language } from '../../models'; -import { codeMirrorBaseUrl } from '../../vendors'; +import { getAppCDN, modulesService } from '../../services'; +import { codeMirrorBasePath } from '../../vendors'; const legacy = (parser: StreamParser) => new LanguageSupport(StreamLanguage.define(parser)); +const codeMirrorBaseUrl = modulesService.getUrl(codeMirrorBasePath, getAppCDN()); const getPath = (mod: string) => codeMirrorBaseUrl + mod; const moduleUrls = { diff --git a/src/livecodes/html/app.html b/src/livecodes/html/app.html index 42101bc7c9..54392c3c5f 100644 --- a/src/livecodes/html/app.html +++ b/src/livecodes/html/app.html @@ -363,10 +363,7 @@ - + {{polyfillScript}} diff --git a/src/livecodes/main.ts b/src/livecodes/main.ts index b65d9c8f4a..d99136b19a 100644 --- a/src/livecodes/main.ts +++ b/src/livecodes/main.ts @@ -5,7 +5,7 @@ import appHTML from './html/app.html?raw'; import type { API, CDN, Config, CustomEvents, EmbedOptions } from './models'; import { modulesService } from './services/modules'; import { isInIframe } from './utils/utils'; -import { codeMirrorBaseUrl, esModuleShimsPath } from './vendors'; +import { codeMirrorBasePath, esModuleShimsPath } from './vendors'; export type { API, Config }; @@ -77,7 +77,7 @@ export const livecodes = (container: string, config: Partial = {}): Prom const loadApp = async () => { const appCDN = await modulesService.checkCDNs(esModuleShimsPath, params.get('appCDN') as CDN); - + const codeMirrorBaseUrl = modulesService.getUrl(codeMirrorBasePath, appCDN as CDN); const supportsImportMaps = HTMLScriptElement.supports ? HTMLScriptElement.supports('importmap') : false; @@ -104,6 +104,17 @@ export const livecodes = (container: string, config: Partial = {}): Prom import * as mod from '${baseUrl}{{hash:codemirror.js}}'; window['${baseUrl}{{hash:codemirror.js}}'] = mod; + `, + ) + .replace( + /{{polyfillScript}}/g, + process.env.LOCAL_MODULES === 'true' + ? '' + : ` + `, ) .replace(/{{codemirrorCoreUrl}}/g, `${codeMirrorBaseUrl}codemirror-core.js`) diff --git a/src/livecodes/services/modules.ts b/src/livecodes/services/modules.ts index ffd853bf95..912aae05af 100644 --- a/src/livecodes/services/modules.ts +++ b/src/livecodes/services/modules.ts @@ -1,6 +1,7 @@ import type { CDN } from '../models'; declare const globalThis: { appCDN: CDN }; +const localModules = process.env.LOCAL_MODULES === 'true'; const moduleCDNs: CDN[] = [ 'esm.sh', @@ -60,14 +61,16 @@ export const modulesService = { }, getUrl: (path: string, cdn?: CDN) => - path.startsWith('http') || path.startsWith('data:') - ? path - : getCdnUrl(path, false, cdn || getAppCDN()) || path, + path.startsWith('data:') ? path : getCdnUrl(path, false, cdn || getAppCDN()) || path, cdnLists: { npm: npmCDNs, module: moduleCDNs, gh: ghCDNs }, checkCDNs: async (testModule: string, preferredCDN?: CDN) => { - const cdns: CDN[] = [preferredCDN, ...modulesService.cdnLists.npm].filter(Boolean) as CDN[]; + const modulesBaseUrl = new URL('./modules/', location.href).href as CDN; + const localCDN = localModules ? modulesBaseUrl : undefined; + const cdns: CDN[] = [preferredCDN, localCDN, ...modulesService.cdnLists.npm].filter( + (x) => x != null, + ); for (const cdn of cdns) { try { const res = await fetch(modulesService.getUrl(testModule, cdn), { @@ -94,6 +97,10 @@ export const getAppCDN = (): CDN => { }; const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { + if (localModules && !isModule) { + return getLocalUrl(modName, defaultCDN); + } + if (modName.startsWith('http') || modName.startsWith('data:')) return modName; const post = isModule && modName.startsWith('unpkg:') ? '?module' : ''; if (modName.startsWith('gh:')) { modName = modName.replace('gh', ghCDNs[0]); @@ -110,6 +117,19 @@ const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { return null; }; +const getLocalUrl = (modName: string, modulesBaseUrl = '/modules/') => { + modName = modName + .replace('https://unpkg.com/', '') + .replace('unpkg:', '') + .replaceAll('https://', '') + .replaceAll(':', '_') + .replaceAll('?', '_'); + if (modName.includes('pyodide')) { + modName = modName.replace('cdn.jsdelivr.net/', ''); + } + return `${modulesBaseUrl}${modName}`; +}; + // based on https://github.com/neoascetic/rawgithack/blob/master/web/rawgithack.js const TEMPLATES: Array<[RegExp, string]> = [ [/^(esm\.sh:)(.+)/i, 'https://esm.sh/$2'], diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 018491acfa..b0ae9902bb 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -1,6 +1,6 @@ import { modulesService } from './services/modules'; -// - only use `getUrl` or full URL (not `getModuleUrl`) +// - only use `getUrl` (not `getModuleUrl` or plain URLs) - except `es-module-shims` and `codeMirrorBasePath` // - only use `gh:` or `unpkg: prefixes if required // - always add full version and file extension // - minimize usage of baseUrls if possible @@ -93,7 +93,7 @@ export const codeiumProviderUrl = /* @__PURE__ */ getUrl( '@live-codes/monaco-codeium-provider@0.2.2/dist/index.js', ); -export const codeMirrorBaseUrl = /* @__PURE__ */ getUrl('@live-codes/codemirror@0.3.2/build/'); +export const codeMirrorBasePath = '@live-codes/codemirror@0.3.2/build/'; export const coffeeScriptUrl = /* @__PURE__ */ getUrl( 'coffeescript@2.7.0/lib/coffeescript-browser-compiler-legacy/coffeescript.js', From 8a64e49bb0dc9341464ba1c403bd4aad49fd2e4c Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 16 Jul 2025 10:49:55 +0300 Subject: [PATCH 12/94] handle module cache by docker --- .dockerignore | 1 + Dockerfile | 13 +++++++--- docker-compose.yml | 1 - package.json | 1 + scripts/build.js | 5 ---- scripts/download-modules.js | 48 ++++++++++++++++++------------------- server/src/app.ts | 4 ---- server/src/cache.ts | 13 ---------- 8 files changed, 35 insertions(+), 51 deletions(-) delete mode 100644 server/src/cache.ts diff --git a/.dockerignore b/.dockerignore index 7b37e79d44..9a9e18fcdd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ docs/.docusaurus .jest build dist +.cache **/*.log .env docs/docs/api diff --git a/Dockerfile b/Dockerfile index be4116e214..64156be368 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,6 @@ COPY server/package*.json server/ RUN npm ci -COPY . . - ARG SELF_HOSTED ARG SELF_HOSTED_SHARE ARG SELF_HOSTED_BROADCAST @@ -23,6 +21,16 @@ ARG FIREBASE_CONFIG ARG DOCS_BASE_URL ARG LOCAL_MODULES +COPY scripts/download-modules.js scripts/ +COPY src/livecodes/vendors.ts src/livecodes/ +COPY src/sdk/package.sdk.json src/sdk/ + +RUN if [ "$LOCAL_MODULES" == "true" ]; \ + then npm run download-modules; \ + fi + +COPY . . + RUN if [ "$DOCS_BASE_URL" == "null" ]; \ then npm run build:app; \ else npm run build; \ @@ -43,7 +51,6 @@ COPY server/package*.json ./ RUN npm ci -COPY --from=builder /app/.cache/ tmp/ COPY --from=builder /app/build/ build/ COPY functions/ functions/ diff --git a/docker-compose.yml b/docker-compose.yml index 678950c2a2..0d581d8c54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,6 @@ services: - VALKEY_PORT=6379 volumes: - ./assets:/srv/build/assets - - ./.cache:/srv/.cache depends_on: - valkey diff --git a/package.json b/package.json index 4b6c3ab1a1..2ff24d11fb 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "build:docs": "cd docs && npm run build", "build:storybook": "cd storybook && npm run build", "copy:assets": "recursive-delete build/livecodes/assets && mkdirp build/livecodes/assets && recursive-copy src/livecodes/assets build/livecodes/assets", + "copy:modules": "recursive-delete build/modules && mkdirp .cache/modules && mkdirp build/modules && recursive-copy .cache/modules build/modules", "download-modules": "node ./scripts/download-modules.js", "typedocs": "run-s typedocs:*", "typedocs:livecodes": "typedoc src/livecodes/main.ts src/livecodes/app.ts src/livecodes/embed.ts src/livecodes/_modules.ts --out build/typedocs/livecodes --exclude **/*.spec.ts --excludeExternals", diff --git a/scripts/build.js b/scripts/build.js index 222ea61e39..13058760d2 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -3,7 +3,6 @@ const { minify: minifyHTML, default: minifyHTMLPlugin } = require('esbuild-plugi const fs = require('fs'); const path = require('path'); -const { downloadModules } = require('./download-modules'); const { applyHash } = require('./hash'); const { injectCss } = require('./inject-css'); const { buildStyles } = require('./styles'); @@ -12,7 +11,6 @@ const { arrToObj, mkdir, uint8arrayToString, iife, getFileNames, getEnvVars } = const args = process.argv.slice(2); const devMode = args.includes('--dev'); -const localModules = args.includes('--download-modules') || process.env.LOCAL_MODULES === 'true'; const root = path.resolve(__dirname + '/..'); const outDir = path.resolve(root, 'build'); @@ -318,9 +316,6 @@ const functionsBuild = () => const stylesBuild = () => buildStyles(devMode); prepareDir().then(async () => { - if (localModules) { - downloadModules(); - } await buildLocalePathLoader(); Promise.all([ esmBuild(), diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 43ac4f5379..374c6702fb 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -5,13 +5,24 @@ const path = require('path'); const stream = require('stream'); const sdkPkg = require('../src/sdk/package.sdk.json'); +const cacheDir = '.cache/'; +const modulesDir = cacheDir + '/modules/'; +const srcVendorsModule = 'src/livecodes/vendors.ts'; +const cacheVendorsModule = cacheDir + 'vendors.js'; + +const transformVendorsModule = (/** @type {string} */ content) => + 'const modulesService = { getUrl: (mod) => mod };\n' + + content.replace('import', '// import').replace('process.env.SDK_VERSION', `"${sdkPkg.version}"`); + const downloadModules = async ({ dryRun = false } = {}) => { - console.log(`Downloading modules...`); + const srcVendorsContent = fs.readFileSync(srcVendorsModule, 'utf-8'); + const cacheVendorsContent = fs.existsSync(cacheVendorsModule) + ? fs.readFileSync(cacheVendorsModule, 'utf-8') + : ''; + + if (srcVendorsContent === cacheVendorsContent) return; - const vendorsModule = 'src/livecodes/vendors.ts'; - const tempDir = '.cache/'; - const modulesDir = tempDir + '/modules/'; - const outputDir = 'build/modules/'; + console.log(`Downloading modules...`); /** @type {string[]} */ const modules = []; @@ -25,15 +36,9 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.mkdirSync(modulesDir, { recursive: true }); - const verdorModulesContent = - 'const modulesService = { getUrl: (mod) => mod };\n' + - fs - .readFileSync(vendorsModule, 'utf8') - .replace('import', '// import') - .replace('process.env.SDK_VERSION', `"${sdkPkg.version}"`); - fs.writeFileSync(tempDir + 'vendors.js', verdorModulesContent, 'utf8'); - - const vendorUrls = require('../' + tempDir + 'vendors.js'); + const verdorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); + fs.writeFileSync(cacheVendorsModule, verdorModulesContent, 'utf8'); + const vendorUrls = require('../' + cacheVendorsModule); // modules vs baseUrls for (const [key, value] of Object.entries(vendorUrls)) { @@ -178,16 +183,16 @@ const downloadModules = async ({ dryRun = false } = {}) => { `static-libraries-${pyodideVersion}.tar.bz2`, `xbuildenv-${pyodideVersion}.tar.bz2`, ]; - fs.mkdirSync(`${tempDir}pyodide/v${pyodideVersion}`, { recursive: true }); + fs.mkdirSync(`${cacheDir}pyodide/v${pyodideVersion}`, { recursive: true }); await Promise.all( pyodideFiles.map((file) => (async () => { - const downloadPath = `${tempDir}pyodide/v${pyodideVersion}/${file}`; + const downloadPath = `${cacheDir}pyodide/v${pyodideVersion}/${file}`; const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; if (!fs.existsSync(downloadPath)) { await fetchAndSaveFile(url, downloadPath); } - await decompress(downloadPath, `${outputDir}pyodide/v${pyodideVersion}/full`, { + await decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { plugins: [decompressTarbz()], map: (file) => { file.path = file.path.split('/').slice(1).join('/'); @@ -199,15 +204,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { ); } - // copy to build directory - fs.mkdirSync(outputDir, { recursive: true }); - await fs.promises.cp(modulesDir, outputDir, { recursive: true }); - - // cleanup - fs.rmSync(tempDir + 'vendors.js'); - // log - console.log(`Modules downloaded to: ${outputDir}`); + console.log(`Modules downloaded to: ${modulesDir}`); if (failedModuleUrls.length) { console.log(`Failed to download ${failedModuleUrls.length} modules.`); } diff --git a/server/src/app.ts b/server/src/app.ts index 2aae023a8f..f4ac9eab5b 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,7 +5,6 @@ import path from 'node:path'; import { onRequest as index } from '../../functions/index.ts'; import { onRequest as oembed } from '../../functions/oembed.ts'; import { broadcast } from './broadcast/index.ts'; -import { saveCache } from './cache.ts'; import { corsProxy } from './cors.ts'; import { sandbox } from './sandbox.ts'; import { share } from './share.ts'; @@ -80,6 +79,3 @@ if (process.env.SELF_HOSTED_BROADCAST === 'true') { userTokens: process.env.BROADCAST_TOKENS || '', }); } - -// save local modules cache to host -saveCache(); diff --git a/server/src/cache.ts b/server/src/cache.ts deleted file mode 100644 index fabc3aab18..0000000000 --- a/server/src/cache.ts +++ /dev/null @@ -1,13 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { dirname } from './utils.ts'; - -export const saveCache = async () => { - const srcDir = path.resolve(dirname, '../../tmp/'); - const dstDir = path.resolve(dirname, '../../.cache/'); - - if (fs.existsSync(srcDir)) { - fs.mkdirSync(dstDir, { recursive: true }); - await fs.promises.cp(srcDir, dstDir, { recursive: true }); - } -}; From 026324598e47caf955a75b69b219d5e781aabf53 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 16 Jul 2025 11:46:16 +0300 Subject: [PATCH 13/94] use self-hosted URL in build --- Dockerfile | 2 ++ docker-compose.yml | 2 ++ scripts/build.js | 21 +++++++++++++++++++-- src/404.html | 3 +-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 64156be368..1205ca3cda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ COPY server/package*.json server/ RUN npm ci ARG SELF_HOSTED +ARG HOST_NAME +ARG PORT ARG SELF_HOSTED_SHARE ARG SELF_HOSTED_BROADCAST ARG BROADCAST_PORT diff --git a/docker-compose.yml b/docker-compose.yml index 0d581d8c54..3b95be9569 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: dockerfile: Dockerfile args: - SELF_HOSTED=true + - HOST_NAME=${HOST_NAME:-livecodes.localhost} + - PORT=${PORT:-443} - SELF_HOSTED_SHARE=${SELF_HOSTED_SHARE:-true} - SELF_HOSTED_BROADCAST=${SELF_HOSTED_BROADCAST:-true} - BROADCAST_PORT=${BROADCAST_PORT:-3030} diff --git a/scripts/build.js b/scripts/build.js index 13058760d2..dd8d255ea0 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -36,7 +36,7 @@ const copyFile = async (filePath, outputName, replace) => { fs.writeFileSync(dist, minified, 'utf8'); }; -const addBaseUrl = (content) => { +const addBaseUrl = (/** @type {string} */ content) => { let baseUrl = process.env.BASE_URL; if (baseUrl && baseUrl !== '/') { if (!baseUrl.startsWith('/') && !baseUrl.startsWith('http')) { @@ -50,6 +50,14 @@ const addBaseUrl = (content) => { return content; }; +const useSelfHostedURL = (/** @type {string} */ content) => { + if (!process.env.HOST_NAME) return content; + const hostname = process.env.HOST_NAME; + const port = Number(process.env.PORT) || 443; + const appUrl = `https://${hostname}${port !== 443 ? ':' + port : ''}`; + return content.replaceAll('https://livecodes.io', `${appUrl}`); +}; + const prepareDir = async () => { mkdir(outDir); mkdir(outDir + '/livecodes/'); @@ -65,9 +73,18 @@ const prepareDir = async () => { copyFile('src/netlify.toml', 'netlify.toml'), copyFile('src/favicon.ico', 'favicon.ico'), copyFile('src/404.html', '404.html', addBaseUrl), - copyFile('src/index.html', 'index.html'), + copyFile('src/index.html', 'index.html', useSelfHostedURL), copyFile('src/livecodes/html/app-base.html', 'app.html'), ]); + await fs.promises + .readFile(path.resolve(root, 'src/livecodes/assets/site.webmanifest'), 'utf8') + .then((siteWebManifest) => + fs.promises.writeFile( + path.resolve(outDir, 'livecodes/assets/site.webmanifest'), + useSelfHostedURL(siteWebManifest), + 'utf8', + ), + ); }; /** @type {Partial} */ diff --git a/src/404.html b/src/404.html index 562664a1b0..0ec7ea8735 100644 --- a/src/404.html +++ b/src/404.html @@ -142,9 +142,8 @@ From 6ed47d53c49a98c72eb2d44323c254943ad8ac7d Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 16 Jul 2025 13:07:23 +0300 Subject: [PATCH 14/94] download modules in parallel --- scripts/download-modules.js | 371 +++++++++++++++++++++--------------- scripts/utils.js | 39 ---- 2 files changed, 219 insertions(+), 191 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 374c6702fb..7afa42dc1f 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -32,8 +32,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { const fontStylSheets = []; /** @type {Array<{module: string; url: string}>} */ const moduleUrls = []; + /** @type {Array<{module: string; url: string; error: string}>} */ + const failedModuleUrls = []; let pyodideBaseUrl = ''; + const downloadQueue = createAsyncQueue(10); + fs.mkdirSync(modulesDir, { recursive: true }); const verdorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); @@ -56,47 +60,48 @@ const downloadModules = async ({ dryRun = false } = {}) => { } // get modules from baseUrls - for (let baseUrl of baseUrls) { - if (baseUrl.includes('@seth0x41/doppio')) { - baseUrl = baseUrl.replace('https://unpkg.com/', '').replace('unpkg:', ''); - } - if (baseUrl.includes('pyodide')) { - pyodideBaseUrl = baseUrl; - } - if (baseUrl.startsWith('https://')) { - continue; - } - const mod = getModuleName(baseUrl); - const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; - const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; - const modInfo = await fetch(modInfoUrl).then((res) => res.json()); - const files = modInfo.files; - if (Array.isArray(files)) { - for (const file of files) { - if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { - modules.push(mod + file.name); - } + await Promise.all( + baseUrls.map(async (baseUrl) => { + if (baseUrl.includes('@seth0x41/doppio')) { + baseUrl = baseUrl.replace('https://unpkg.com/', '').replace('unpkg:', ''); + } + if (baseUrl.includes('pyodide')) { + pyodideBaseUrl = baseUrl; } - } else if (type === 'gh') { - // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). - const [repo, version] = mod.split('@'); - const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; - const repoInfo = await fetch(filesUrl).then((res) => res.json()); - const files = repoInfo.tree; + if (baseUrl.startsWith('https://')) return; + + const mod = getModuleName(baseUrl); + const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; + const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; + const modInfo = await fetch(modInfoUrl).then((res) => res.json()); + const files = modInfo.files; if (Array.isArray(files)) { - const basePath = baseUrl.split(mod + '/')[1]; for (const file of files) { - if ( - file.type === 'blob' && - file.path.includes(basePath) && - !shouldExclude(mod + '/' + file.path) - ) { - modules.push('gh:' + mod + '/' + file.path); + if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { + modules.push(mod + file.name); + } + } + } else if (type === 'gh') { + // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). + const [repo, version] = mod.split('@'); + const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; + const repoInfo = await fetch(filesUrl).then((res) => res.json()); + const files = repoInfo.tree; + if (Array.isArray(files)) { + const basePath = baseUrl.split(mod + '/')[1]; + for (const file of files) { + if ( + file.type === 'blob' && + file.path.includes(basePath) && + !shouldExclude(mod + '/' + file.path) + ) { + modules.push('gh:' + mod + '/' + file.path); + } } } } - } - } + }), + ); // get moduleUrls for (const module of modules) { @@ -111,14 +116,13 @@ const downloadModules = async ({ dryRun = false } = {}) => { // use unpkg - no restriction on file types (e.g. jar) moduleUrls.push({ module, url: `https://unpkg.com/${module}` }); } - // TODO: handle font absolute urls in css (in font CDNs) } // download modules - const download = async (/** @type {Array<{module: string; url: string}>} */ moduleUrls) => { - /** @type {Array<{module: string; url: string; error: string}>} */ - const failedModuleUrls = []; - + const download = ( + /** @type {Array<{module: string; url: string}>} */ moduleUrls, + isRetry = false, + ) => { for (const { module, url } of moduleUrls) { const fullPath = modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); @@ -131,48 +135,48 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.mkdirSync(dirPath, { recursive: true }); fs.writeFileSync(fullPath, text); } else { - const result = await fetchAndSaveFile(url, fullPath); - if (result instanceof Error) { - failedModuleUrls.push({ module, url, error: result.message }); - continue; - } - const urlPattern = /https:\/\/[^'"\)]*/g; + downloadQueue.add(() => + fetchAndSaveFile(url, fullPath).then(async (result) => { + if (result instanceof Error) { + if (isRetry) { + failedModuleUrls.push({ module, url, error: result.message }); + console.error(`Failed to download module (${module}): ${result.message}`); + return; + } + download([{ module, url }], true); + return; + } + const urlPattern = /https:\/\/[^'"\)]*/g; - if (fullPath.includes('fonts.googleapis.com/css')) { - const content = fs.readFileSync(fullPath, 'utf8'); - const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); - for (const fontUrl of fontUrls) { - const fontPath = fontUrl.replace('https://', modulesDir); - await fetchAndSaveFile(fontUrl, fontPath); - } - const patched = content.replaceAll('https://fonts.gstatic.com/', '../fonts.gstatic.com/'); - fs.writeFileSync(fullPath, patched); - } - if (fullPath.includes('fonts.cdnfonts.com/css')) { - const content = fs.readFileSync(fullPath, 'utf8'); - const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); - for (const fontUrl of fontUrls) { - const fontPath = fontUrl.replace('https://', modulesDir); - await fetchAndSaveFile(fontUrl, fontPath); - } - const patched = content.replaceAll('https://fonts.cdnfonts.com/', '../'); - fs.writeFileSync(fullPath, patched); - } + if (fullPath.includes('fonts.googleapis.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll( + 'https://fonts.gstatic.com/', + '../fonts.gstatic.com/', + ); + fs.writeFileSync(fullPath, patched); + } + if (fullPath.includes('fonts.cdnfonts.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll('https://fonts.cdnfonts.com/', '../'); + fs.writeFileSync(fullPath, patched); + } + }), + ); } } - return failedModuleUrls; }; - - let failedModuleUrls = await download(moduleUrls); - if (failedModuleUrls.length) { - // retry - failedModuleUrls = await download(failedModuleUrls); - if (failedModuleUrls.length) { - for (const { module, error } of failedModuleUrls) { - console.error(`Failed to download module (${module}): ${error}`); - } - } - } + download(moduleUrls); // download Pyodide if (pyodideBaseUrl) { @@ -184,93 +188,156 @@ const downloadModules = async ({ dryRun = false } = {}) => { `xbuildenv-${pyodideVersion}.tar.bz2`, ]; fs.mkdirSync(`${cacheDir}pyodide/v${pyodideVersion}`, { recursive: true }); - await Promise.all( - pyodideFiles.map((file) => - (async () => { - const downloadPath = `${cacheDir}pyodide/v${pyodideVersion}/${file}`; - const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; - if (!fs.existsSync(downloadPath)) { - await fetchAndSaveFile(url, downloadPath); - } - await decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { - plugins: [decompressTarbz()], - map: (file) => { - file.path = file.path.split('/').slice(1).join('/'); - return file; - }, - }); - })(), - ), - ); - } - - // log - console.log(`Modules downloaded to: ${modulesDir}`); - if (failedModuleUrls.length) { - console.log(`Failed to download ${failedModuleUrls.length} modules.`); + pyodideFiles.forEach((file) => { + const downloadPath = `${cacheDir}pyodide/v${pyodideVersion}/${file}`; + const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; + if (!fs.existsSync(downloadPath)) { + downloadQueue.add(() => + fetchAndSaveFile(url, downloadPath).then((result) => { + if (result instanceof Error) { + failedModuleUrls.push({ module: file, url, error: result.message }); + console.error(`Failed to download module (${module}): ${result.message}`); + return; + } + return decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { + plugins: [decompressTarbz()], + map: (file) => { + file.path = file.path.split('/').slice(1).join('/'); + return file; + }, + }); + }), + ); + } + }); } - // utils - /** - * @param {string} module - */ - function getModuleName(module) { - if (module.startsWith('gh:')) { - return module.replace('gh:', '').split('/').slice(0, 2).join('/'); + downloadQueue.onFinish(() => { + // log + console.log(`Modules downloaded to: ${modulesDir}`); + if (failedModuleUrls.length) { + console.log(`Failed to download ${failedModuleUrls.length} modules.`); } - const parts = module.split('/'); - if (parts[0].startsWith('@')) { - return parts[0] + '/' + parts[1]; - } else { - return parts[0]; + }); +}; + +// utils +/** + * @param {string} module + */ +function getModuleName(module) { + if (module.startsWith('gh:')) { + return module.replace('gh:', '').split('/').slice(0, 2).join('/'); + } + const parts = module.split('/'); + if (parts[0].startsWith('@')) { + return parts[0] + '/' + parts[1]; + } else { + return parts[0]; + } +} +/** + * @param {string} module + */ +function shouldExclude(module) { + const includePackages = ['@live-codes/browser-compilers']; + const excludeExtensions = ['.map', '.md', '.d.ts', 'package.json', 'package-lock.json']; + for (const pkg of includePackages) { + if (module.includes(pkg)) return false; + } + for (const extension of excludeExtensions) { + if (module.endsWith(extension)) { + return true; } } - /** - * @param {string} module - */ - function shouldExclude(module) { - const includePackages = ['@live-codes/browser-compilers']; - const excludeExtensions = ['.map', '.md', '.d.ts', 'package.json', 'package-lock.json']; - for (const pkg of includePackages) { - if (module.includes(pkg)) return false; + return false; +} + +/** + * @param {string | URL | Request} url + * @param {any} filePath + */ +async function fetchAndSaveFile(url, filePath) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - for (const extension of excludeExtensions) { - if (module.endsWith(extension)) { - return true; - } + if (!response.body) { + throw new Error('Response body is empty.'); } - return false; + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + const writer = fs.createWriteStream(filePath); + // @ts-ignore + const readableStream = stream.Readable.fromWeb(response.body); + readableStream.pipe(writer); + return /** @type {Promise} */ ( + new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }) + ); + } catch (error) { + console.error(`Error downloading file (${url}): ${error.message}`); + return error; } +} + +const createAsyncQueue = (concurrency = 1) => { + /** @typedef {(() => Promise | unknown) | Promise} Task */ + + /** @type {Task[]} */ + const queue = []; + /** @type {Array<() => unknown>} */ + const subscribers = []; + let running = 0; + + const add = (/** @type {Task} */ task) => { + queue.push(task); + processQueue(); + }; + + const processQueue = async () => { + if (running >= concurrency || queue.length === 0) { + await runSubscribers(); + return; + } + + running++; + const task = queue.shift(); - /** - * @param {string | URL | Request} url - * @param {any} filePath - */ - async function fetchAndSaveFile(url, filePath) { try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - if (!response.body) { - throw new Error('Response body is empty.'); + if (typeof task === 'function') { + await task(); + } else if (typeof task === 'object' && 'then' in task) { + await task; } - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - const writer = fs.createWriteStream(filePath); - // @ts-ignore - const readableStream = stream.Readable.fromWeb(response.body); - readableStream.pipe(writer); - return /** @type {Promise} */ ( - new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - }) - ); } catch (error) { - console.error(`Error downloading file (${url}): ${error.message}`); - return error; + console.error('Task failed:', error); + } finally { + running--; + processQueue(); } - } + }; + + const runSubscribers = async () => { + if (queue.length === 0 && running === 0 && subscribers.length > 0) { + const cb = subscribers.shift(); + if (typeof cb === 'function') { + await cb(); + } + await runSubscribers(); + } + }; + + const onFinish = (/** @type {() => unknown} */ cb) => { + subscribers.push(cb); + }; + + return { + add, + onFinish, + }; }; module.exports = { downloadModules }; diff --git a/scripts/utils.js b/scripts/utils.js index e58f0b1457..74091a6ec0 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -127,44 +127,6 @@ const getEnvVars = (/** @type {boolean} */ devMode) => { }; }; -const createAsyncQueue = (concurrency = 1) => { - /** @typedef {(() => Promise | void) | Promise} Task */ - - /** @type {Task[]} */ - const queue = []; - let running = 0; - - const add = (/** @type {Task} */ task) => { - queue.push(task); - processQueue(); - }; - - const processQueue = async () => { - if (running >= concurrency || queue.length === 0) { - return; - } - - running++; - const task = queue.shift(); - - try { - if (typeof task === 'function') { - await task(); - } else if (typeof task === 'object' && 'then' in task) { - await task; - } - } catch (error) { - console.error('Task failed:', error); - } finally { - running--; - processQueue(); - } - }; - return { - add, - }; -}; - module.exports = { arrToObj, mkdir, @@ -172,5 +134,4 @@ module.exports = { iife, getFileNames, getEnvVars, - createAsyncQueue, }; From 9bbf1006fa7226f6635bbeaa1343135209fea571 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 16 Jul 2025 13:08:52 +0300 Subject: [PATCH 15/94] fix --- src/livecodes/editor/monaco/monaco.ts | 4 ++-- src/livecodes/vendors.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/livecodes/editor/monaco/monaco.ts b/src/livecodes/editor/monaco/monaco.ts index 1442c170dc..7a6a080369 100644 --- a/src/livecodes/editor/monaco/monaco.ts +++ b/src/livecodes/editor/monaco/monaco.ts @@ -24,7 +24,7 @@ import { monacoBaseUrl, monacoEmacsUrl, monacoVimUrl, - monacoVolarUrl, + monacoVolarBaseUrl, vendorsBaseUrl, } from '../../vendors'; import { getEditorTheme } from '../themes'; @@ -265,7 +265,7 @@ export const createEditor = async (options: EditorOptions): Promise const addVueSupport = async () => { if (vueRegistered) return; vueRegistered = true; - const { registerVue, registerHighlighter } = await import(monacoVolarUrl); + const { registerVue, registerHighlighter } = await import(monacoVolarBaseUrl + 'index.js'); const tsCompilerOptions = { ...getCompilerOptions('vue'), jsx: 'preserve' }; await registerVue({ editor, monaco, tsCompilerOptions, silent: true }); shikiThemes = registerHighlighter(monaco); diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index b0ae9902bb..b92c2cd987 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -309,9 +309,7 @@ export const monacoThemesBaseUrl = /* @__PURE__ */ getUrl('monaco-themes@0.4.4/t export const monacoVimUrl = /* @__PURE__ */ getUrl('monaco-vim@0.4.1/dist/monaco-vim.js'); -export const monacoVolarUrl = /* @__PURE__ */ getUrl( - '@live-codes/monaco-volar@0.1.0/dist/index.js', -); +export const monacoVolarBaseUrl = /* @__PURE__ */ getUrl('@live-codes/monaco-volar@0.1.0/dist/'); export const mustacheUrl = /* @__PURE__ */ getUrl('mustache@4.2.0/mustache.js'); From e65f703b61862bc3d9f761fd401e98945d00a478 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 17 Jul 2025 01:38:49 +0300 Subject: [PATCH 16/94] add docs for local modules --- docs/docs/advanced/docker.mdx | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/docs/advanced/docker.mdx b/docs/docs/advanced/docker.mdx index 1e0188300a..e8a3a02a63 100644 --- a/docs/docs/advanced/docker.mdx +++ b/docs/docs/advanced/docker.mdx @@ -169,10 +169,56 @@ The hostname and many other options can be set using [environment variables](#en Runs code in a separate origin [sandboxed iframe](https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/) to prevent cross-site scripting. +- Local modules + + See [Local Modules](#local-modules) section for details. + - [404 page](https://livecodes.io/404) Custom 404 page for resources that are not found. +## Local Modules + +(EXPERIMENTAL) + +LiveCodes depends on a large number of external modules to support a wide range of features (e.g. code editors, compilers, formatters, etc). +These modules are loaded from CDNs (e.g. [jsDelivr](https://www.jsdelivr.com/), [unpkg](https://unpkg.com/), etc). +So, in spite of being a client-side app, LiveCodes requires an internet connection to load these modules. + +However, if you want to be able to run LiveCodes without an internet connection, you can download all these modules locally by setting the [environment variable](#environment-variables) `LOCAL_MODULES=true` (it is set to `false` by default). +In this case, all modules are downloaded during build (~ 1.5 GB), and are served locally. + +When working without internet connection, all resources have to be available locally. So, automatic [module resolution](../features/module-resolution.mdx) (e.g. of npm modules imported in user code) and loading type definitions for [intellisense](../features/intellisense.mdx) and auto-complete will not work out of the box. +However, you can provide the list of modules to load using the [`imports`](../configuration/configuration-object.mdx#imports) property of the [config](../configuration/configuration-object.mdx) object (see [Custom Module Resolution](../features/module-resolution.mdx#custom-module-resolution)). +Similarly, you can also provide the list of type definitions to load using the [`types`](../configuration/configuration-object.mdx#types) property (see [Custom Types](../features/intellisense.mdx#custom-types)). +These modules and type definitions should be prepared in advance and made available to the running app (e.g. in the `/assets` directory - see [Volumes](#volumes)). + +Example: + +To run [React starter template](https://livecodes.io/?template=react) locally without internet connection, you need to: + +(assuming that the app is running on this URL: https://livecodes.localhost/) + +- Add [these files](https://github.com/hatemhosny/custom-modules-demo/tree/main/modules) to the [`/assets` directory](#volumes). +- Open React starter template: https://livecodes.localhost/?template=react +- Add this code to custom settings (Project menu → Custom Settings): + +```json +{ + "imports": { + "react": "https://livecodes.localhost/assets/react.js", + "react/compiler-runtime": "https://livecodes.localhost/assets/react-compiler-runtime.js", + "react/jsx-runtime": "https://livecodes.localhost/assets/react-jsx-runtime.js", + "react-dom": "https://livecodes.localhost/assets/react-dom.js", + "react-dom/client": "https://livecodes.localhost/assets/react-dom-client.js", + "scheduler": "https://livecodes.localhost/assets/scheduler.js" + }, + "types": { + "react": "https://livecodes.localhost/assets/react.d.ts" + } +} +``` + ## Environment Variables The app can be customized by setting different environment variables. @@ -205,6 +251,7 @@ The following environment variables are supported: | `FIREBASE_CONFIG` | [Firebase config object](https://firebase.google.com/docs/web/learn-more#config-object) (JSON), used for [authentication](../features/github-integration.mdx) | | | `DOCS_BASE_URL` | [Base URL](../features/self-hosting.mdx#custom-build) of the documentation (e.g. `/docs/`) | `null` | | `LOG_URL` | Full URL to send [server-side analytics](https://github.com/live-codes/livecodes/blob/develop/functions/index.ts) (e.g. `https://api.website.com/log`) | `null` | +| `LOCAL_MODULES` | Download and use all modules locally (see [Local Modules](#local-modules)) | `false` | :::info note When running in a non-local environment (e.g. VPS), From 6f876a0438958aa9144e7ce882c53c0ed8e364b6 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 17 Jul 2025 01:39:41 +0300 Subject: [PATCH 17/94] set LOCAL_MODULES to false by default --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3b95be9569..bd16aaab02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: - SANDBOX_PORT=${SANDBOX_PORT:-8090} - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} - - LOCAL_MODULES=${LOCAL_MODULES:-true} + - LOCAL_MODULES=${LOCAL_MODULES:-false} restart: unless-stopped environment: - SELF_HOSTED=true From bddae1ba8bed43c188610edf0605959aa623f13c Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 17 Jul 2025 02:03:56 +0300 Subject: [PATCH 18/94] fix --- src/livecodes/services/modules.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/livecodes/services/modules.ts b/src/livecodes/services/modules.ts index 912aae05af..f05f3cb939 100644 --- a/src/livecodes/services/modules.ts +++ b/src/livecodes/services/modules.ts @@ -68,9 +68,9 @@ export const modulesService = { checkCDNs: async (testModule: string, preferredCDN?: CDN) => { const modulesBaseUrl = new URL('./modules/', location.href).href as CDN; const localCDN = localModules ? modulesBaseUrl : undefined; - const cdns: CDN[] = [preferredCDN, localCDN, ...modulesService.cdnLists.npm].filter( + const cdns = [preferredCDN, localCDN, ...modulesService.cdnLists.npm].filter( (x) => x != null, - ); + ) as CDN[]; for (const cdn of cdns) { try { const res = await fetch(modulesService.getUrl(testModule, cdn), { From 52213b9a1c74af493f32ac4129fedbae67b172a1 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 17 Jul 2025 06:37:40 +0300 Subject: [PATCH 19/94] upgrade to node v24.4.1 --- .nvmrc | 2 +- Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nvmrc b/.nvmrc index e35b986d37..564e92d084 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.1.0 +v24.4.1 diff --git a/Dockerfile b/Dockerfile index 1205ca3cda..8e4a49017a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.1.0-alpine3.21 AS builder +FROM node:24.4.1-alpine3.22 AS builder RUN apk update --no-cache && apk add --no-cache git @@ -38,7 +38,7 @@ RUN if [ "$DOCS_BASE_URL" == "null" ]; \ else npm run build; \ fi -FROM node:24.1.0-alpine3.21 AS server +FROM node:24.4.1-alpine3.22 AS server RUN addgroup -S appgroup RUN adduser -S appuser -G appgroup From adc6098fd6e31fc8cbc894dd09a57f76d1e345f0 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 17 Jul 2025 06:38:02 +0300 Subject: [PATCH 20/94] fix --- src/livecodes/services/modules.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/livecodes/services/modules.ts b/src/livecodes/services/modules.ts index f05f3cb939..d83c4e363c 100644 --- a/src/livecodes/services/modules.ts +++ b/src/livecodes/services/modules.ts @@ -100,7 +100,7 @@ const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { if (localModules && !isModule) { return getLocalUrl(modName, defaultCDN); } - if (modName.startsWith('http') || modName.startsWith('data:')) return modName; + if (modName.startsWith('data:')) return modName; const post = isModule && modName.startsWith('unpkg:') ? '?module' : ''; if (modName.startsWith('gh:')) { modName = modName.replace('gh', ghCDNs[0]); @@ -114,6 +114,7 @@ const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { return modName.replace(pattern, template) + post; } } + if (modName.startsWith('http')) return modName; return null; }; From cc5cb78d9ca9d2ab079e491ff0e9ed2f157d791e Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Fri, 18 Jul 2025 18:10:34 +0300 Subject: [PATCH 21/94] add in docker NODE_OPTIONS="--max-old-space-size=4096" --- Dockerfile | 1 + docker-compose.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8e4a49017a..65b79df75e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ ARG SANDBOX_PORT ARG FIREBASE_CONFIG ARG DOCS_BASE_URL ARG LOCAL_MODULES +ARG NODE_OPTIONS COPY scripts/download-modules.js scripts/ COPY src/livecodes/vendors.ts src/livecodes/ diff --git a/docker-compose.yml b/docker-compose.yml index bd16aaab02..801f8bfa11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} - LOCAL_MODULES=${LOCAL_MODULES:-false} + - NODE_OPTIONS="--max-old-space-size=4096" restart: unless-stopped environment: - SELF_HOSTED=true @@ -29,6 +30,7 @@ services: - LOG_URL=${LOG_URL:-null} - VALKEY_HOST=valkey - VALKEY_PORT=6379 + - NODE_OPTIONS="--max-old-space-size=4096" volumes: - ./assets:/srv/build/assets depends_on: From 21b47b8213a85a50a4e429d3c0f4fe8dc96c657b Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Fri, 18 Jul 2025 18:20:22 +0300 Subject: [PATCH 22/94] fix --- scripts/download-modules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 7afa42dc1f..50acfbafe6 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -36,7 +36,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { const failedModuleUrls = []; let pyodideBaseUrl = ''; - const downloadQueue = createAsyncQueue(10); + const downloadQueue = createAsyncQueue(5); fs.mkdirSync(modulesDir, { recursive: true }); From 0343cbff88714e75868f79054101ef32a92ab80f Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 20 Jul 2025 05:46:56 +0300 Subject: [PATCH 23/94] fix --- docker-compose.yml | 4 ++-- scripts/download-modules.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 801f8bfa11..dfcc8c54ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} - LOCAL_MODULES=${LOCAL_MODULES:-false} - - NODE_OPTIONS="--max-old-space-size=4096" + - NODE_OPTIONS=--max-old-space-size=4096 restart: unless-stopped environment: - SELF_HOSTED=true @@ -30,7 +30,7 @@ services: - LOG_URL=${LOG_URL:-null} - VALKEY_HOST=valkey - VALKEY_PORT=6379 - - NODE_OPTIONS="--max-old-space-size=4096" + - NODE_OPTIONS=--max-old-space-size=4096 volumes: - ./assets:/srv/build/assets depends_on: diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 50acfbafe6..0004773f2b 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -36,7 +36,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { const failedModuleUrls = []; let pyodideBaseUrl = ''; - const downloadQueue = createAsyncQueue(5); + const downloadQueue = createAsyncQueue(10); fs.mkdirSync(modulesDir, { recursive: true }); @@ -196,7 +196,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { fetchAndSaveFile(url, downloadPath).then((result) => { if (result instanceof Error) { failedModuleUrls.push({ module: file, url, error: result.message }); - console.error(`Failed to download module (${module}): ${result.message}`); + console.error(`Failed to download module (${file}): ${result.message}`); return; } return decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { From 8f45b35ad07253b2f6d8bfa9ddcc71c3cbc38a9e Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 27 Jul 2025 11:39:32 +0300 Subject: [PATCH 24/94] avoid using node sync methods --- server/src/broadcast/index.ts | 8 ++++---- server/src/sandbox.ts | 16 +++++++--------- server/src/utils.ts | 9 ++++----- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/server/src/broadcast/index.ts b/server/src/broadcast/index.ts index ae46f28602..c2a30c59d2 100644 --- a/server/src/broadcast/index.ts +++ b/server/src/broadcast/index.ts @@ -106,7 +106,7 @@ export const broadcast = ({ }); }); - app.get('/channels/:id', (req, res) => { + app.get('/channels/:id', async (req, res) => { const channel = req.params.id; if (channels[channel]) { channels[channel].lastAccessed = Date.now(); @@ -114,9 +114,9 @@ export const broadcast = ({ const views = ['index', 'code', 'result'] as const; const view = req.query.view; const file = views.find((v) => v === view) || (hasData ? 'index' : 'result'); - const fileContent = fs - .readFileSync(path.join(broadcastDir, `/${file}.html`), 'utf-8') - .replaceAll('{{AppUrl}}', appUrl); + const fileContent = ( + await fs.promises.readFile(path.join(broadcastDir, `/${file}.html`), 'utf-8') + ).replaceAll('{{AppUrl}}', appUrl); res.status(200).send(fileContent); } else { res.status(404).send('Channel not found!'); diff --git a/server/src/sandbox.ts b/server/src/sandbox.ts index 553f1c5251..c252ff3e7f 100644 --- a/server/src/sandbox.ts +++ b/server/src/sandbox.ts @@ -5,7 +5,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { dirname } from './utils.ts'; -export const sandbox = ({ hostname, port }: { hostname: string; port: number }) => { +export const sandbox = async ({ hostname, port }: { hostname: string; port: number }) => { const app = express(); app.use(cors()); @@ -13,11 +13,12 @@ export const sandbox = ({ hostname, port }: { hostname: string; port: number }) const sandboxDir = path.resolve(dirname, 'sandbox'); let sandboxVersionDir = path.resolve(sandboxDir, 'v8'); - fs.readdirSync(sandboxDir).forEach((v) => { - if (fs.statSync(path.resolve(sandboxDir, v)).isDirectory()) { + const dirs = await fs.promises.readdir(sandboxDir); + for (const v of dirs) { + if ((await fs.promises.stat(path.resolve(sandboxDir, v))).isDirectory()) { sandboxVersionDir = path.resolve(sandboxDir, v); } - }); + } app.use('/', (req, res) => { if (req.path === '/') { @@ -32,11 +33,8 @@ export const sandbox = ({ hostname, port }: { hostname: string; port: number }) : req.path; res.set('Content-Type', 'text/html'); const filePath = path.resolve(dirname, 'sandbox' + reqPath); - if (fs.existsSync(filePath)) { - res.status(200).sendFile(filePath); - return; - } - res.status(404).sendFile(path.resolve(sandboxVersionDir, 'index.html')); + const onError = () => res.status(404).sendFile(path.resolve(sandboxVersionDir, 'index.html')); + res.status(200).sendFile(filePath, onError); }); app.listen(port, () => { diff --git a/server/src/utils.ts b/server/src/utils.ts index 1bc567acae..3d6b17b74e 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -10,7 +10,7 @@ const getDirname = (metaUrl: string) => path.dirname(fileURLToPath(metaUrl)); export const dirname = getDirname(import.meta.url); export const appDir = path.resolve(dirname, '../../build/'); -const getFileContent = async (fullUrl: string) => { +const getFileContent = (fullUrl: string): Promise => { let pathname: string; try { const url = new URL(fullUrl); @@ -22,10 +22,9 @@ const getFileContent = async (fullUrl: string) => { pathname = 'index.html'; } let filePath = path.resolve(appDir, pathname); - if (!fs.existsSync(filePath)) { - filePath = path.resolve(appDir, '404.html'); - } - return fs.promises.readFile(filePath, 'utf8'); + return fs.promises + .readFile(filePath, 'utf8') + .catch(() => fs.promises.readFile(path.resolve(appDir, '404.html'), 'utf8')); }; const convertToWebRequest = (req: express.Request) => { From 3cb3b2523f68798fe3717a2de9152973f8fba966 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 27 Jul 2025 11:56:26 +0300 Subject: [PATCH 25/94] fixes --- scripts/download-modules.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 0004773f2b..e828e668fb 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -20,7 +20,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { ? fs.readFileSync(cacheVendorsModule, 'utf-8') : ''; - if (srcVendorsContent === cacheVendorsContent) return; + const transformedSrcContent = transformVendorsModule(srcVendorsContent); + if (transformedSrcContent === cacheVendorsContent) return; console.log(`Downloading modules...`); @@ -40,8 +41,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.mkdirSync(modulesDir, { recursive: true }); - const verdorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); - fs.writeFileSync(cacheVendorsModule, verdorModulesContent, 'utf8'); + const vendorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); + fs.writeFileSync(cacheVendorsModule, vendorModulesContent, 'utf8'); const vendorUrls = require('../' + cacheVendorsModule); // modules vs baseUrls @@ -73,7 +74,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { const mod = getModuleName(baseUrl); const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; - const modInfo = await fetch(modInfoUrl).then((res) => res.json()); + const response = await fetch(modInfoUrl); + if (!response.ok) { + console.warn(`Failed to fetch module info for ${mod}: ${response.status}`); + return; + } + const modInfo = await response.json(); const files = modInfo.files; if (Array.isArray(files)) { for (const file of files) { @@ -85,7 +91,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). const [repo, version] = mod.split('@'); const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; - const repoInfo = await fetch(filesUrl).then((res) => res.json()); + const response = await fetch(filesUrl); + if (!response.ok) { + console.warn(`Failed to fetch repo info for ${repo}: ${response.status}`); + return; + } + const repoInfo = await response.json(); const files = repoInfo.tree; if (Array.isArray(files)) { const basePath = baseUrl.split(mod + '/')[1]; From 48a9dd6c280e06af269b9311f60a992f9bbd79ed Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 3 Aug 2025 01:16:55 +0300 Subject: [PATCH 26/94] fix color picker --- src/livecodes/styles/inc-menu.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livecodes/styles/inc-menu.scss b/src/livecodes/styles/inc-menu.scss index 98ecebe646..4b92adf7c2 100644 --- a/src/livecodes/styles/inc-menu.scss +++ b/src/livecodes/styles/inc-menu.scss @@ -570,7 +570,7 @@ i.arrow { width: var(--s16); &[for='theme-color-custom'] { - background: conic-gradient(in hsl longer hue, red 0 0); + background: conic-gradient(in hsl longer hue, red 0 100%); filter: contrast(0.5); } From 8b5d433c675ca0271f6f3fc018087a77d0d2eff9 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 24 Aug 2025 01:25:59 +0300 Subject: [PATCH 27/94] fix --- server/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/utils.ts b/server/src/utils.ts index 3d6b17b74e..890cd399fc 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -21,7 +21,7 @@ const getFileContent = (fullUrl: string): Promise => { if (!pathname.trim()) { pathname = 'index.html'; } - let filePath = path.resolve(appDir, pathname); + const filePath = path.resolve(appDir, pathname); return fs.promises .readFile(filePath, 'utf8') .catch(() => fs.promises.readFile(path.resolve(appDir, '404.html'), 'utf8')); From 35315768366b6289f22d6ea815e56df145235e7e Mon Sep 17 00:00:00 2001 From: Muhammad Ayman Date: Mon, 1 Sep 2025 14:08:19 +0300 Subject: [PATCH 28/94] init --- YAEGI_INTEGRATION.md | 125 + e2e/specs/compilers.spec.ts | 3597 ++++++------ functions/vendors/templates.js | 5074 ++++++++--------- scripts/build.js | 1 + src/livecodes/languages/go-wasm/index.ts | 1 + .../languages/go-wasm/lang-go-wasm-script.ts | 191 + .../languages/go-wasm/lang-go-wasm.ts | 18 + src/livecodes/languages/languages.ts | 2 + src/livecodes/vendors.ts | 2 + src/sdk/models.ts | 6 + 10 files changed, 4697 insertions(+), 4320 deletions(-) create mode 100644 YAEGI_INTEGRATION.md create mode 100644 src/livecodes/languages/go-wasm/index.ts create mode 100644 src/livecodes/languages/go-wasm/lang-go-wasm-script.ts create mode 100644 src/livecodes/languages/go-wasm/lang-go-wasm.ts diff --git a/YAEGI_INTEGRATION.md b/YAEGI_INTEGRATION.md new file mode 100644 index 0000000000..9bd9640c9a --- /dev/null +++ b/YAEGI_INTEGRATION.md @@ -0,0 +1,125 @@ +# Yaegi WebAssembly Integration + +This document describes the integration of Yaegi (Go interpreter) compiled to WebAssembly into LiveCodes. + +## Overview + +Yaegi is a Go interpreter that can be compiled to WebAssembly, allowing Go code to run directly in the browser. This integration provides a new language option in LiveCodes called "Go (Wasm)" that uses Yaegi to execute Go code. + +## Files Added/Modified + +### New Files + +- `src/livecodes/languages/go-wasm/lang-go-wasm.ts` - Language specification +- `src/livecodes/languages/go-wasm/lang-go-wasm-script.ts` - WebAssembly implementation +- `src/livecodes/languages/go-wasm/index.ts` - Module exports + +### Modified Files + +- `src/livecodes/vendors.ts` - Added `yaegiWasmBaseUrl` +- `src/livecodes/languages/languages.ts` - Registered the new language +- `src/sdk/models.ts` - Added language types and script type +- `scripts/build.js` - Added script to build process +- `e2e/specs/compilers.spec.ts` - Added test case + +## Configuration + +The integration uses the following CDN URLs: + +- WASM file: `https://cdn.jsdelivr.net/npm/yaegi-wasm@1.0.1/src/yaegi-browser.wasm` +- Support script: `https://cdn.jsdelivr.net/npm/yaegi-wasm@1.0.1/src/wasm_exec.js` + +## Usage + +### In LiveCodes Editor + +1. Select "Go (Wasm)" from the language dropdown +2. Write Go code in the editor +3. The code will be executed using Yaegi WebAssembly + +### Example Go Code + +```go +package main + +import "fmt" + +func main() { + fmt.Println("Hello from Yaegi!") + + // Simple calculations + x := 10 + y := 20 + fmt.Printf("Sum: %d\n", x+y) + + // Loops + for i := 0; i < 5; i++ { + fmt.Printf("Count: %d\n", i) + } +} +``` + +### Supported File Extensions + +- `.wasm.go` +- `.yaegi` +- `.go-wasm` +- `.gowasm` +- `.goyae` + +## Implementation Details + +### WebAssembly Worker + +The implementation uses a Web Worker to load and execute Yaegi WebAssembly code. The worker: + +1. Loads the Go WebAssembly support script (`wasm_exec.js`) +2. Initializes Yaegi by loading the WASM file +3. Executes Go code and captures output + +### Code Execution + +The Go code is executed in a WebAssembly environment that provides: + +- Standard Go runtime features +- Console output capture +- Input/output redirection + +## Testing + +A test case has been added to `e2e/specs/compilers.spec.ts` that verifies: + +- The language is available in the UI +- Go code can be written and executed +- Output is correctly displayed + +## Limitations + +1. **Standard Library**: Not all Go standard library packages may be available in the WebAssembly version. + +2. **Performance**: WebAssembly execution may be slower than native Go execution. + +## Future Improvements + +1. **Better Error Handling**: Implement error messages and debugging capabilities. + +## Troubleshooting + +### Common Issues + +1. **Yaegi not loading**: Check that the CDN URLs are accessible and the WASM file is properly loaded. + +2. **Output not appearing**: Check the browser console for any JavaScript errors. + +### Debug Mode + +Enable browser developer tools to see detailed error messages and debug the WebAssembly execution. + +## Contributing + +To improve this integration: + +1. Test with various Go code examples +2. Report issues with specific Go features +3. Contribute improvements to error handling +4. Add support for additional Go packages diff --git a/e2e/specs/compilers.spec.ts b/e2e/specs/compilers.spec.ts index bba6112526..ce34a916a2 100644 --- a/e2e/specs/compilers.spec.ts +++ b/e2e/specs/compilers.spec.ts @@ -1,1783 +1,1814 @@ -import { expect } from '@playwright/test'; -import { getLoadedApp, waitForEditorFocus } from '../helpers'; -import { test } from '../test-fixtures'; - -test.describe('Compiler Results', () => { - test('HTML/CSS/JS', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.type('hello, '); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text="CSS"'); - await waitForEditorFocus(app); - await page.keyboard.type('body {color: blue;}'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text="JavaScript"'); - await waitForEditorFocus(app); - await page.keyboard.type('document.body.innerHTML += "world!"'); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('body'); - - expect(resultText).toContain('hello, world!'); - expect(await getResult().$eval('body', (e) => getComputedStyle(e).color)).toBe( - 'rgb(0, 0, 255)', - ); - }); - - test('Markdown', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Markdown'); - await waitForEditorFocus(app); - await app.page().keyboard.type('# Hi There'); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toBe('Hi There'); - }); - - test('MDX', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=MDX'); - await waitForEditorFocus(app); - await app.page().keyboard.type(` -import {Hello} from './script'; - - -`); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JSX'); - await waitForEditorFocus(app); - await app.page().keyboard.type(` -import React from 'react'; -export const Hello = (props) =>

Hello, {props.title}!

; -`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toBe('Hello, World!'); - }); - - test('Astro', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Astro'); - await waitForEditorFocus(app); - await app.page().keyboard.type(`--- -const title = "World"; ---- - -

Hello, {title}!

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toBe('Hello, World!'); - }); - - test('Pug', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Pug'); - await waitForEditorFocus(app); - await page.keyboard.type('h1 Hi There'); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toBe('Hi There'); - }); - - test('Haml', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "Haml"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text="Haml"'); - await waitForEditorFocus(app); - await page.keyboard.type('.content Hello, #{name}!'); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('.content'); - - expect(resultText).toContain('Hello, Haml!'); - }); - - test('AsciiDoc', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=AsciiDoc'); - await waitForEditorFocus(app); - await page.keyboard.type('== Hello, World!'); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h2'); - - expect(resultText).toContain('Hello, World!'); - }); - - test('Mustache', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "Mustache"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Mustache'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{name}}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Mustache'); - }); - - test('Mustache dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'Mustache' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Mustache'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{name}}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Mustache'); - }); - - test('Handlebars', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "Handlebars"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Handlebars'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{name}}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Handlebars'); - }); - - test('Handlebars dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'Handlebars' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Handlebars'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{name}}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Handlebars'); - }); - - test('Nunjucks', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "Nunjucks"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Nunjucks'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{name}}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Nunjucks'); - }); - - test('Nunjucks dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'Nunjucks' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Nunjucks'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{name}}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Nunjucks'); - }); - - test('EJS', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "EJS"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=EJS'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to <%= name %>

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to EJS'); - }); - - test('EJS dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'EJS' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=EJS'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to <%= name %>

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to EJS'); - }); - - test('Eta', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "Eta"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text="Eta"'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to <%= it.name %>

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Eta'); - }); - - test('Eta dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'Eta' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text="Eta"'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to <%= it.name %>

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Eta'); - }); - - test('Liquid', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name":"liquid"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Liquid'); - await waitForEditorFocus(app); - await page.keyboard.type(`{{ name | capitalize | prepend: "Welcome to "}}`); - - await waitForResultUpdate(); - const body = await getResult().$('body'); - - expect(await body?.innerHTML()).toContain('Welcome to Liquid'); - }); - - test('Liquid dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'liquid' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Liquid'); - await waitForEditorFocus(app); - await page.keyboard.type(`{{ name | capitalize | prepend: "Welcome to "}}`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const body = await getResult().$('body'); - - expect(await body?.innerHTML()).toContain('Welcome to Liquid'); - }); - - test('doT', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name":"doT"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=doT'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{=it.name}}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to doT'); - }); - - test('doT dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'doT' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=doT'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{=it.name}}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to doT'); - }); - - test('Twig', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "Twig"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Twig'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Twig'); - }); - - test('Twig dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'Twig' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Twig'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Twig'); - }); - - test('Vento', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "Vento"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Vento'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Vento'); - }); - - test('Vento dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'Vento' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=Vento'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to Vento'); - }); - - test('art-template', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "art-template"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=art-template'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to art-template'); - }); - - test('art-template dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'art-template' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=art-template'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to art-template'); - }); - - test('jinja', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"data":{"name": "jinja"}}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text="Jinja"'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to jinja'); - }); - - test('jinja dynamic', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('[aria-label="Project"]'); - await app.click('text=Custom Settings'); - await waitForEditorFocus(app, '#custom-settings-editor'); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Delete'); - await page.keyboard.type(`{"template":{"prerender": false}}`); - await app.click('button:has-text("Load"):visible'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=JavaScript'); - await waitForEditorFocus(app); - await page.keyboard.type(`window.livecodes.templateData = { name: 'jinja' };`); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text="Jinja"'); - await waitForEditorFocus(app); - await page.keyboard.type(`

Welcome to {{ name }}

`); - - await waitForResultUpdate(); - await app.waitForTimeout(3000); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain('Welcome to jinja'); - }); - - test('BBCode', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=BBCode'); - await waitForEditorFocus(app); - await app.page().keyboard.type('[quote]quoted text[/quote]'); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('blockquote'); - - expect(resultText).toBe('quoted text'); - }); - - test('MJML', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 1)'); - await app.click('text=MJML'); - await waitForEditorFocus(app); - await page.keyboard.type(` - - - - - - Hello MJML! - - - - - -`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('table'); - - expect(resultText).toContain('Hello MJML!'); - }); - - test('SCSS', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=SCSS'); - await waitForEditorFocus(app); - await page.keyboard.type( - `$font-stack: Helvetica, sans-serif; body { font: 100% $font-stack; }`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain('font: 100% Helvetica, sans-serif;'); - }); - - test('Sass', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Sass'); - await waitForEditorFocus(app); - await page.keyboard.type(`$font-stack: Helvetica, sans-serif\nbody\n font: 100% $font-stack`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain('font: 100% Helvetica, sans-serif;'); - }); - - test('Less', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Less'); - await waitForEditorFocus(app); - await page.keyboard.type(`@width: 10px; #header { width: @width; }`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain('width: 10px;'); - }); - - test('Stylus', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Stylus'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`font-size = 14px\nbody\n font font-size Arial, sans-serif`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain('font: 14px Arial, sans-serif;'); - }); - - test('Stylis', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Stylis'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - '[namespace] {\n div {\n display: flex;\n\n @media screen {\n color: blue;\n }\n }\n\n div {\n transform: translateZ(0);\n\n h1, h2 {\n color: red;\n }\n }\n}', - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain( - '[namespace] div{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}@media screen{[namespace] div{color:blue;}}[namespace] div{-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);}[namespace] div h1,[namespace] div h2{color:red;}', - ); - }); - - test('PostCSS/postcssImportUrl', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Import Url'); - await app.click('text=CSS'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`@import "github-markdown-css";`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain('.markdown-body'); - }); - - test('PostCSS/Autoprefixer', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Autoprefixer'); - await app.click('text=CSS'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`.example { user-select: none; }`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain('-webkit-user-select: none;'); - }); - - test('PostCSS/Preset Env', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Preset Env'); - await app.click('text=CSS'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - `:root { --mainColor: #12345678; --secondaryColor: lab(32.5 38.5 -47.6 / 90%); }`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('style'); - - expect(resultText).toContain('--mainColor: rgba(18,52,86,0.47059);'); - }); - - test('Babel', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Babel'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`[1, 2, 3].map(n => n + 1);`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('body > script'); - - expect(resultText).toContain( - `[1, 2, 3].map(function (n) { - return n + 1; -});`, - ); - }); - - test('Sucrase', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Sucrase'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`const Greet = (name: string) => <>Hello {name}!;`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('body > script'); - - expect(resultText).toContain( - `const Greet = (name) => React.createElement(React.Fragment, null, "Hello " , name, "!");`, - ); - }); - - test('TypeScript', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=TypeScript'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - `type Fish = { swim: () => void }; -type Bird = { fly: () => void }; -declare function getSmallPet(): Fish | Bird; -// ---cut--- -function isFish(pet: Fish | Bird): pet is Fish { - return (pet as Fish).swim !== undefined; -}`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('body > script'); - - expect(resultText).toContain( - `// ---cut--- -function isFish(pet) { - return pet.swim !== undefined; -}`, - ); - }); - - test('Flow', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Flow'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - 'function foo(x: ?number): string {if (x) { return x; } return "default string"; }', - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('body > script'); - - expect(resultText).toContain( - 'function foo(x ) {if (x) { return x; } return "default string"; }', - ); - }); - - test('JSX', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`
Loading...
`); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text="JSX"'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` -const Hello = (props) =>

Hello, {props.name}

-export default () => ; -`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, React`); - }); - - test('TSX', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text="TSX"'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` -interface Props { name: string } -const Hello = (props: Props) =>

Hello, {props.name}

-export default () => ; -`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, React`); - }); - - test('React', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`
Loading...
`); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text="React"'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` -const Hello = (props) =>

Hello, {props.name}

-export default () => ; -`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, React`); - }); - - test('React (TSX)', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text="React (TSX)"'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` -interface Props { name: string } -const Hello = (props: Props) =>

Hello, {props.name}

-export default () => ; -`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, React`); - }); - - test('Vue', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await (await app.$('[data-lang="vue"]'))?.click(); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` - -`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, Vue`); - expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); - }); - - test('Vue JSX', async ({ page, getTestUrl }) => { - const sfc = ` - - -`; - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - await app.click(':nth-match([title="Change Language"], 3)'); - await (await app.$('[data-lang="vue"]'))?.click(); - await waitForEditorFocus(app); - await page.keyboard.insertText(sfc); - - await waitForResultUpdate(); - - await getResult().click('text=Click me'); - await getResult().click('text=Click me'); - await getResult().click('text=Click me'); - - const titleText = await getResult().innerText('h1'); - expect(titleText).toBe('Hello, Vue!'); - - const counterText = await getResult().innerText('text=You clicked'); - expect(counterText).toBe('You clicked 3 times.'); - }); - - test('Vue import', async ({ page, getTestUrl }) => { - const sfc = ` - - -`; - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await (await app.$('[data-lang="vue"]'))?.click(); - await waitForEditorFocus(app); - await page.keyboard.insertText(sfc); - - await waitForResultUpdate(); - - // css import - const headHTML = await getResult().innerHTML('head'); - expect(headHTML).toContain( - '', - ); - - // bare module import - const uuidText = await getResult().innerText('#uuid'); - expect(uuidText.length).toBeGreaterThan(10); - - // import vue component - await getResult().click(':nth-match(button, 1)'); - await getResult().click(':nth-match(button, 1)'); - await getResult().click(':nth-match(button, 1)'); - - // import vue component that has relative imports and fetches absolute url - const buttonText = await getResult().innerText(':nth-match(button, 1)'); - expect(buttonText).toBe('Count is: 3, double is: 6'); - }); - - test('Vue langs', async ({ page, getTestUrl }) => { - const sfc = ` - - - - -`; - - await page.goto( - getTestUrl({ - x: 'code/N4IgLglmA2CmIC4QFUB2kawCYAIAKATgPYBWsAxmCADQhawDO5BEADpEaoiDeAIYBzBogDaAXVp9KEAG6wAolihEC3Ji3a8AtnwIBrAK6tEoaH1QCDg+EgAWYLdF7lOYWOm4gAvrQZgAnnAmIGYWVgI2IOQMwrQu6O5USN6+zGxJpuaW1twyBvBxroncADxuWqxmbjihAgC8ADogWlhNAHwNBA2oODgAxDgAErDQ0ETUOMDAOKh8WrA4Xl4AhJ3dvSWsOAjkZjGNIAAkfoGwAHRuAB5g7QxE8zhXYCUA9KwdXaiv5ZV8bh-dNZfdTpGpZA5gBhNHAMWBgIwAno4eJ+GZzWAIGFgFgWHB1HAAcgAavkcABmAkAbiBrxB7ERNJOcDBFgOTBi0K0RCwBjgiN6hwAZq4ALR+KR6THDaBySDkPgTBjmBhi2AsQXUz4C1gsHQEfwilxjAiYvoAVgtmvWOGOFE4WF0BqNKkxAgIsHcVtQQN6tgAjJMfb0cML0JiAEwABkjAFIbaGwGKwBKvcHkURjZjDjqIHqnRmVKnFkCgxdYNdA1rgwnMX7o3GhaLxeQ9EXes6TTbYfEHfrDQWCEWvDSXkzYG0UiAxwxbB7IaIJFO0uxhAhxHEYoRGHDPLQdURyIw7gRV+uogY-PcAMpwyAWVfAHwgXOsFTzhCP2gBViMExPtx+A+IRZOEkTfowy5UIUCQeMkT5yCeECcNwAAs3hAA', - }), - ); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await (await app.$('[data-lang="vue"]'))?.click(); - await waitForEditorFocus(app); - // await page.keyboard.insertText(sfc); - - await waitForResultUpdate(); - - const headerText = await getResult().innerText('h1'); - - // markdown, scss, typescript - expect(headerText).toContain(`Hello, Vue 3!`); - expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(85, 85, 85)'); - - // css modules - expect(await getResult().$eval('p', (e) => getComputedStyle(e).color)).toBe('rgb(0, 128, 0)'); - }); - - test('Vue + Tailwind CSS', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await (await app.$('[data-lang="vue"]'))?.click(); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` -`, - ); - - await app.click(':nth-match([title="Change Language"], 2)'); - await app.click('text=Tailwind CSS'); - await app.click('text=CSS'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - `@tailwind base; -@tailwind components; -@tailwind utilities; -`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Tailwind in Vue SFC`); - expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe( - 'rgb(220, 38, 38)', - ); - }); - - test('Vue 2', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Vue 2'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` - -`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, Vue 2`); - expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); - }); - - test('Svelte', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await (await app.$('[data-lang="svelte"]'))?.click(); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` - -
-

Hello, {title}

-
-`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, Svelte`); - expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); - }); - - test('Malina.js', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Malina.js'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - ` - -
-

Hello, {title}

-
-`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, Malina.js`); - expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); - }); - - test('Stencil', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText(''); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Stencil'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - `import { Component, Prop, h } from "@stencil/core"; -@Component({ - tag: "my-app", - styles: "h1 { color: blue; }", -}) -export class App { - @Prop() title: string; - render() { - return ( -

Hello, {this.title}

- ); - } -}`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('h1'); - - expect(resultText).toContain(`Hello, Stencil`); - expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); - }); - - test('CoffeeScript', async ({ page, getTestUrl }) => { - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=CoffeeScript'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`square = (x) -> x * x`); - - await waitForResultUpdate(); - const resultText = await getResult().innerHTML('body > script'); - - expect(resultText).toContain(`var square;`); - expect(resultText).toContain(`square = function(x) { - return x * x; -};`); - }); - - test('LiveScript', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, World

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=LiveScript'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`{ capitalize, join, map, words } = require 'prelude-ls' -title = 'live script' -|> words -|> map capitalize -|> join '' -(document.getElementById \\title).textContent = title`); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, LiveScript`); - }); - - test('Riot.js', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText(''); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Riot.js'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, {props.title}

'); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, Riot.js`); - }); - - test('AssemblyScript', async ({ page, getTestUrl }) => { - test.fixme(); - - await page.goto(getTestUrl()); - - const { app, getResult } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText( - `

Hello, World

- `, - ); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=AssemblyScript'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`export function getTitle(): string {return "AssemblyScript";`); - // workaround for monaco auto-complete - await page.keyboard.press('Delete'); - await page.keyboard.insertText(`}`); - - await app.waitForTimeout(15000); - const resultText = await getResult().innerText('text=Hello, AssemblyScript'); - - expect(resultText).toContain(`Hello, AssemblyScript`); - }); - - test('Python', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, World

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Python'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`from browser import document -title = 'Python' -document['header'].html = f"Hello, {title}"`); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, Python`); - }); - - test('Pyodide', async ({ page, getTestUrl }) => { - test.skip(); - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, World

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Pyodide'); - await waitForEditorFocus(app); - await page.keyboard.insertText(`from js import document -title = 'Python' -document.getElementById('header').innerHTML = f"Hello, {title}"`); - - await waitForResultUpdate(); - await getResult().waitForSelector('text=Hello, Python'); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, Python`); - }); - - test('Ruby', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, world

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Ruby'); - await waitForEditorFocus(app); - - await page.keyboard.insertText(`require "native" -title = 'Ruby' -$$.document.querySelector('#title').innerHTML = title`); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('text=Hello, Ruby'); - - expect(resultText).toContain(`Hello, Ruby`); - }); - - test('Go', async ({ page, getTestUrl, editor }) => { - test.slow(); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, world

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Go'); - await waitForEditorFocus(app); - - await page.keyboard.insertText(`package main -import "syscall/js" -func main() { - js.Global().Get("document").Call("querySelector", "#title").Set("innerHTML", "Golang") -}`); - - await waitForResultUpdate({ delay: 4000, timeout: 60_000 }); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, Golang`); - }); - - test('PHP', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, world

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=PHP'); - await waitForEditorFocus(app); - - // go below pre-inserted 'getElementById('title')->textContent = $title;`, - ); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, PHP`); - }); - - test('Perl', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, world

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Perl'); - await waitForEditorFocus(app); - - await page.keyboard.insertText(`use strict; -my $title = 'Perl'; -JS::inline('document.getElementById("title").innerHTML') = $title;`); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, Perl`); - }); - - test('Lua', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, world

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Lua'); - await waitForEditorFocus(app); - - await page.keyboard.insertText(`js = require "js" -window = js.global -document = window.document -document:getElementById("title").innerHTML = "Lua"`); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, Lua`); - }); - - test('Scheme', async ({ page, getTestUrl, editor }) => { - test.skip(editor === 'codejar', 'please fix'); - - await page.goto(getTestUrl()); - - const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); - - await app.click('text="HTML"'); - await waitForEditorFocus(app); - await page.keyboard.insertText('

Hello, world

'); - - await app.click(':nth-match([title="Change Language"], 3)'); - await app.click('text=Scheme'); - await waitForEditorFocus(app); - - await page.keyboard.insertText(`(let ((title "Scheme")) - (set-content! "#title" title))`); - - await waitForResultUpdate(); - const resultText = await getResult().innerText('h1'); - - expect(resultText).toContain(`Hello, Scheme`); - }); -}); +import { expect } from '@playwright/test'; +import { getLoadedApp, waitForEditorFocus } from '../helpers'; +import { test } from '../test-fixtures'; + +test.describe('Compiler Results', () => { + test('HTML/CSS/JS', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.type('hello, '); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text="CSS"'); + await waitForEditorFocus(app); + await page.keyboard.type('body {color: blue;}'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text="JavaScript"'); + await waitForEditorFocus(app); + await page.keyboard.type('document.body.innerHTML += "world!"'); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('body'); + + expect(resultText).toContain('hello, world!'); + expect(await getResult().$eval('body', (e) => getComputedStyle(e).color)).toBe( + 'rgb(0, 0, 255)', + ); + }); + + test('Markdown', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Markdown'); + await waitForEditorFocus(app); + await app.page().keyboard.type('# Hi There'); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toBe('Hi There'); + }); + + test('MDX', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=MDX'); + await waitForEditorFocus(app); + await app.page().keyboard.type(` +import {Hello} from './script'; + + +`); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JSX'); + await waitForEditorFocus(app); + await app.page().keyboard.type(` +import React from 'react'; +export const Hello = (props) =>

Hello, {props.title}!

; +`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toBe('Hello, World!'); + }); + + test('Astro', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Astro'); + await waitForEditorFocus(app); + await app.page().keyboard.type(`--- +const title = "World"; +--- + +

Hello, {title}!

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toBe('Hello, World!'); + }); + + test('Pug', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Pug'); + await waitForEditorFocus(app); + await page.keyboard.type('h1 Hi There'); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toBe('Hi There'); + }); + + test('Haml', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "Haml"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text="Haml"'); + await waitForEditorFocus(app); + await page.keyboard.type('.content Hello, #{name}!'); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('.content'); + + expect(resultText).toContain('Hello, Haml!'); + }); + + test('AsciiDoc', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=AsciiDoc'); + await waitForEditorFocus(app); + await page.keyboard.type('== Hello, World!'); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h2'); + + expect(resultText).toContain('Hello, World!'); + }); + + test('Mustache', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "Mustache"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Mustache'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{name}}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Mustache'); + }); + + test('Mustache dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'Mustache' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Mustache'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{name}}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Mustache'); + }); + + test('Handlebars', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "Handlebars"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Handlebars'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{name}}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Handlebars'); + }); + + test('Handlebars dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'Handlebars' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Handlebars'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{name}}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Handlebars'); + }); + + test('Nunjucks', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "Nunjucks"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Nunjucks'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{name}}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Nunjucks'); + }); + + test('Nunjucks dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'Nunjucks' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Nunjucks'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{name}}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Nunjucks'); + }); + + test('EJS', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "EJS"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=EJS'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to <%= name %>

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to EJS'); + }); + + test('EJS dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'EJS' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=EJS'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to <%= name %>

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to EJS'); + }); + + test('Eta', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "Eta"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text="Eta"'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to <%= it.name %>

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Eta'); + }); + + test('Eta dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'Eta' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text="Eta"'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to <%= it.name %>

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Eta'); + }); + + test('Liquid', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name":"liquid"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Liquid'); + await waitForEditorFocus(app); + await page.keyboard.type(`{{ name | capitalize | prepend: "Welcome to "}}`); + + await waitForResultUpdate(); + const body = await getResult().$('body'); + + expect(await body?.innerHTML()).toContain('Welcome to Liquid'); + }); + + test('Liquid dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'liquid' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Liquid'); + await waitForEditorFocus(app); + await page.keyboard.type(`{{ name | capitalize | prepend: "Welcome to "}}`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const body = await getResult().$('body'); + + expect(await body?.innerHTML()).toContain('Welcome to Liquid'); + }); + + test('doT', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name":"doT"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=doT'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{=it.name}}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to doT'); + }); + + test('doT dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'doT' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=doT'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{=it.name}}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to doT'); + }); + + test('Twig', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "Twig"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Twig'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Twig'); + }); + + test('Twig dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'Twig' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Twig'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Twig'); + }); + + test('Vento', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "Vento"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Vento'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Vento'); + }); + + test('Vento dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'Vento' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=Vento'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to Vento'); + }); + + test('art-template', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "art-template"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=art-template'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to art-template'); + }); + + test('art-template dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'art-template' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=art-template'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to art-template'); + }); + + test('jinja', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"data":{"name": "jinja"}}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text="Jinja"'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to jinja'); + }); + + test('jinja dynamic', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=Custom Settings'); + await waitForEditorFocus(app, '#custom-settings-editor'); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Delete'); + await page.keyboard.type(`{"template":{"prerender": false}}`); + await app.click('button:has-text("Load"):visible'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=JavaScript'); + await waitForEditorFocus(app); + await page.keyboard.type(`window.livecodes.templateData = { name: 'jinja' };`); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text="Jinja"'); + await waitForEditorFocus(app); + await page.keyboard.type(`

Welcome to {{ name }}

`); + + await waitForResultUpdate(); + await app.waitForTimeout(3000); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain('Welcome to jinja'); + }); + + test('BBCode', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=BBCode'); + await waitForEditorFocus(app); + await app.page().keyboard.type('[quote]quoted text[/quote]'); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('blockquote'); + + expect(resultText).toBe('quoted text'); + }); + + test('MJML', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 1)'); + await app.click('text=MJML'); + await waitForEditorFocus(app); + await page.keyboard.type(` + + + + + + Hello MJML! + + + + + +`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('table'); + + expect(resultText).toContain('Hello MJML!'); + }); + + test('SCSS', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=SCSS'); + await waitForEditorFocus(app); + await page.keyboard.type( + `$font-stack: Helvetica, sans-serif; body { font: 100% $font-stack; }`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain('font: 100% Helvetica, sans-serif;'); + }); + + test('Sass', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Sass'); + await waitForEditorFocus(app); + await page.keyboard.type(`$font-stack: Helvetica, sans-serif\nbody\n font: 100% $font-stack`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain('font: 100% Helvetica, sans-serif;'); + }); + + test('Less', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Less'); + await waitForEditorFocus(app); + await page.keyboard.type(`@width: 10px; #header { width: @width; }`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain('width: 10px;'); + }); + + test('Stylus', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Stylus'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`font-size = 14px\nbody\n font font-size Arial, sans-serif`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain('font: 14px Arial, sans-serif;'); + }); + + test('Stylis', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Stylis'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + '[namespace] {\n div {\n display: flex;\n\n @media screen {\n color: blue;\n }\n }\n\n div {\n transform: translateZ(0);\n\n h1, h2 {\n color: red;\n }\n }\n}', + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain( + '[namespace] div{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}@media screen{[namespace] div{color:blue;}}[namespace] div{-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);}[namespace] div h1,[namespace] div h2{color:red;}', + ); + }); + + test('PostCSS/postcssImportUrl', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Import Url'); + await app.click('text=CSS'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`@import "github-markdown-css";`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain('.markdown-body'); + }); + + test('PostCSS/Autoprefixer', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Autoprefixer'); + await app.click('text=CSS'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`.example { user-select: none; }`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain('-webkit-user-select: none;'); + }); + + test('PostCSS/Preset Env', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Preset Env'); + await app.click('text=CSS'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + `:root { --mainColor: #12345678; --secondaryColor: lab(32.5 38.5 -47.6 / 90%); }`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('style'); + + expect(resultText).toContain('--mainColor: rgba(18,52,86,0.47059);'); + }); + + test('Babel', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Babel'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`[1, 2, 3].map(n => n + 1);`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('body > script'); + + expect(resultText).toContain( + `[1, 2, 3].map(function (n) { + return n + 1; +});`, + ); + }); + + test('Sucrase', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Sucrase'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`const Greet = (name: string) => <>Hello {name}!;`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('body > script'); + + expect(resultText).toContain( + `const Greet = (name) => React.createElement(React.Fragment, null, "Hello " , name, "!");`, + ); + }); + + test('TypeScript', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=TypeScript'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + `type Fish = { swim: () => void }; +type Bird = { fly: () => void }; +declare function getSmallPet(): Fish | Bird; +// ---cut--- +function isFish(pet: Fish | Bird): pet is Fish { + return (pet as Fish).swim !== undefined; +}`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('body > script'); + + expect(resultText).toContain( + `// ---cut--- +function isFish(pet) { + return pet.swim !== undefined; +}`, + ); + }); + + test('Flow', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Flow'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + 'function foo(x: ?number): string {if (x) { return x; } return "default string"; }', + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('body > script'); + + expect(resultText).toContain( + 'function foo(x ) {if (x) { return x; } return "default string"; }', + ); + }); + + test('JSX', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`
Loading...
`); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text="JSX"'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` +const Hello = (props) =>

Hello, {props.name}

+export default () => ; +`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, React`); + }); + + test('TSX', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text="TSX"'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` +interface Props { name: string } +const Hello = (props: Props) =>

Hello, {props.name}

+export default () => ; +`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, React`); + }); + + test('React', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`
Loading...
`); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text="React"'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` +const Hello = (props) =>

Hello, {props.name}

+export default () => ; +`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, React`); + }); + + test('React (TSX)', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text="React (TSX)"'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` +interface Props { name: string } +const Hello = (props: Props) =>

Hello, {props.name}

+export default () => ; +`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, React`); + }); + + test('Vue', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await (await app.$('[data-lang="vue"]'))?.click(); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` + +`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, Vue`); + expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); + }); + + test('Vue JSX', async ({ page, getTestUrl }) => { + const sfc = ` + + +`; + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + await app.click(':nth-match([title="Change Language"], 3)'); + await (await app.$('[data-lang="vue"]'))?.click(); + await waitForEditorFocus(app); + await page.keyboard.insertText(sfc); + + await waitForResultUpdate(); + + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + + const titleText = await getResult().innerText('h1'); + expect(titleText).toBe('Hello, Vue!'); + + const counterText = await getResult().innerText('text=You clicked'); + expect(counterText).toBe('You clicked 3 times.'); + }); + + test('Vue import', async ({ page, getTestUrl }) => { + const sfc = ` + + +`; + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await (await app.$('[data-lang="vue"]'))?.click(); + await waitForEditorFocus(app); + await page.keyboard.insertText(sfc); + + await waitForResultUpdate(); + + // css import + const headHTML = await getResult().innerHTML('head'); + expect(headHTML).toContain( + '', + ); + + // bare module import + const uuidText = await getResult().innerText('#uuid'); + expect(uuidText.length).toBeGreaterThan(10); + + // import vue component + await getResult().click(':nth-match(button, 1)'); + await getResult().click(':nth-match(button, 1)'); + await getResult().click(':nth-match(button, 1)'); + + // import vue component that has relative imports and fetches absolute url + const buttonText = await getResult().innerText(':nth-match(button, 1)'); + expect(buttonText).toBe('Count is: 3, double is: 6'); + }); + + test('Vue langs', async ({ page, getTestUrl }) => { + const sfc = ` + + + + +`; + + await page.goto( + getTestUrl({ + x: 'code/N4IgLglmA2CmIC4QFUB2kawCYAIAKATgPYBWsAxmCADQhawDO5BEADpEaoiDeAIYBzBogDaAXVp9KEAG6wAolihEC3Ji3a8AtnwIBrAK6tEoaH1QCDg+EgAWYLdF7lOYWOm4gAvrQZgAnnAmIGYWVgI2IOQMwrQu6O5USN6+zGxJpuaW1twyBvBxroncADxuWqxmbjihAgC8ADogWlhNAHwNBA2oODgAxDgAErDQ0ETUOMDAOKh8WrA4Xl4AhJ3dvSWsOAjkZjGNIAAkfoGwAHRuAB5g7QxE8zhXYCUA9KwdXaiv5ZV8bh-dNZfdTpGpZA5gBhNHAMWBgIwAno4eJ+GZzWAIGFgFgWHB1HAAcgAavkcABmAkAbiBrxB7ERNJOcDBFgOTBi0K0RCwBjgiN6hwAZq4ALR+KR6THDaBySDkPgTBjmBhi2AsQXUz4C1gsHQEfwilxjAiYvoAVgtmvWOGOFE4WF0BqNKkxAgIsHcVtQQN6tgAjJMfb0cML0JiAEwABkjAFIbaGwGKwBKvcHkURjZjDjqIHqnRmVKnFkCgxdYNdA1rgwnMX7o3GhaLxeQ9EXes6TTbYfEHfrDQWCEWvDSXkzYG0UiAxwxbB7IaIJFO0uxhAhxHEYoRGHDPLQdURyIw7gRV+uogY-PcAMpwyAWVfAHwgXOsFTzhCP2gBViMExPtx+A+IRZOEkTfowy5UIUCQeMkT5yCeECcNwAAs3hAA', + }), + ); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await (await app.$('[data-lang="vue"]'))?.click(); + await waitForEditorFocus(app); + // await page.keyboard.insertText(sfc); + + await waitForResultUpdate(); + + const headerText = await getResult().innerText('h1'); + + // markdown, scss, typescript + expect(headerText).toContain(`Hello, Vue 3!`); + expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(85, 85, 85)'); + + // css modules + expect(await getResult().$eval('p', (e) => getComputedStyle(e).color)).toBe('rgb(0, 128, 0)'); + }); + + test('Vue + Tailwind CSS', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await (await app.$('[data-lang="vue"]'))?.click(); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` +`, + ); + + await app.click(':nth-match([title="Change Language"], 2)'); + await app.click('text=Tailwind CSS'); + await app.click('text=CSS'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + `@tailwind base; +@tailwind components; +@tailwind utilities; +`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Tailwind in Vue SFC`); + expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe( + 'rgb(220, 38, 38)', + ); + }); + + test('Vue 2', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Vue 2'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` + +`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, Vue 2`); + expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); + }); + + test('Svelte', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await (await app.$('[data-lang="svelte"]'))?.click(); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` + +
+

Hello, {title}

+
+`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, Svelte`); + expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); + }); + + test('Malina.js', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Malina.js'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + ` + +
+

Hello, {title}

+
+`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, Malina.js`); + expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); + }); + + test('Stencil', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText(''); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Stencil'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + `import { Component, Prop, h } from "@stencil/core"; +@Component({ + tag: "my-app", + styles: "h1 { color: blue; }", +}) +export class App { + @Prop() title: string; + render() { + return ( +

Hello, {this.title}

+ ); + } +}`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('h1'); + + expect(resultText).toContain(`Hello, Stencil`); + expect(await getResult().$eval('h1', (e) => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)'); + }); + + test('CoffeeScript', async ({ page, getTestUrl }) => { + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=CoffeeScript'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`square = (x) -> x * x`); + + await waitForResultUpdate(); + const resultText = await getResult().innerHTML('body > script'); + + expect(resultText).toContain(`var square;`); + expect(resultText).toContain(`square = function(x) { + return x * x; +};`); + }); + + test('LiveScript', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, World

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=LiveScript'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`{ capitalize, join, map, words } = require 'prelude-ls' +title = 'live script' +|> words +|> map capitalize +|> join '' +(document.getElementById \\title).textContent = title`); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, LiveScript`); + }); + + test('Riot.js', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText(''); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Riot.js'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, {props.title}

'); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, Riot.js`); + }); + + test('AssemblyScript', async ({ page, getTestUrl }) => { + test.fixme(); + + await page.goto(getTestUrl()); + + const { app, getResult } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText( + `

Hello, World

+ `, + ); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=AssemblyScript'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`export function getTitle(): string {return "AssemblyScript";`); + // workaround for monaco auto-complete + await page.keyboard.press('Delete'); + await page.keyboard.insertText(`}`); + + await app.waitForTimeout(15000); + const resultText = await getResult().innerText('text=Hello, AssemblyScript'); + + expect(resultText).toContain(`Hello, AssemblyScript`); + }); + + test('Python', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, World

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Python'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`from browser import document +title = 'Python' +document['header'].html = f"Hello, {title}"`); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, Python`); + }); + + test('Pyodide', async ({ page, getTestUrl }) => { + test.skip(); + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, World

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Pyodide'); + await waitForEditorFocus(app); + await page.keyboard.insertText(`from js import document +title = 'Python' +document.getElementById('header').innerHTML = f"Hello, {title}"`); + + await waitForResultUpdate(); + await getResult().waitForSelector('text=Hello, Python'); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, Python`); + }); + + test('Ruby', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, world

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Ruby'); + await waitForEditorFocus(app); + + await page.keyboard.insertText(`require "native" +title = 'Ruby' +$$.document.querySelector('#title').innerHTML = title`); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('text=Hello, Ruby'); + + expect(resultText).toContain(`Hello, Ruby`); + }); + + test('Go', async ({ page, getTestUrl, editor }) => { + test.slow(); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, world

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Go'); + await waitForEditorFocus(app); + + await page.keyboard.insertText(`package main +import "syscall/js" +func main() { + js.Global().Get("document").Call("querySelector", "#title").Set("innerHTML", "Golang") +}`); + + await waitForResultUpdate({ delay: 4000, timeout: 60_000 }); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, Golang`); + }); + + test('PHP', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, world

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=PHP'); + await waitForEditorFocus(app); + + // go below pre-inserted 'getElementById('title')->textContent = $title;`, + ); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, PHP`); + }); + + test('Perl', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, world

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Perl'); + await waitForEditorFocus(app); + + await page.keyboard.insertText(`use strict; +my $title = 'Perl'; +JS::inline('document.getElementById("title").innerHTML') = $title;`); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, Perl`); + }); + + test('Lua', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, world

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Lua'); + await waitForEditorFocus(app); + + await page.keyboard.insertText(`js = require "js" +window = js.global +document = window.document +document:getElementById("title").innerHTML = "Lua"`); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, Lua`); + }); + + test('Scheme', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, world

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Scheme'); + await waitForEditorFocus(app); + + await page.keyboard.insertText(`(let ((title "Scheme")) + (set-content! "#title" title))`); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('h1'); + + expect(resultText).toContain(`Hello, Scheme`); + }); + + test('Go (Yaegi WebAssembly)', async ({ page, getTestUrl, editor }) => { + test.skip(editor === 'codejar', 'please fix'); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('text="HTML"'); + await waitForEditorFocus(app); + await page.keyboard.insertText('

Hello, world

'); + + await app.click(':nth-match([title="Change Language"], 3)'); + await app.click('text=Go (Wasm)'); + await waitForEditorFocus(app); + + await page.keyboard.insertText(`package main + +import "fmt" + +func main() { + fmt.Println("Hello from Yaegi!") + fmt.Println("Go WebAssembly is working!") +}`); + + await waitForResultUpdate(); + const resultText = await getResult().innerText('body'); + + expect(resultText).toContain(`Hello from Yaegi!`); + expect(resultText).toContain(`Go WebAssembly is working!`); + }); +}); diff --git a/functions/vendors/templates.js b/functions/vendors/templates.js index 10dbce55f7..d5b92aef2c 100644 --- a/functions/vendors/templates.js +++ b/functions/vendors/templates.js @@ -447,10 +447,10 @@ body { font-size: 3.5rem; } } -`.trimStart()},script:{language:"javascript",content:""},stylesheets:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/css/bootstrap.min.css"],scripts:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"],cssPreset:"",imports:{},types:{}};var y={name:"coffeescript",title:getTemplateName("templates.starter.coffeescript","CoffeeScript Starter"),thumbnail:"assets/templates/coffeescript.svg",activeEditor:"script",markup:{language:"html",content:` +`.trimStart()},script:{language:"javascript",content:""},stylesheets:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/css/bootstrap.min.css"],scripts:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"],cssPreset:"",imports:{},types:{}};var y={name:"civet",title:getTemplateName("templates.starter.civet","Civet Starter"),thumbnail:"assets/templates/civet.png",activeEditor:"script",markup:{language:"html",content:`

Hello, World!

- +

You clicked 0 times.

@@ -463,25 +463,25 @@ body { .logo { width: 150px; } -`.trimStart()},script:{language:"coffeescript",content:` -titleElement = document.getElementById 'title' -counterElement = document.getElementById 'counter' -button = document.getElementById 'counter-button' +`.trimStart()},script:{language:"civet",content:` +titleElement := document.getElementById 'title' +counterElement := document.getElementById 'counter' +button := document.getElementById 'counter-button' -title = 'CoffeeScript' +title := 'Civet' titleElement.innerText = title -counter = (count) -> -> count += 1 -increment = counter 0 +counter := (count: number) => => count += 1 +increment := counter 0 +function handleClick: void counterElement.innerText = increment() -button.addEventListener('click', - -> counterElement.innerText = increment()) -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var x={name:"go",title:getTemplateName("templates.starter.go","Go Starter"),thumbnail:"assets/templates/go.svg",activeEditor:"script",markup:{language:"html",content:` +button.addEventListener 'click', handleClick +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var x={name:"clio",title:getTemplateName("templates.starter.clio","Clio Starter"),thumbnail:"assets/templates/clio.png",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- +

Hello, World!

+

You clicked 0 times.

- +
`.trimStart()},style:{language:"css",content:` .container, @@ -490,62 +490,38 @@ button.addEventListener('click', font: 1em sans-serif; } .logo { - width: 250px; + width: 150px; } -`.trimStart()},script:{language:"go",content:` -package main - -import ( - "fmt" - "syscall/js" - "time" -) - -func main() { - title := querySelector("#title") - title.Set("innerHTML", "Golang") +`.trimStart()},script:{language:"clio",content:` +fn capitalize str: + (str.charAt 0 -> .toUpperCase) + (str.slice 1 -> .toLowerCase) - registerCounter() +fn greet name: + f"Hello, {name}!" - // yes, you can use goroutines (check the console) - go greet() - fmt.Println("Hello!") -} +fn setTitle name: + title = document.querySelector "#title" + title.innerText = name -> capitalize -> greet -func querySelector(id string) js.Value { - return js.Global().Get("document").Call("querySelector", id) -} +fn increment value: + (Number value) + 1 -func registerCounter() { - btn := querySelector("#counter-button") - counter := querySelector("#counter") - count := 0 +fn activateBtn btn: + btn.disabled = false + btn.innerText = "Click me" + btn - var cb js.Func - cb = js.FuncOf(func(this js.Value, args []js.Value) interface{} { - count += 1 - counter.Set("innerHTML", count) - return nil - }) - btn.Call("addEventListener", "click", cb) -} +fn onBtnClick: + counter = document.querySelector "#counter" + counter.innerText = increment counter.innerText -func greet() { - if hours, _, _ := time.Now().Clock(); hours < 12 { - fmt.Println("Good morning") - } else if hours < 18 { - fmt.Println("Good afternoon") - } else { - fmt.Println("Good evening") - } -} -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var w={name:"jquery",title:getTemplateName("templates.starter.jquery","jQuery Starter"),thumbnail:"assets/templates/jquery.svg",activeEditor:"script",markup:{language:"html",content:` -
-

Hello, World!

- -

You clicked 0 times.

- -
+export fn main argv: + setTitle "clio" + document.querySelector "#counter-button" + -> activateBtn + -> .addEventListener "click" onBtnClick +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var w={name:"clojurescript",title:getTemplateName("templates.starter.clojurescript","ClojureScript Starter"),thumbnail:"assets/templates/cljs.svg",activeEditor:"script",markup:{language:"html",content:` +
Loading...
`.trimStart()},style:{language:"css",content:` .container, .container button { @@ -553,24 +529,43 @@ func greet() { font: 1em sans-serif; } .logo { - width: 300px; + width: 150px; } -`.trimStart()},script:{language:"javascript",content:` -import $ from "jquery"; +`.trimStart()},script:{language:"clojurescript",content:` +(ns react.component + (:require + ;; you may use npm packages + ["canvas-confetti$default" :as confetti] + ["react$default" :as React] + ["react" :refer [useState]] + ["react-dom/client" :refer [createRoot]])) -$("#title").text('jQuery'); +(defn Counter [^:js {:keys [name]}] + (let [[counter setCount] (useState 0)] + #jsx [:div + {:className "container"} + [:h1 (str "Hello, " name "!")] + [:img + {:className "logo" + :alt "logo" + :src "{{ __livecodes_baseUrl__ }}assets/templates/cljs.svg"}] + [:p "You clicked " counter " times."] + [:button + {:onClick (fn [] + (if (= (mod counter 3) 0) (confetti)) + (setCount (inc counter)))} + "Click me"]])) -let count = 0; -$("#counter-button").click(() => { - count += 1; - $("#counter").text(count); -}); -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var S={name:"knockout",title:getTemplateName("templates.starter.knockout","Knockout Starter"),thumbnail:"assets/templates/knockout.svg",activeEditor:"script",markup:{language:"html",content:` +(def title "ClojureScript") +(print (str "Hello, " title "!")) +(defonce root (createRoot (js/document.querySelector "#app"))) +(.render root #jsx [Counter #js {:name title}]) +`.trimStart()}};var S={name:"coffeescript",title:getTemplateName("templates.starter.coffeescript","CoffeeScript Starter"),thumbnail:"assets/templates/coffeescript.svg",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- -

You clicked 0 times.

- +

Hello, World!

+ +

You clicked 0 times.

+
`.trimStart()},style:{language:"css",content:` .container, @@ -579,27 +574,25 @@ $("#counter-button").click(() => { font: 1em sans-serif; } .logo { - width: 250px; + width: 150px; } -`.trimStart()},script:{language:"javascript",content:` -import ko from "knockout"; +`.trimStart()},script:{language:"coffeescript",content:` +titleElement = document.getElementById 'title' +counterElement = document.getElementById 'counter' +button = document.getElementById 'counter-button' -class ClickCounterViewModel { - constructor() { - this.title = 'Knockout'; - this.numberOfClicks = ko.observable(0); +title = 'CoffeeScript' +titleElement.innerText = title - this.registerClick = function () { - this.numberOfClicks(this.numberOfClicks() + 1); - }; - } -} +counter = (count) -> -> count += 1 +increment = counter 0 -ko.applyBindings(new ClickCounterViewModel()); -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var k={name:"livescript",title:getTemplateName("templates.starter.livescript","LiveScript Starter"),thumbnail:"assets/templates/livescript.svg",activeEditor:"script",markup:{language:"html",content:` +button.addEventListener('click', + -> counterElement.innerText = increment()) +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var k={name:"commonlisp",title:getTemplateName("templates.starter.commonlisp","Common Lisp Starter"),thumbnail:"assets/templates/commonlisp.svg",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- +

Hello, World!

+

You clicked 0 times.

@@ -612,31 +605,70 @@ ko.applyBindings(new ClickCounterViewModel()); .logo { width: 150px; } -`.trimStart()},script:{language:"livescript",content:` -{ capitalize, join, map, words } = require 'prelude-ls' - -title = 'live script' -|> words -|> map capitalize -|> join '' - -(document.getElementById \\title).innerText = title +`.trimStart()},script:{language:"commonlisp",content:` +(defun set-attribute (&key selector attribute value) + (let ((node + (#j:document:querySelector selector))) + (setf (jscl::oget node attribute) value) + node)) -increment = (count) -> -> count += 1 -counter = increment 0 +(let ((title "Common Lisp")) + (set-attribute :selector "#title" :attribute "innerHTML" + :value (format nil "Hello, ~A!" title))) -counter-element = document.getElementById \\counter -button = document.getElementById \\counter-button +(let ((counter 0)) + (set-attribute :selector "#counter-button" :attribute "onclick" + :value #'(lambda (ev) + (setf counter (+ counter 1)) + (set-attribute :selector "#counter" :attribute "innerHTML" + :value counter)))) -button.addEventListener \\click, - -> counter-element.innerText = counter! -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var _={name:"lua",title:getTemplateName("templates.starter.lua","Lua Starter"),thumbnail:"assets/templates/lua.svg",activeEditor:"script",markup:{language:"html",content:` +(#j:console:clear) +(write "Hello, Common Lisp!") +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var _={name:"cpp",title:getTemplateName("templates.starter.cpp","C++ Starter"),thumbnail:"assets/templates/cpp.svg",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- -

You clicked 0 times.

+

Hello, World!

+ +

You clicked 0 times.

+ + +`.trimStart(), + }, + style: { + language: 'css', + content: ` +.container, +.container button { + text-align: center; + font: 1em sans-serif; +} +.logo { + width: 150px; +} +`.trimStart(), + }, + script: { + language: 'go-wasm', + content: ` +package main + +import "fmt" + +func main() { + fmt.Println("Go (Wasm)") + + // we need to read stdin and increment count + fmt.Println("0") +} +`.trimStart(), + }, +}; diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts index 1075d26834..9f8a22eb10 100644 --- a/src/livecodes/templates/starter/index.ts +++ b/src/livecodes/templates/starter/index.ts @@ -21,6 +21,7 @@ import { diagramsStarter } from './diagrams-starter'; import { fennelStarter } from './fennel-starter'; import { gleamStarter } from './gleam-starter'; import { goStarter } from './go-starter'; +import { goWasmStarter } from './go-wasm-starter'; import { imbaStarter } from './imba-starter'; import { javaStarter } from './java-starter'; import { javascriptStarter } from './javascript-starter'; @@ -111,6 +112,7 @@ export const starterTemplates = [ rubyStarter, rubyWasmStarter, goStarter, + goWasmStarter, phpStarter, phpWasmStarter, cppStarter, diff --git a/src/sdk/models.ts b/src/sdk/models.ts index 9acf3171b4..985b8d35fb 100644 --- a/src/sdk/models.ts +++ b/src/sdk/models.ts @@ -1423,6 +1423,7 @@ export type TemplateName = | 'ruby' | 'ruby-wasm' | 'go' + | 'go-wasm' | 'php' | 'php-wasm' | 'cpp' From f05fe7f92a21969a2fe9c80f8e9a7b54ed8984d5 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 4 Sep 2025 01:04:27 +0300 Subject: [PATCH 38/94] fixes --- docs/src/components/LanguageSliders.tsx | 1 + docs/src/components/TemplateList.tsx | 1 + src/livecodes/UI/command-menu-actions.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/src/components/LanguageSliders.tsx b/docs/src/components/LanguageSliders.tsx index e1daff5ac6..960c4933a6 100644 --- a/docs/src/components/LanguageSliders.tsx +++ b/docs/src/components/LanguageSliders.tsx @@ -83,6 +83,7 @@ export default function Sliders() { { name: 'ruby', title: 'Ruby' }, { name: 'ruby-wasm', title: 'Ruby (Wasm)' }, { name: 'go', title: 'Go' }, + { name: 'go-wasm', title: 'Go (Wasm)' }, { name: 'php', title: 'PHP' }, { name: 'php-wasm', title: 'PHP (Wasm)' }, { name: 'cpp', title: 'C++' }, diff --git a/docs/src/components/TemplateList.tsx b/docs/src/components/TemplateList.tsx index 126f377f72..a246d4d47a 100644 --- a/docs/src/components/TemplateList.tsx +++ b/docs/src/components/TemplateList.tsx @@ -42,6 +42,7 @@ const templates = [ { name: 'ruby', title: 'Ruby Starter', thumbnail: 'ruby.svg' }, { name: 'ruby-wasm', title: 'Ruby (Wasm) Starter', thumbnail: 'ruby.svg' }, { name: 'go', title: 'Go Starter', thumbnail: 'go.svg' }, + { name: 'go-wasm', title: 'Go (Wasm) Starter', thumbnail: 'go.svg' }, { name: 'php', title: 'PHP Starter', thumbnail: 'php.svg' }, { name: 'php-wasm', title: 'PHP (Wasm) Starter', thumbnail: 'php.svg' }, { name: 'cpp', title: 'C++ Starter', thumbnail: 'cpp.svg' }, diff --git a/src/livecodes/UI/command-menu-actions.ts b/src/livecodes/UI/command-menu-actions.ts index d42de5ca55..708f5ba1f7 100644 --- a/src/livecodes/UI/command-menu-actions.ts +++ b/src/livecodes/UI/command-menu-actions.ts @@ -298,6 +298,7 @@ export const getCommandMenuActions = ({ 'ruby', 'ruby-wasm', 'go', + 'go-wasm', 'php', 'php-wasm', 'cpp', From 3a78adeda8124c87594576488913a5f13cf1520a Mon Sep 17 00:00:00 2001 From: Muhammad Ayman Date: Wed, 10 Sep 2025 22:34:08 +0300 Subject: [PATCH 39/94] Stdin Buffer Management --- functions/vendors/templates.js | 5191 +++++++++-------- .../languages/go-wasm/lang-go-wasm-script.ts | 6 +- .../templates/starter/go-wasm-starter.ts | 225 +- src/livecodes/vendors.ts | 2 +- 4 files changed, 2816 insertions(+), 2608 deletions(-) diff --git a/functions/vendors/templates.js b/functions/vendors/templates.js index 2d666d2b52..58b26099cf 100644 --- a/functions/vendors/templates.js +++ b/functions/vendors/templates.js @@ -447,10 +447,10 @@ body { font-size: 3.5rem; } } -`.trimStart()},script:{language:"javascript",content:""},stylesheets:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/css/bootstrap.min.css"],scripts:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"],cssPreset:"",imports:{},types:{}};var y={name:"coffeescript",title:getTemplateName("templates.starter.coffeescript","CoffeeScript Starter"),thumbnail:"assets/templates/coffeescript.svg",activeEditor:"script",markup:{language:"html",content:` +`.trimStart()},script:{language:"javascript",content:""},stylesheets:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/css/bootstrap.min.css"],scripts:["{{ __CDN_URL__ }}bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"],cssPreset:"",imports:{},types:{}};var y={name:"civet",title:getTemplateName("templates.starter.civet","Civet Starter"),thumbnail:"assets/templates/civet.png",activeEditor:"script",markup:{language:"html",content:`

Hello, World!

- +

You clicked 0 times.

@@ -463,25 +463,25 @@ body { .logo { width: 150px; } -`.trimStart()},script:{language:"coffeescript",content:` -titleElement = document.getElementById 'title' -counterElement = document.getElementById 'counter' -button = document.getElementById 'counter-button' +`.trimStart()},script:{language:"civet",content:` +titleElement := document.getElementById 'title' +counterElement := document.getElementById 'counter' +button := document.getElementById 'counter-button' -title = 'CoffeeScript' +title := 'Civet' titleElement.innerText = title -counter = (count) -> -> count += 1 -increment = counter 0 +counter := (count: number) => => count += 1 +increment := counter 0 +function handleClick: void counterElement.innerText = increment() -button.addEventListener('click', - -> counterElement.innerText = increment()) -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var x={name:"go",title:getTemplateName("templates.starter.go","Go Starter"),thumbnail:"assets/templates/go.svg",activeEditor:"script",markup:{language:"html",content:` +button.addEventListener 'click', handleClick +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var x={name:"clio",title:getTemplateName("templates.starter.clio","Clio Starter"),thumbnail:"assets/templates/clio.png",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- +

Hello, World!

+

You clicked 0 times.

- +
`.trimStart()},style:{language:"css",content:` .container, @@ -490,62 +490,38 @@ button.addEventListener('click', font: 1em sans-serif; } .logo { - width: 250px; + width: 150px; } -`.trimStart()},script:{language:"go",content:` -package main - -import ( - "fmt" - "syscall/js" - "time" -) - -func main() { - title := querySelector("#title") - title.Set("innerHTML", "Golang") +`.trimStart()},script:{language:"clio",content:` +fn capitalize str: + (str.charAt 0 -> .toUpperCase) + (str.slice 1 -> .toLowerCase) - registerCounter() +fn greet name: + f"Hello, {name}!" - // yes, you can use goroutines (check the console) - go greet() - fmt.Println("Hello!") -} +fn setTitle name: + title = document.querySelector "#title" + title.innerText = name -> capitalize -> greet -func querySelector(id string) js.Value { - return js.Global().Get("document").Call("querySelector", id) -} +fn increment value: + (Number value) + 1 -func registerCounter() { - btn := querySelector("#counter-button") - counter := querySelector("#counter") - count := 0 +fn activateBtn btn: + btn.disabled = false + btn.innerText = "Click me" + btn - var cb js.Func - cb = js.FuncOf(func(this js.Value, args []js.Value) interface{} { - count += 1 - counter.Set("innerHTML", count) - return nil - }) - btn.Call("addEventListener", "click", cb) -} +fn onBtnClick: + counter = document.querySelector "#counter" + counter.innerText = increment counter.innerText -func greet() { - if hours, _, _ := time.Now().Clock(); hours < 12 { - fmt.Println("Good morning") - } else if hours < 18 { - fmt.Println("Good afternoon") - } else { - fmt.Println("Good evening") - } -} -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var w={name:"jquery",title:getTemplateName("templates.starter.jquery","jQuery Starter"),thumbnail:"assets/templates/jquery.svg",activeEditor:"script",markup:{language:"html",content:` -
-

Hello, World!

- -

You clicked 0 times.

- -
+export fn main argv: + setTitle "clio" + document.querySelector "#counter-button" + -> activateBtn + -> .addEventListener "click" onBtnClick +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var w={name:"clojurescript",title:getTemplateName("templates.starter.clojurescript","ClojureScript Starter"),thumbnail:"assets/templates/cljs.svg",activeEditor:"script",markup:{language:"html",content:` +
Loading...
`.trimStart()},style:{language:"css",content:` .container, .container button { @@ -553,24 +529,43 @@ func greet() { font: 1em sans-serif; } .logo { - width: 300px; + width: 150px; } -`.trimStart()},script:{language:"javascript",content:` -import $ from "jquery"; +`.trimStart()},script:{language:"clojurescript",content:` +(ns react.component + (:require + ;; you may use npm packages + ["canvas-confetti$default" :as confetti] + ["react$default" :as React] + ["react" :refer [useState]] + ["react-dom/client" :refer [createRoot]])) -$("#title").text('jQuery'); +(defn Counter [^:js {:keys [name]}] + (let [[counter setCount] (useState 0)] + #jsx [:div + {:className "container"} + [:h1 (str "Hello, " name "!")] + [:img + {:className "logo" + :alt "logo" + :src "{{ __livecodes_baseUrl__ }}assets/templates/cljs.svg"}] + [:p "You clicked " counter " times."] + [:button + {:onClick (fn [] + (if (= (mod counter 3) 0) (confetti)) + (setCount (inc counter)))} + "Click me"]])) -let count = 0; -$("#counter-button").click(() => { - count += 1; - $("#counter").text(count); -}); -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var S={name:"knockout",title:getTemplateName("templates.starter.knockout","Knockout Starter"),thumbnail:"assets/templates/knockout.svg",activeEditor:"script",markup:{language:"html",content:` +(def title "ClojureScript") +(print (str "Hello, " title "!")) +(defonce root (createRoot (js/document.querySelector "#app"))) +(.render root #jsx [Counter #js {:name title}]) +`.trimStart()}};var S={name:"coffeescript",title:getTemplateName("templates.starter.coffeescript","CoffeeScript Starter"),thumbnail:"assets/templates/coffeescript.svg",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- -

You clicked 0 times.

- +

Hello, World!

+ +

You clicked 0 times.

+
`.trimStart()},style:{language:"css",content:` .container, @@ -579,27 +574,25 @@ $("#counter-button").click(() => { font: 1em sans-serif; } .logo { - width: 250px; + width: 150px; } -`.trimStart()},script:{language:"javascript",content:` -import ko from "knockout"; +`.trimStart()},script:{language:"coffeescript",content:` +titleElement = document.getElementById 'title' +counterElement = document.getElementById 'counter' +button = document.getElementById 'counter-button' -class ClickCounterViewModel { - constructor() { - this.title = 'Knockout'; - this.numberOfClicks = ko.observable(0); +title = 'CoffeeScript' +titleElement.innerText = title - this.registerClick = function () { - this.numberOfClicks(this.numberOfClicks() + 1); - }; - } -} +counter = (count) -> -> count += 1 +increment = counter 0 -ko.applyBindings(new ClickCounterViewModel()); -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var k={name:"livescript",title:getTemplateName("templates.starter.livescript","LiveScript Starter"),thumbnail:"assets/templates/livescript.svg",activeEditor:"script",markup:{language:"html",content:` +button.addEventListener('click', + -> counterElement.innerText = increment()) +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var k={name:"commonlisp",title:getTemplateName("templates.starter.commonlisp","Common Lisp Starter"),thumbnail:"assets/templates/commonlisp.svg",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- +

Hello, World!

+

You clicked 0 times.

@@ -612,31 +605,70 @@ ko.applyBindings(new ClickCounterViewModel()); .logo { width: 150px; } -`.trimStart()},script:{language:"livescript",content:` -{ capitalize, join, map, words } = require 'prelude-ls' - -title = 'live script' -|> words -|> map capitalize -|> join '' - -(document.getElementById \\title).innerText = title +`.trimStart()},script:{language:"commonlisp",content:` +(defun set-attribute (&key selector attribute value) + (let ((node + (#j:document:querySelector selector))) + (setf (jscl::oget node attribute) value) + node)) -increment = (count) -> -> count += 1 -counter = increment 0 +(let ((title "Common Lisp")) + (set-attribute :selector "#title" :attribute "innerHTML" + :value (format nil "Hello, ~A!" title))) -counter-element = document.getElementById \\counter -button = document.getElementById \\counter-button +(let ((counter 0)) + (set-attribute :selector "#counter-button" :attribute "onclick" + :value #'(lambda (ev) + (setf counter (+ counter 1)) + (set-attribute :selector "#counter" :attribute "innerHTML" + :value counter)))) -button.addEventListener \\click, - -> counter-element.innerText = counter! -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var _={name:"lua",title:getTemplateName("templates.starter.lua","Lua Starter"),thumbnail:"assets/templates/lua.svg",activeEditor:"script",markup:{language:"html",content:` +(#j:console:clear) +(write "Hello, Common Lisp!") +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var _={name:"cpp",title:getTemplateName("templates.starter.cpp","C++ Starter"),thumbnail:"assets/templates/cpp.svg",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- -

You clicked 0 times.

+

Hello, World!

+ +

You clicked 0 times.

+ + `.trimStart(), @@ -59,13 +104,71 @@ export const goWasmStarter: Template = { style: { language: 'css', content: ` -.container, -.container button { - text-align: center; - font: 1em sans-serif; +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } + .logo { width: 150px; + display: block; + margin: 20px auto; +} + +.demo-section { + background: #f5f5f5; + padding: 20px; + margin: 20px 0; + border-radius: 8px; + border-left: 4px solid #00add8; +} + +.demo-section h2 { + margin-top: 0; + color: #333; +} + +button { + background: #00add8; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + margin: 10px 5px; +} + +button:hover:not(:disabled) { + background: #0099c7; +} + +button:disabled { + background: #ccc; + cursor: not-allowed; +} + +input[type="text"], input[type="number"] { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 16px; + margin: 5px; + width: 200px; +} + +#counter { + font-weight: bold; + color: #00add8; + font-size: 24px; +} + +#greeting, #result { + font-weight: bold; + color: #333; + margin-top: 10px; } `.trimStart(), }, @@ -74,13 +177,53 @@ export const goWasmStarter: Template = { content: ` package main -import "fmt" +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) func main() { - fmt.Println("Go (Wasm)") - - // we need to read stdin and increment count - fmt.Println("0") + // Read input from stdin + scanner := bufio.NewScanner(os.Stdin) + + if scanner.Scan() { + input := strings.TrimSpace(scanner.Text()) + + // Try to parse as number (for counter demo) + if count, err := strconv.Atoi(input); err == nil { + // Counter demo - just return the number + fmt.Println(count) + return + } + + // Try to parse as two numbers separated by newline (for math demo) + if strings.Contains(input, "\\n") { + parts := strings.Split(input, "\\n") + if len(parts) == 2 { + if num1, err1 := strconv.Atoi(parts[0]); err1 == nil { + if num2, err2 := strconv.Atoi(parts[1]); err2 == nil { + // Math demo + sum := num1 + num2 + product := num1 * num2 + fmt.Printf("Sum: %d + %d = %d\\n", num1, num2, sum) + fmt.Printf("Product: %d × %d = %d\\n", num1, num2, product) + return + } + } + } + } + + // Greeting demo - treat as name + fmt.Printf("Hello, %s! Welcome to Go WebAssembly!\\n", input) + fmt.Println("This is running in your browser using Go compiled to WebAssembly.") + } else { + // No input provided + fmt.Println("Hello from Go WebAssembly!") + fmt.Println("This program demonstrates stdin handling in Go WASM.") + } } `.trimStart(), }, diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 451794b133..2207984b32 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -99,7 +99,7 @@ export const cppWasmBaseUrl = /* @__PURE__ */ getUrl('@chriskoch/cpp-wasm@1.0.2/ export const csharpWasmBaseUrl = /* @__PURE__ */ getUrl('@seth0x41/csharp-wasm@1.0.3/'); -export const yaegiWasmBaseUrl = /* @__PURE__ */ getUrl('yaegi-wasm@1.0.1/src/'); +export const yaegiWasmBaseUrl = /* @__PURE__ */ getUrl('yaegi-wasm@1.0.2/src/'); export const csstreeUrl = /* @__PURE__ */ getUrl('css-tree@2.3.1/dist/csstree.js'); From 9208fb847afe864bde28f448634189d08fe7afb8 Mon Sep 17 00:00:00 2001 From: Muhammad Ayman Date: Wed, 10 Sep 2025 22:44:50 +0300 Subject: [PATCH 40/94] JavaScript gets current count from Ui --- src/livecodes/templates/starter/go-wasm-starter.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/livecodes/templates/starter/go-wasm-starter.ts b/src/livecodes/templates/starter/go-wasm-starter.ts index b20701ae72..91dcb1a2a2 100644 --- a/src/livecodes/templates/starter/go-wasm-starter.ts +++ b/src/livecodes/templates/starter/go-wasm-starter.ts @@ -53,9 +53,8 @@ export const goWasmStarter: Template = { // Counter demo incrementBtn.onclick = async () => { const currentCount = parseInt(document.querySelector("#counter").textContent); - const newCount = currentCount + 1; - const {output, error} = await livecodes.goWasm.run(newCount.toString()); + const {output, error} = await livecodes.goWasm.run(currentCount.toString()); if (error) { console.error('Error:', error); } else { @@ -194,8 +193,9 @@ func main() { // Try to parse as number (for counter demo) if count, err := strconv.Atoi(input); err == nil { - // Counter demo - just return the number - fmt.Println(count) + // Counter demo - increment and return the new number + newCount := count + 1 + fmt.Println(newCount) return } From 8e77243b8ed4a1c91a67e08a6787b2d71112ab60 Mon Sep 17 00:00:00 2001 From: Muhammad Ayman Date: Wed, 10 Sep 2025 22:46:22 +0300 Subject: [PATCH 41/94] fix Loading... issue --- src/livecodes/templates/starter/go-wasm-starter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/livecodes/templates/starter/go-wasm-starter.ts b/src/livecodes/templates/starter/go-wasm-starter.ts index 91dcb1a2a2..7bef3c2418 100644 --- a/src/livecodes/templates/starter/go-wasm-starter.ts +++ b/src/livecodes/templates/starter/go-wasm-starter.ts @@ -45,10 +45,13 @@ export const goWasmStarter: Template = { const greetBtn = document.querySelector("#greet-btn"); const calculateBtn = document.querySelector("#calculate-btn"); - // Enable buttons + // Enable buttons and update text incrementBtn.disabled = false; + incrementBtn.textContent = "Increment"; greetBtn.disabled = false; + greetBtn.textContent = "Greet"; calculateBtn.disabled = false; + calculateBtn.textContent = "Calculate"; // Counter demo incrementBtn.onclick = async () => { From 169f23cd37a5624d17274a7484f86d356aa09d27 Mon Sep 17 00:00:00 2001 From: Muhammad Ayman Date: Wed, 10 Sep 2025 22:59:25 +0300 Subject: [PATCH 42/94] finish the starter --- .../templates/starter/go-wasm-starter.ts | 47 +------------------ 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/src/livecodes/templates/starter/go-wasm-starter.ts b/src/livecodes/templates/starter/go-wasm-starter.ts index 7bef3c2418..a280059656 100644 --- a/src/livecodes/templates/starter/go-wasm-starter.ts +++ b/src/livecodes/templates/starter/go-wasm-starter.ts @@ -26,14 +26,7 @@ export const goWasmStarter: Template = {

-
-

Math Operations

-

Enter two numbers:

- - - -

-
+ `.trimStart(), @@ -202,22 +174,7 @@ func main() { return } - // Try to parse as two numbers separated by newline (for math demo) - if strings.Contains(input, "\\n") { - parts := strings.Split(input, "\\n") - if len(parts) == 2 { - if num1, err1 := strconv.Atoi(parts[0]); err1 == nil { - if num2, err2 := strconv.Atoi(parts[1]); err2 == nil { - // Math demo - sum := num1 + num2 - product := num1 * num2 - fmt.Printf("Sum: %d + %d = %d\\n", num1, num2, sum) - fmt.Printf("Product: %d × %d = %d\\n", num1, num2, product) - return - } - } - } - } + // Greeting demo - treat as name fmt.Printf("Hello, %s! Welcome to Go WebAssembly!\\n", input) From b5e57a8fc72a794a968d03fadc2525053d0b36dd Mon Sep 17 00:00:00 2001 From: Muhammad Ayman Date: Thu, 11 Sep 2025 12:44:39 +0300 Subject: [PATCH 43/94] add info --- functions/vendors/templates.js | 205 +++++++++++++----- src/livecodes/html/language-info.html | 8 + .../i18n/locales/en/language-info.ts | 5 + 3 files changed, 167 insertions(+), 51 deletions(-) diff --git a/functions/vendors/templates.js b/functions/vendors/templates.js index 58b26099cf..2bbc7ed68f 100644 --- a/functions/vendors/templates.js +++ b/functions/vendors/templates.js @@ -1029,7 +1029,7 @@ svg .container h3:not(:nth-child(1)) { margin-top: 3em; } -`.trimStart()},script:{language:"javascript",content:""},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var $={name:"fennel",title:getTemplateName("templates.starter.fennel","Fennel Starter"),thumbnail:"assets/templates/fennel.svg",activeEditor:"script",markup:{language:"html",content:` +`.trimStart()},script:{language:"javascript",content:""},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var q={name:"fennel",title:getTemplateName("templates.starter.fennel","Fennel Starter"),thumbnail:"assets/templates/fennel.svg",activeEditor:"script",markup:{language:"html",content:`

Hello, World!

@@ -1063,7 +1063,7 @@ svg (global counter (Counter:new nil)) (global button (document:querySelector "#counter-button")) (button:addEventListener :click (fn [] (counter:increment) (counter:show))) -`.trimStart()}};var L=["esm.sh","skypack","esm.run","jsdelivr.esm","fastly.jsdelivr.esm","gcore.jsdelivr.esm","testingcf.jsdelivr.esm","jsdelivr.b-cdn.esm","jspm"],B=["jsdelivr","fastly.jsdelivr","unpkg","gcore.jsdelivr","testingcf.jsdelivr","jsdelivr.b-cdn","npmcdn"],M=["jsdelivr.gh","fastly.jsdelivr.gh","statically","gcore.jsdelivr.gh","testingcf.jsdelivr.gh","jsdelivr.b-cdn.gh"],r={getModuleUrl:(e,{isModule:s=!0,defaultCDN:i="esm.sh",external:o}={})=>{e=e.replace(/#nobundle/g,"");let a=n=>!o||!n.includes("https://esm.sh")?n:n.includes("?")?`${n}&external=${o}`:`${n}?external=${o}`,l=q(e,s,i);return l?a(l):s?a("https://esm.sh/"+e):"https://cdn.jsdelivr.net/npm/"+e},getUrl:(e,s)=>e.startsWith("http")||e.startsWith("data:")?e:q(e,!1,s||$t())||e,cdnLists:{npm:B,module:L,gh:M},checkCDNs:async(e,s)=>{let i=[s,...r.cdnLists.npm].filter(Boolean);for(let o of i)try{if((await fetch(r.getUrl(e,o),{method:"HEAD"})).ok)return o}catch{}return r.cdnLists.npm[0]}},$t=()=>{if(globalThis.appCDN)return globalThis.appCDN;try{return new URL(location.href).searchParams.get("appCDN")||r.cdnLists.npm[0]}catch{return r.cdnLists.npm[0]}},q=(e,s,i)=>{let o=s&&e.startsWith("unpkg:")?"?module":"";e.startsWith("gh:")?e=e.replace("gh",M[0]):e.includes(":")||(e=(i||(s?L[0]:B[0]))+":"+e);for(let a of qt){let[l,n]=a;if(l.test(e))return e.replace(l,n)+o}return null},qt=[[/^(esm\.sh:)(.+)/i,"https://esm.sh/$2"],[/^(npm:)(.+)/i,"https://esm.sh/$2"],[/^(node:)(.+)/i,"https://esm.sh/$2"],[/^(jsr:)(.+)/i,"https://esm.sh/jsr/$2"],[/^(pr:)(.+)/i,"https://esm.sh/pr/$2"],[/^(pkg\.pr\.new:)(.+)/i,"https://esm.sh/pkg.pr.new/$2"],[/^(skypack:)(.+)/i,"https://cdn.skypack.dev/$2"],[/^(jsdelivr:)(.+)/i,"https://cdn.jsdelivr.net/npm/$2"],[/^(fastly\.jsdelivr:)(.+)/i,"https://fastly.jsdelivr.net/npm/$2"],[/^(gcore\.jsdelivr:)(.+)/i,"https://gcore.jsdelivr.net/npm/$2"],[/^(testingcf\.jsdelivr:)(.+)/i,"https://testingcf.jsdelivr.net/npm/$2"],[/^(jsdelivr\.b-cdn:)(.+)/i,"https://jsdelivr.b-cdn.net/npm/$2"],[/^(jsdelivr\.gh:)(.+)/i,"https://cdn.jsdelivr.net/gh/$2"],[/^(fastly\.jsdelivr\.gh:)(.+)/i,"https://fastly.jsdelivr.net/gh/$2"],[/^(gcore\.jsdelivr\.gh:)(.+)/i,"https://gcore.jsdelivr.net/gh/$2"],[/^(testingcf\.jsdelivr\.gh:)(.+)/i,"https://testingcf.jsdelivr.net/gh/$2"],[/^(jsdelivr\.b-cdn\.gh:)(.+)/i,"https://jsdelivr.b-cdn.net/gh/$2"],[/^(statically:)(.+)/i,"https://cdn.statically.io/gh/$2"],[/^(esm\.run:)(.+)/i,"https://esm.run/$2"],[/^(jsdelivr\.esm:)(.+)/i,"https://cdn.jsdelivr.net/npm/$2/+esm"],[/^(fastly\.jsdelivr\.esm:)(.+)/i,"https://fastly.jsdelivr.net/npm/$2/+esm"],[/^(gcore\.jsdelivr\.esm:)(.+)/i,"https://gcore.jsdelivr.net/npm/$2/+esm"],[/^(testingcf\.jsdelivr\.esm:)(.+)/i,"https://testingcf.jsdelivr.net/npm/$2/+esm"],[/^(jsdelivr\.b-cdn\.esm:)(.+)/i,"https://jsdelivr.b-cdn.net/npm/$2/+esm"],[/^(jspm:)(.+)/i,"https://jspm.dev/$2"],[/^(esbuild:)(.+)/i,"https://esbuild.vercel.app/$2"],[/^(bundle\.run:)(.+)/i,"https://bundle.run/$2"],[/^(unpkg:)(.+)/i,"https://unpkg.com/$2"],[/^(npmcdn:)(.+)/i,"https://npmcdn.com/$2"],[/^(bundlejs:)(.+)/i,"https://deno.bundlejs.com/?file&q=$2"],[/^(bundle:)(.+)/i,"https://deno.bundlejs.com/?file&q=$2"],[/^(deno:)(.+)/i,"https://deno.bundlejs.com/?file&q=https://deno.land/x/$2/mod.ts"],[/^(https:\/\/deno\.land\/.+)/i,"https://deno.bundlejs.com/?file&q=$1"],[/^(github:|https:\/\/github\.com\/)(.[^\/]+?)\/(.[^\/]+?)\/(?!releases\/)(?:(?:blob|raw)\/)?(.+?\/.+)/i,"https://deno.bundlejs.com/?file&q=https://cdn.jsdelivr.net/gh/$2/$3@$4"],[/^(gist\.github:)(.+?\/[0-9a-f]+\/raw\/(?:[0-9a-f]+\/)?.+)$/i,"https://gist.githack.com/$2"],[/^(gitlab:|https:\/\/gitlab\.com\/)([^\/]+.*\/[^\/]+)\/(?:raw|blob)\/(.+?)(?:\?.*)?$/i,"https://deno.bundlejs.com/?file&q=https://gl.githack.com/$2/raw/$3"],[/^(bitbucket:|https:\/\/bitbucket\.org\/)([^\/]+\/[^\/]+)\/(?:raw|src)\/(.+?)(?:\?.*)?$/i,"https://deno.bundlejs.com/?file&q=https://bb.githack.com/$2/raw/$3"],[/^(bitbucket:)snippets\/([^\/]+\/[^\/]+)\/revisions\/([^\/\#\?]+)(?:\?[^#]*)?(?:\#file-(.+?))$/i,"https://bb.githack.com/!api/2.0/snippets/$2/$3/files/$4"],[/^(bitbucket:)snippets\/([^\/]+\/[^\/\#\?]+)(?:\?[^#]*)?(?:\#file-(.+?))$/i,"https://bb.githack.com/!api/2.0/snippets/$2/HEAD/files/$3"],[/^(bitbucket:)\!api\/2.0\/snippets\/([^\/]+\/[^\/]+\/[^\/]+)\/files\/(.+?)(?:\?.*)?$/i,"https://bb.githack.com/!api/2.0/snippets/$2/files/$3"],[/^(api\.bitbucket:)2.0\/snippets\/([^\/]+\/[^\/]+\/[^\/]+)\/files\/(.+?)(?:\?.*)?$/i,"https://bb.githack.com/!api/2.0/snippets/$2/files/$3"],[/^(rawgit:)(.+?\/[0-9a-f]+\/raw\/(?:[0-9a-f]+\/)?.+)$/i,"https://gist.githack.com/$2"],[/^(rawgit:|https:\/\/raw\.githubusercontent\.com)(\/[^\/]+\/[^\/]+|[0-9A-Za-z-]+\/[0-9a-f]+\/raw)\/(.+)/i,"https://deno.bundlejs.com/?file&q=https://raw.githack.com/$2/$3"]];var{getUrl:Lt,getModuleUrl:te}=r;var c=Lt("gh:live-codes/gleam-precompiled@v0.5.0/");var p=c+"build/packages/plinth/src/plinth/",m=c+"build/dev/javascript/plinth/plinth/",P={name:"gleam",title:getTemplateName("templates.starter.gleam","Gleam Starter"),thumbnail:"assets/templates/gleam.svg",activeEditor:"script",markup:{language:"html",content:` +`.trimStart()}};var L=["esm.sh","skypack","esm.run","jsdelivr.esm","fastly.jsdelivr.esm","gcore.jsdelivr.esm","testingcf.jsdelivr.esm","jsdelivr.b-cdn.esm","jspm"],B=["jsdelivr","fastly.jsdelivr","unpkg","gcore.jsdelivr","testingcf.jsdelivr","jsdelivr.b-cdn","npmcdn"],M=["jsdelivr.gh","fastly.jsdelivr.gh","statically","gcore.jsdelivr.gh","testingcf.jsdelivr.gh","jsdelivr.b-cdn.gh"],r={getModuleUrl:(e,{isModule:s=!0,defaultCDN:i="esm.sh",external:o}={})=>{e=e.replace(/#nobundle/g,"");let a=n=>!o||!n.includes("https://esm.sh")?n:n.includes("?")?`${n}&external=${o}`:`${n}?external=${o}`,l=$(e,s,i);return l?a(l):s?a("https://esm.sh/"+e):"https://cdn.jsdelivr.net/npm/"+e},getUrl:(e,s)=>e.startsWith("http")||e.startsWith("data:")?e:$(e,!1,s||qt())||e,cdnLists:{npm:B,module:L,gh:M},checkCDNs:async(e,s)=>{let i=[s,...r.cdnLists.npm].filter(Boolean);for(let o of i)try{if((await fetch(r.getUrl(e,o),{method:"HEAD"})).ok)return o}catch{}return r.cdnLists.npm[0]}},qt=()=>{if(globalThis.appCDN)return globalThis.appCDN;try{return new URL(location.href).searchParams.get("appCDN")||r.cdnLists.npm[0]}catch{return r.cdnLists.npm[0]}},$=(e,s,i)=>{let o=s&&e.startsWith("unpkg:")?"?module":"";e.startsWith("gh:")?e=e.replace("gh",M[0]):e.includes(":")||(e=(i||(s?L[0]:B[0]))+":"+e);for(let a of $t){let[l,n]=a;if(l.test(e))return e.replace(l,n)+o}return null},$t=[[/^(esm\.sh:)(.+)/i,"https://esm.sh/$2"],[/^(npm:)(.+)/i,"https://esm.sh/$2"],[/^(node:)(.+)/i,"https://esm.sh/$2"],[/^(jsr:)(.+)/i,"https://esm.sh/jsr/$2"],[/^(pr:)(.+)/i,"https://esm.sh/pr/$2"],[/^(pkg\.pr\.new:)(.+)/i,"https://esm.sh/pkg.pr.new/$2"],[/^(skypack:)(.+)/i,"https://cdn.skypack.dev/$2"],[/^(jsdelivr:)(.+)/i,"https://cdn.jsdelivr.net/npm/$2"],[/^(fastly\.jsdelivr:)(.+)/i,"https://fastly.jsdelivr.net/npm/$2"],[/^(gcore\.jsdelivr:)(.+)/i,"https://gcore.jsdelivr.net/npm/$2"],[/^(testingcf\.jsdelivr:)(.+)/i,"https://testingcf.jsdelivr.net/npm/$2"],[/^(jsdelivr\.b-cdn:)(.+)/i,"https://jsdelivr.b-cdn.net/npm/$2"],[/^(jsdelivr\.gh:)(.+)/i,"https://cdn.jsdelivr.net/gh/$2"],[/^(fastly\.jsdelivr\.gh:)(.+)/i,"https://fastly.jsdelivr.net/gh/$2"],[/^(gcore\.jsdelivr\.gh:)(.+)/i,"https://gcore.jsdelivr.net/gh/$2"],[/^(testingcf\.jsdelivr\.gh:)(.+)/i,"https://testingcf.jsdelivr.net/gh/$2"],[/^(jsdelivr\.b-cdn\.gh:)(.+)/i,"https://jsdelivr.b-cdn.net/gh/$2"],[/^(statically:)(.+)/i,"https://cdn.statically.io/gh/$2"],[/^(esm\.run:)(.+)/i,"https://esm.run/$2"],[/^(jsdelivr\.esm:)(.+)/i,"https://cdn.jsdelivr.net/npm/$2/+esm"],[/^(fastly\.jsdelivr\.esm:)(.+)/i,"https://fastly.jsdelivr.net/npm/$2/+esm"],[/^(gcore\.jsdelivr\.esm:)(.+)/i,"https://gcore.jsdelivr.net/npm/$2/+esm"],[/^(testingcf\.jsdelivr\.esm:)(.+)/i,"https://testingcf.jsdelivr.net/npm/$2/+esm"],[/^(jsdelivr\.b-cdn\.esm:)(.+)/i,"https://jsdelivr.b-cdn.net/npm/$2/+esm"],[/^(jspm:)(.+)/i,"https://jspm.dev/$2"],[/^(esbuild:)(.+)/i,"https://esbuild.vercel.app/$2"],[/^(bundle\.run:)(.+)/i,"https://bundle.run/$2"],[/^(unpkg:)(.+)/i,"https://unpkg.com/$2"],[/^(npmcdn:)(.+)/i,"https://npmcdn.com/$2"],[/^(bundlejs:)(.+)/i,"https://deno.bundlejs.com/?file&q=$2"],[/^(bundle:)(.+)/i,"https://deno.bundlejs.com/?file&q=$2"],[/^(deno:)(.+)/i,"https://deno.bundlejs.com/?file&q=https://deno.land/x/$2/mod.ts"],[/^(https:\/\/deno\.land\/.+)/i,"https://deno.bundlejs.com/?file&q=$1"],[/^(github:|https:\/\/github\.com\/)(.[^\/]+?)\/(.[^\/]+?)\/(?!releases\/)(?:(?:blob|raw)\/)?(.+?\/.+)/i,"https://deno.bundlejs.com/?file&q=https://cdn.jsdelivr.net/gh/$2/$3@$4"],[/^(gist\.github:)(.+?\/[0-9a-f]+\/raw\/(?:[0-9a-f]+\/)?.+)$/i,"https://gist.githack.com/$2"],[/^(gitlab:|https:\/\/gitlab\.com\/)([^\/]+.*\/[^\/]+)\/(?:raw|blob)\/(.+?)(?:\?.*)?$/i,"https://deno.bundlejs.com/?file&q=https://gl.githack.com/$2/raw/$3"],[/^(bitbucket:|https:\/\/bitbucket\.org\/)([^\/]+\/[^\/]+)\/(?:raw|src)\/(.+?)(?:\?.*)?$/i,"https://deno.bundlejs.com/?file&q=https://bb.githack.com/$2/raw/$3"],[/^(bitbucket:)snippets\/([^\/]+\/[^\/]+)\/revisions\/([^\/\#\?]+)(?:\?[^#]*)?(?:\#file-(.+?))$/i,"https://bb.githack.com/!api/2.0/snippets/$2/$3/files/$4"],[/^(bitbucket:)snippets\/([^\/]+\/[^\/\#\?]+)(?:\?[^#]*)?(?:\#file-(.+?))$/i,"https://bb.githack.com/!api/2.0/snippets/$2/HEAD/files/$3"],[/^(bitbucket:)\!api\/2.0\/snippets\/([^\/]+\/[^\/]+\/[^\/]+)\/files\/(.+?)(?:\?.*)?$/i,"https://bb.githack.com/!api/2.0/snippets/$2/files/$3"],[/^(api\.bitbucket:)2.0\/snippets\/([^\/]+\/[^\/]+\/[^\/]+)\/files\/(.+?)(?:\?.*)?$/i,"https://bb.githack.com/!api/2.0/snippets/$2/files/$3"],[/^(rawgit:)(.+?\/[0-9a-f]+\/raw\/(?:[0-9a-f]+\/)?.+)$/i,"https://gist.githack.com/$2"],[/^(rawgit:|https:\/\/raw\.githubusercontent\.com)(\/[^\/]+\/[^\/]+|[0-9A-Za-z-]+\/[0-9a-f]+\/raw)\/(.+)/i,"https://deno.bundlejs.com/?file&q=https://raw.githack.com/$2/$3"]];var{getUrl:Lt,getModuleUrl:te}=r;var c=Lt("gh:live-codes/gleam-precompiled@v0.5.0/");var p=c+"build/packages/plinth/src/plinth/",m=c+"build/dev/javascript/plinth/plinth/",P={name:"gleam",title:getTemplateName("templates.starter.gleam","Gleam Starter"),thumbnail:"assets/templates/gleam.svg",activeEditor:"script",markup:{language:"html",content:`

Hello, World!

@@ -1131,7 +1131,7 @@ pub fn hello(str: String) -> String // npm module @external(javascript, "npm:cowsay2", "say") pub fn cowsay(str: String) -> String -`.trimStart()},customSettings:{imports:{"my_pkg/greet.js":c+"demo/greet.js"},gleam:{modules:{"plinth/browser/document":{srcUrl:p+"browser/document.gleam",compiledUrl:m+"browser/document.mjs"},"plinth/browser/element":{srcUrl:p+"browser/element.gleam",compiledUrl:m+"browser/element.mjs"},"plinth/browser/event":{srcUrl:p+"browser/event.gleam",compiledUrl:m+"browser/event.mjs"}}}}};var N={name:"go",title:getTemplateName("templates.starter.go","Go Starter"),thumbnail:"assets/templates/go.svg",activeEditor:"script",markup:{language:"html",content:` +`.trimStart()},customSettings:{imports:{"my_pkg/greet.js":c+"demo/greet.js"},gleam:{modules:{"plinth/browser/document":{srcUrl:p+"browser/document.gleam",compiledUrl:m+"browser/document.mjs"},"plinth/browser/element":{srcUrl:p+"browser/element.gleam",compiledUrl:m+"browser/element.mjs"},"plinth/browser/event":{srcUrl:p+"browser/event.gleam",compiledUrl:m+"browser/event.mjs"}}}}};var R={name:"go",title:getTemplateName("templates.starter.go","Go Starter"),thumbnail:"assets/templates/go.svg",activeEditor:"script",markup:{language:"html",content:`

Hello, World!

@@ -1194,72 +1194,175 @@ func greet() { fmt.Println("Good evening") } } -`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var R={name:"go-wasm",title:"C++ (Wasm) Starter",thumbnail:"assets/templates/go.svg",activeEditor:"script",markup:{language:"html",content:` +`.trimStart()},stylesheets:[],scripts:[],cssPreset:"",imports:{},types:{}};var N={name:"go-wasm",title:"Go (Wasm) Starter",thumbnail:"assets/templates/go.svg",activeEditor:"script",markup:{language:"html",content:`
-

Hello, World!

- -

You clicked 0 times.

- +

Go WebAssembly Demo

+ + +
+

Interactive Counter

+

Current count: 0

+ +
+ +
+

Stdin Input Demo

+

Enter your name:

+ + +

+
+ +
`, - ]); + const html = ``; + return tailwind.generateStylesFromContent(addCodeInStyleBlocks(code, html), [html]); }; const tailwind4: CompilerFunction = async (code, { config, options }) => { - const prepareCode = (css: string) => { + const prepareCode = (css: string, html: string) => { let result = replaceStyleImports(css, [/tailwindcss/g]); if (!result.includes('@import')) { result = `@import "tailwindcss";${result}`; } - return result; + return addCodeInStyleBlocks(result, html); }; + const html = ``; - const css = prepareCode(code); + const css = prepareCode(code, html); try { const compiler = await self.tailwindcss.compile(css, { base: '/', diff --git a/src/livecodes/languages/tailwindcss/utils.ts b/src/livecodes/languages/tailwindcss/utils.ts new file mode 100644 index 0000000000..ec39c53713 --- /dev/null +++ b/src/livecodes/languages/tailwindcss/utils.ts @@ -0,0 +1,13 @@ +export const addCodeInStyleBlocks = (css: string, html: string) => { + // from compiler/compile-blocks.ts#compileBlocks + const getBlockPattern = (el: 'style', langAttr = 'lang') => + `(<${el}\\s*)(?:([\\s\\S]*?)${langAttr}\\s*=\\s*["']([A-Za-z0-9 _]*)["'])?((?:[^>]*)>)([\\s\\S]*?)(<\\/${el}>)`; + const pattern = getBlockPattern('style'); + for (const arr of [...html.matchAll(new RegExp(pattern, 'g'))]) { + const content = arr[5]; + if (content?.trim()) { + css += `\n${content}`; + } + } + return css; +}; From 1f0292c199eeb338b797ba84d6f4f195e8bbcbae Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 24 Sep 2025 17:20:30 +0300 Subject: [PATCH 60/94] fix(Config): fix updating editor config --- src/livecodes/core.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index c9d9898714..473b87cdd7 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -99,6 +99,7 @@ import type { CustomEditors, CustomSettings, Editor, + EditorConfig, EditorId, EditorLanguages, EditorOptions, @@ -209,6 +210,7 @@ let compiler: Await>; let formatter: Formatter; let editors: Editors; let customEditors: CustomEditors; +let currentEditorConfig: EditorConfig; let toolsPane: ToolsPane | undefined; export let authService: ReturnType | undefined; let editorLanguages: EditorLanguages | undefined; @@ -527,6 +529,8 @@ const createEditors = async (config: Config) => { const styleEditor = await createEditor(styleOptions); const scriptEditor = await createEditor(scriptOptions); + currentEditorConfig = { ...getEditorConfig(config), ...getFormatterConfig(config) }; + setEditorTitle('markup', markupOptions.language); setEditorTitle('style', styleOptions.language); setEditorTitle('script', scriptOptions.language); @@ -1521,12 +1525,12 @@ const applyConfig = async (newConfig: Partial, reload = false) => { }; const hasEditorConfig = Object.values(editorConfig).some((value) => value != null); if (hasEditorConfig) { - const currentEditorConfig = { - ...getEditorConfig(currentConfig), - ...getFormatterConfig(currentConfig), - }; for (const key in editorConfig) { - if ((editorConfig as any)[key] !== (currentEditorConfig as any)[key]) { + if ( + key in newConfig && + (editorConfig as any)[key] !== (currentEditorConfig as any)[key] && + !key.toLowerCase().includes('theme') + ) { shouldReloadEditors = true; break; } From 79b4a70629c34497bc5cff78e0acacc90e0d5e00 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 25 Sep 2025 00:46:50 +0300 Subject: [PATCH 61/94] fix(SDK): fix `height` in Vue SDK --- src/sdk/vue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdk/vue.ts b/src/sdk/vue.ts index 312b7f98ce..18f767a3b9 100644 --- a/src/sdk/vue.ts +++ b/src/sdk/vue.ts @@ -125,7 +125,7 @@ const LiveCodes: LiveCodesComponent = { 'div', { ref: containerRef, - 'data-height': height, + 'data-height': height.value, }, ctx.slots.default?.() || '', ); From b717efcd10e2c3d7af3a2edf6dfd2fac373f0424 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 27 Sep 2025 01:48:28 +0300 Subject: [PATCH 62/94] fix(Config): fix changing editor config from SDK --- src/livecodes/core.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index 473b87cdd7..565dd27a1d 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -1523,18 +1523,10 @@ const applyConfig = async (newConfig: Partial, reload = false) => { ...getEditorConfig(newConfig as Config), ...getFormatterConfig(newConfig as Config), }; + const hasEditorConfig = Object.values(editorConfig).some((value) => value != null); - if (hasEditorConfig) { - for (const key in editorConfig) { - if ( - key in newConfig && - (editorConfig as any)[key] !== (currentEditorConfig as any)[key] && - !key.toLowerCase().includes('theme') - ) { - shouldReloadEditors = true; - break; - } - } + if (hasEditorConfig && newConfig.editor && newConfig.editor !== currentEditorConfig.editor) { + shouldReloadEditors = true; } if ('configureTailwindcss' in editors.markup) { if (newConfig.processors?.includes('tailwindcss')) { @@ -1550,6 +1542,12 @@ const applyConfig = async (newConfig: Partial, reload = false) => { } if (shouldReloadEditors) { await reloadEditors(combinedConfig); + } else if (hasEditorConfig) { + currentEditorConfig = { + ...getEditorConfig(combinedConfig), + ...getFormatterConfig(combinedConfig), + }; + getAllEditors().forEach((editor) => editor.changeSettings(currentEditorConfig)); } parent.dispatchEvent(new Event(customEvents.ready)); From 69cf99d00f42406cfd48531e10f67799aa879b9c Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 27 Sep 2025 02:04:29 +0300 Subject: [PATCH 63/94] fix --- src/livecodes/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index 565dd27a1d..d852d3985c 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -1524,7 +1524,7 @@ const applyConfig = async (newConfig: Partial, reload = false) => { ...getFormatterConfig(newConfig as Config), }; - const hasEditorConfig = Object.values(editorConfig).some((value) => value != null); + const hasEditorConfig = Object.keys(editorConfig).some((k) => k in newConfig); if (hasEditorConfig && newConfig.editor && newConfig.editor !== currentEditorConfig.editor) { shouldReloadEditors = true; } From f58ca118d9feb1f9032c192795e67f0465da0189 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 27 Sep 2025 03:05:16 +0300 Subject: [PATCH 64/94] fix content-only update from SDK --- src/livecodes/core.ts | 29 +++++++++++------------------ src/livecodes/utils/utils.ts | 22 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index d852d3985c..3a455a72f8 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -1432,8 +1432,8 @@ const loadConfig = async ( changingContent = false; }; -const applyConfig = async (newConfig: Partial, reload = false) => { - const currentConfig = getConfig(); +const applyConfig = async (newConfig: Partial, reload = false, oldConfig?: Config) => { + const currentConfig = oldConfig || getConfig(); const combinedConfig: Config = { ...currentConfig, ...newConfig }; if (reload) { await updateEditors(editors, getConfig()); @@ -1518,16 +1518,20 @@ const applyConfig = async (newConfig: Partial, reload = false) => { }); } - let shouldReloadEditors = false; const editorConfig = { ...getEditorConfig(newConfig as Config), ...getFormatterConfig(newConfig as Config), }; const hasEditorConfig = Object.keys(editorConfig).some((k) => k in newConfig); - if (hasEditorConfig && newConfig.editor && newConfig.editor !== currentEditorConfig.editor) { - shouldReloadEditors = true; - } + let shouldReloadEditors = (() => { + if (newConfig.editor != null && !(newConfig.editor in editors.markup)) return true; + if (newConfig.mode != null) { + if (newConfig.mode !== 'result' && editors.markup.isFake) return true; + if (newConfig.mode !== 'codeblock' && editors.markup.codejar) return true; + } + return false; + })(); if ('configureTailwindcss' in editors.markup) { if (newConfig.processors?.includes('tailwindcss')) { editors.markup.configureTailwindcss?.(true); @@ -5536,14 +5540,6 @@ const createApi = (): API => { const shouldRun = newConfig.mode != null && newConfig.mode !== 'editor' && newConfig.mode !== 'codeblock'; const shouldReloadCompiler = shouldRun && compiler.isFake; - const shouldReloadCodeEditors = (() => { - if (newConfig.editor != null && !(newConfig.editor in editors.markup)) return true; - if (newConfig.mode != null) { - if (newConfig.mode !== 'result' && editors.markup.isFake) return true; - if (newConfig.mode !== 'codeblock' && editors.markup.codejar) return true; - } - return false; - })(); const isContentOnlyChange = compareObjects( newConfig, currentConfig as Record, @@ -5568,10 +5564,7 @@ const createApi = (): API => { if (shouldReloadCompiler) { await reloadCompiler(newAppConfig); } - if (shouldReloadCodeEditors) { - await createEditors(newAppConfig); - } - await applyConfig(newConfig, /* reload = */ true); + await applyConfig(newConfig, /* reload = */ true, currentConfig); const content = getContentConfig(newConfig as Config); const hasContent = Object.values(content).some((value) => value != null); if (hasContent) { diff --git a/src/livecodes/utils/utils.ts b/src/livecodes/utils/utils.ts index b22e84b73a..072d7df2e9 100644 --- a/src/livecodes/utils/utils.ts +++ b/src/livecodes/utils/utils.ts @@ -633,10 +633,24 @@ export const compareObjects = /* @__PURE__ */ ( } else if (!(key in dstObj)) { diff.push(key); } else if (srcObj[key] !== null && typeof srcObj[key] === 'object') { - const objDiff = compareObjects(srcObj[key] as any, dstObj[key] as any).map( - (k) => `${key}.${k}`, - ); - diff.push(...objDiff); + if (Array.isArray(srcObj[key])) { + if (!Array.isArray(dstObj[key])) { + diff.push(key); + } else if (srcObj[key].length !== dstObj[key].length) { + diff.push(key); + } else { + for (let i = 0; i < srcObj[key].length; i++) { + if (srcObj[key][i] !== dstObj[key][i]) { + diff.push(`${key}[${i}]`); + } + } + } + } else { + const objDiff = compareObjects(srcObj[key] as any, dstObj[key] as any).map( + (k) => `${key}.${k}`, + ); + diff.push(...objDiff); + } } else if (srcObj[key] !== dstObj[key]) { diff.push(key); } From 5b0ba071d0cdc2b0631b9d1c6e5188694d526f01 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 27 Sep 2025 03:32:43 +0300 Subject: [PATCH 65/94] fix --- src/livecodes/utils/utils.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/livecodes/utils/utils.ts b/src/livecodes/utils/utils.ts index 072d7df2e9..5ae0cc8929 100644 --- a/src/livecodes/utils/utils.ts +++ b/src/livecodes/utils/utils.ts @@ -623,35 +623,37 @@ export const isFocusable = /* @__PURE__ */ (item: any | null): boolean => { * whose values are different from the destination object. */ export const compareObjects = /* @__PURE__ */ ( - srcObj: Record, - dstObj: Record, + srcObj: Partial>, + dstObj: Partial>, ) => { const diff: string[] = []; for (const key of Object.keys(srcObj)) { - if (typeof srcObj[key] === 'function') { + const srcObjProp = srcObj[key]; + const dstObjProp = dstObj[key]; + if (typeof srcObjProp === 'function') { continue; } else if (!(key in dstObj)) { diff.push(key); - } else if (srcObj[key] !== null && typeof srcObj[key] === 'object') { - if (Array.isArray(srcObj[key])) { - if (!Array.isArray(dstObj[key])) { + } else if (srcObjProp !== null && typeof srcObjProp === 'object') { + if (!dstObjProp || typeof dstObjProp !== 'object') { + diff.push(key); + } else if (Array.isArray(srcObjProp)) { + if (!Array.isArray(dstObjProp)) { diff.push(key); - } else if (srcObj[key].length !== dstObj[key].length) { + } else if (srcObjProp.length !== dstObjProp.length) { diff.push(key); } else { - for (let i = 0; i < srcObj[key].length; i++) { - if (srcObj[key][i] !== dstObj[key][i]) { + for (let i = 0; i < srcObjProp.length; i++) { + if (srcObjProp[i] !== dstObjProp[i]) { diff.push(`${key}[${i}]`); } } } } else { - const objDiff = compareObjects(srcObj[key] as any, dstObj[key] as any).map( - (k) => `${key}.${k}`, - ); + const objDiff = compareObjects(srcObjProp, dstObjProp).map((k) => `${key}.${k}`); diff.push(...objDiff); } - } else if (srcObj[key] !== dstObj[key]) { + } else if (srcObjProp !== dstObjProp) { diff.push(key); } } From f077639c0420a7eb4e4ac5ad1ae5b90547b92a9e Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 27 Sep 2025 19:41:00 +0300 Subject: [PATCH 66/94] fix(Editor): do not show lineNumbers in console editor --- src/livecodes/editor/monaco/monaco.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/livecodes/editor/monaco/monaco.ts b/src/livecodes/editor/monaco/monaco.ts index 1442c170dc..338c985815 100644 --- a/src/livecodes/editor/monaco/monaco.ts +++ b/src/livecodes/editor/monaco/monaco.ts @@ -189,6 +189,7 @@ export const createEditor = async (options: EditorOptions): Promise glyphMargin: true, folding: false, lineDecorationsWidth: 0, + lineNumbers: 'off', lineNumbersMinChars: 0, scrollbar: { vertical: 'auto', From 2d6fa4c77ac449193da9609b49183bbc8dec83cd Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sat, 27 Sep 2025 20:25:53 +0300 Subject: [PATCH 67/94] fix --- src/livecodes/core.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index 3a455a72f8..e4fc99c32c 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -1969,11 +1969,12 @@ const loadSelectedScreen = () => { return false; }; -const getAllEditors = (): CodeEditor[] => [ - ...Object.values(editors), - ...[toolsPane?.console?.getEditor?.()], - ...[toolsPane?.compiled?.getEditor?.()], -]; +const getAllEditors = (): CodeEditor[] => + [ + ...Object.values(editors), + toolsPane?.console?.getEditor?.(), + toolsPane?.compiled?.getEditor?.(), + ].filter((x) => x != null); const setTheme = (theme: Theme, editorTheme: Config['editorTheme']) => { const themes = ['light', 'dark']; From 68796ad819be9f009e5196a9ba0cb60c3930334c Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:44 +0300 Subject: [PATCH 68/94] upgrade node version --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index d78bf0a56c..e35b986d37 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.20.4 +v24.1.0 From 638c27247f7baa5b8deee1b785756187dff6ecf7 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:44 +0300 Subject: [PATCH 69/94] refactor vendors.ts to use `getUrl` & file extensions --- src/livecodes/languages/utils.ts | 5 ++- src/livecodes/types/bundle-types.ts | 4 +- src/livecodes/vendors.ts | 60 +++++++++++++++++------------ 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/livecodes/languages/utils.ts b/src/livecodes/languages/utils.ts index 903ca3aa17..beafe93f45 100644 --- a/src/livecodes/languages/utils.ts +++ b/src/livecodes/languages/utils.ts @@ -1,6 +1,6 @@ import type { Compiler, Config, CustomSettings, Language, Processor } from '../models'; import { getLanguageCustomSettings } from '../utils/utils'; -import { highlightjsUrl } from '../vendors'; +import { vendorsBaseUrl } from '../vendors'; export const getLanguageByAlias = (alias: string = ''): Language | undefined => { if (!alias) return; @@ -97,7 +97,8 @@ export const getCustomSettings = ( export const detectLanguage = async (code: string, languages: Language[]) => { (window as any).HighlightJS = - (window as any).HighlightJS || (await import(highlightjsUrl)).default; + (window as any).HighlightJS || + (await import(vendorsBaseUrl + 'highlight.js/highlight.js')).default; const result = (window as any).HighlightJS.highlightAuto(code, languages); return { language: result.language as Language, diff --git a/src/livecodes/types/bundle-types.ts b/src/livecodes/types/bundle-types.ts index 89da0f9284..bf03eaaa38 100644 --- a/src/livecodes/types/bundle-types.ts +++ b/src/livecodes/types/bundle-types.ts @@ -1,6 +1,6 @@ // based on dts-bundle -import { pathBrowserifyUrl } from '../vendors'; +import { vendorsBaseUrl } from '../vendors'; // const dtsExp = /\.d\.ts$/; const bomOptExp = /^\uFEFF?/; @@ -62,7 +62,7 @@ export interface BundleResult { } export async function bundle(options: Options): Promise { - const path = await import(pathBrowserifyUrl); + const path = (await import(vendorsBaseUrl + 'path-browserify/path-browserify.js')).default; assert(typeof options === 'object' && options, 'options must be an object'); // option parsing & validation diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 2207984b32..fc626eda32 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -1,9 +1,15 @@ import { modulesService } from './services/modules'; -const { getUrl, getModuleUrl } = modulesService; +// - only use `getUrl` or full URL (not `getModuleUrl`) +// - always add full version and file extension +// - minimize usage of baseUrls if possible +// - if es module imports others, use baseUrl instead +// - after `vendorBaseUrl` the file is sorted alphabetically -export const vendorsBaseUrl = // 'http://127.0.0.1:8081/'; - /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.3/dist/'); +const { getUrl } = modulesService; + +export const vendorsBaseUrl = 'http://127.0.0.1:8081/'; +// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.3/dist/'); export const acornUrl = /* @__PURE__ */ getUrl('acorn@8.12.1/dist/acorn.js'); @@ -41,7 +47,7 @@ export const browserJestUrl = /* @__PURE__ */ getUrl( export const brythonBaseUrl = /* @__PURE__ */ getUrl('brython@3.12.4/'); -export const chaiUrl = /* @__PURE__ */ getModuleUrl('chai@5.1.2'); +export const chaiUrl = /* @__PURE__ */ getUrl('chai@5.2.1/chai.js'); export const cherryCljsBaseUrl = /* @__PURE__ */ getUrl('cherry-cljs@0.2.19/'); @@ -144,7 +150,7 @@ export const fontCascadiaCodeUrl = /* @__PURE__ */ getUrl( ); export const fontCodeNewRomanUrl = /* @__PURE__ */ getUrl( - 'https://fonts.cdnfonts.com/css/code-new-roman-2', + 'https://fonts.cdnfonts.com/css/code-new-roman-2?style.css', ); export const fontComicMonoUrl = /* @__PURE__ */ getUrl('comic-mono@0.0.1/index.css'); @@ -154,7 +160,7 @@ export const fontCourierPrimeUrl = /* @__PURE__ */ getUrl( ); export const fontDECTerminalModernUrl = /* @__PURE__ */ getUrl( - 'https://fonts.cdnfonts.com/css/dec-terminal-modern', + 'https://fonts.cdnfonts.com/css/dec-terminal-modern?style.css', ); export const fontDejaVuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/dejavu-mono@4.5.4/index.css'); @@ -165,22 +171,24 @@ export const fontFantasqueUrl = /* @__PURE__ */ getUrl( export const fontFiraCodeUrl = /* @__PURE__ */ getUrl('firacode@6.2.0/distr/fira_code.css'); -export const fontFixedsysUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/fixedsys-62'); +export const fontFixedsysUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/fixedsys-62?style.css', +); export const fontHackUrl = /* @__PURE__ */ getUrl('hack-font@3.3.0/build/web/hack.css'); export const fontHermitUrl = /* @__PURE__ */ getUrl('typeface-hermit@0.0.44/index.css'); export const fontIBMPlexMonoUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap', + 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap&style.css', ); export const fontInconsolataUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=Inconsolata&display=swap', + 'https://fonts.googleapis.com/css2?family=Inconsolata&display=swap&style.css', ); export const fontInterUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css?family=Inter:300,400,500', + 'https://fonts.googleapis.com/css?family=Inter:300,400,500&style.css', ); export const fontIosevkaUrl = /* @__PURE__ */ getUrl('@fontsource/iosevka@4.5.4/index.css'); @@ -190,23 +198,27 @@ export const fontJetbrainsMonoUrl = /* @__PURE__ */ getUrl( ); export const fontMaterialIconsUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css?family=Material+Icons&display=swap', + 'https://fonts.googleapis.com/css?family=Material+Icons&display=swap&style.css', ); -export const fontMenloUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/menlo'); +export const fontMenloUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/menlo?style.css', +); export const fontMonaspaceBaseUrl = /* @__PURE__ */ getUrl('monaspace-font@0.0.2/'); -export const fontMonofurUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/monofur'); +export const fontMonofurUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/monofur?style.css', +); export const fontMonoidUrl = /* @__PURE__ */ getUrl('@typopro/web-monoid@3.7.5/TypoPRO-Monoid.css'); export const fontNotoUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap', + 'https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap&style.css', ); export const fontNovaMonoUrl = /* @__PURE__ */ getUrl( - 'https://fonts.googleapis.com/css2?family=Nova+Mono&display=swap', + 'https://fonts.googleapis.com/css2?family=Nova+Mono&display=swap&style.css', ); export const fontOpenDyslexicUrl = /* @__PURE__ */ getUrl( @@ -214,12 +226,14 @@ export const fontOpenDyslexicUrl = /* @__PURE__ */ getUrl( ); export const fontProFontWindowsUrl = /* @__PURE__ */ getUrl( - 'https://fonts.cdnfonts.com/css/profontwindows', + 'https://fonts.cdnfonts.com/css/profontwindows?style.css', ); export const fontRobotoMonoUrl = /* @__PURE__ */ getUrl('@fontsource/roboto-mono@4.5.8/index.css'); -export const fontSFMonoUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/sf-mono'); +export const fontSFMonoUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/sf-mono?style.css', +); export const fontSourceCodeProUrl = /* @__PURE__ */ getUrl( '@fontsource/source-code-pro@4.5.12/index.css', @@ -227,7 +241,9 @@ export const fontSourceCodeProUrl = /* @__PURE__ */ getUrl( export const fontSpaceMonoUrl = /* @__PURE__ */ getUrl('@fontsource/space-mono@4.5.10/index.css'); -export const fontSudoVarUrl = /* @__PURE__ */ getUrl('https://fonts.cdnfonts.com/css/sudo-var'); +export const fontSudoVarUrl = /* @__PURE__ */ getUrl( + 'https://fonts.cdnfonts.com/css/sudo-var?style.css', +); export const fontUbuntuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/ubuntu-mono@4.5.11/index.css'); @@ -247,8 +263,6 @@ export const graphreCdnUrl = /* @__PURE__ */ getUrl('graphre@0.1.3/dist/graphre. export const handlebarsBaseUrl = /* @__PURE__ */ getUrl('handlebars@4.7.8/dist/'); -export const highlightjsUrl = /* @__PURE__ */ getModuleUrl('highlight.js@11.11.1'); - export const hpccJsCdnUrl = /* @__PURE__ */ getUrl('@hpcc-js/wasm@2.13.0/dist/index.js'); export const htmlToImageUrl = /* @__PURE__ */ getUrl('html-to-image@1.11.11/dist/html-to-image.js'); @@ -321,8 +335,6 @@ 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 pgliteUrl = /* @__PURE__ */ getUrl('@electric-sql/pglite@0.1.5/dist/index.js'); export const pintoraUrl = /* @__PURE__ */ getUrl( @@ -447,9 +459,9 @@ export const vegaCdnUrl = /* @__PURE__ */ getUrl('vega@5.25.0/build/vega.js'); export const vegaLiteCdnUrl = /* @__PURE__ */ getUrl('vega-lite@5.9.3/build/vega-lite.js'); -export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3'); +export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3.5.17/dist/vue.global.js'); -export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2'); +export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2.7.16/dist/vue.js'); export const vueRuntimeUrl = /* @__PURE__ */ getUrl('vue@3/dist/vue.runtime.esm-browser.prod.js'); From 3247d667de05aad7d1c3ceda951c1434166637fd Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 70/94] optimize vendor urls for download --- .../diagrams/lang-diagrams-compiler-esm.ts | 6 ++-- .../lang-postgresql-compiler-esm.ts | 4 +-- .../rescript/lang-rescript-compiler-esm.ts | 12 +++---- .../rescript/lang-rescript-formatter.ts | 4 +-- src/livecodes/vendors.ts | 33 ++++++++++++------- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts b/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts index ff45f7120b..d27d3da78d 100644 --- a/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts +++ b/src/livecodes/languages/diagrams/lang-diagrams-compiler-esm.ts @@ -16,7 +16,7 @@ import { cytoscapeUrl, elkjsBaseUrl, graphreCdnUrl, - hpccJsCdnUrl, + hpccJsCdnBaseUrl, mermaidCdnUrl, nomnomlCdnUrl, pintoraUrl, @@ -180,7 +180,7 @@ const compileGnuplot = async (code: string) => { const compileMermaid = async (code: string) => { let mermaid: any; const load = async () => { - mermaid = (await import(mermaidCdnUrl)).default; + mermaid = await loadScript(mermaidCdnUrl, 'mermaid'); mermaid.initialize({ startOnLoad: false, }); @@ -201,7 +201,7 @@ const compileMermaid = async (code: string) => { const compileGraphviz = async (code: string) => { let graphviz: any; const load = async () => { - const hpccWasm = await import(hpccJsCdnUrl); + const hpccWasm = await import(hpccJsCdnBaseUrl + 'index.js'); graphviz = await hpccWasm.Graphviz.load(); }; const render = (src: string, script: HTMLScriptElement) => { diff --git a/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts b/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts index aacc74f089..baa85b387b 100644 --- a/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts +++ b/src/livecodes/languages/postgresql/lang-postgresql-compiler-esm.ts @@ -1,6 +1,6 @@ import type { CompilerFunction } from '../../models'; import { getLanguageCustomSettings, safeName } from '../../utils/utils'; -import { pgliteUrl } from '../../vendors'; +import { pgliteBaseUrl } from '../../vendors'; declare global { interface Window { @@ -11,7 +11,7 @@ declare global { export const pgSqlCompiler: CompilerFunction = async (code, { config }) => { if (!code.trim()) return '{ data: [] }'; - window.PGlite = window.PGlite || (await import(pgliteUrl)).PGlite; + window.PGlite = window.PGlite || (await import(pgliteBaseUrl + 'index.js')).PGlite; const options = getLanguageCustomSettings('pgsql', config); const { dbName, scriptURLs, ...pgliteOptions } = options; diff --git a/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts b/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts index c12bf5243d..53170e41de 100644 --- a/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts +++ b/src/livecodes/languages/rescript/lang-rescript-compiler-esm.ts @@ -6,7 +6,10 @@ import { reasonReactUrl, reasonStdLibBaseUrl, requireUrl, - rescriptCdnBaseUrl, + rescriptCdnUrl1, + rescriptCdnUrl2, + rescriptCdnUrl3, + rescriptCdnUrl4, rescriptStdLibBaseUrl, } from '../../vendors'; @@ -57,12 +60,7 @@ const loadCompiler = async (language: Language) => { ); } else { window.require( - [ - rescriptCdnBaseUrl + 'compiler.js', - rescriptCdnBaseUrl + 'compiler-builtins/cmij.js', - rescriptCdnBaseUrl + '%40rescript/react/cmij.js', - rescriptCdnBaseUrl + '%40rescript/core/cmij.js', - ], + [rescriptCdnUrl1, rescriptCdnUrl2, rescriptCdnUrl3, rescriptCdnUrl4], () => { window.rescript_ocaml_compiler = window.rescript_compiler; window.rescript_compiler = undefined; diff --git a/src/livecodes/languages/rescript/lang-rescript-formatter.ts b/src/livecodes/languages/rescript/lang-rescript-formatter.ts index 6851d6cb7d..4036a7d642 100644 --- a/src/livecodes/languages/rescript/lang-rescript-formatter.ts +++ b/src/livecodes/languages/rescript/lang-rescript-formatter.ts @@ -1,12 +1,12 @@ import type { LanguageFormatter } from '../../models'; import { getAbsoluteUrl } from '../../utils'; -import { rescriptCdnBaseUrl } from '../../vendors'; +import { rescriptCdnUrl1 } from '../../vendors'; declare const importScripts: any; const createRescriptFormatter: LanguageFormatter['factory'] = (baseUrl, language) => { if (!(self as any).rescript_compiler) { - importScripts(getAbsoluteUrl(rescriptCdnBaseUrl + 'compiler.js', baseUrl)); + importScripts(getAbsoluteUrl(rescriptCdnUrl1, baseUrl)); } const compiler = (self as any).rescript_compiler.make(); compiler.setModuleSystem('es6'); diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index fc626eda32..23b5e91860 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -9,7 +9,7 @@ import { modulesService } from './services/modules'; const { getUrl } = modulesService; export const vendorsBaseUrl = 'http://127.0.0.1:8081/'; -// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.3/dist/'); +// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.4/dist/'); export const acornUrl = /* @__PURE__ */ getUrl('acorn@8.12.1/dist/acorn.js'); @@ -263,7 +263,7 @@ export const graphreCdnUrl = /* @__PURE__ */ getUrl('graphre@0.1.3/dist/graphre. export const handlebarsBaseUrl = /* @__PURE__ */ getUrl('handlebars@4.7.8/dist/'); -export const hpccJsCdnUrl = /* @__PURE__ */ getUrl('@hpcc-js/wasm@2.13.0/dist/index.js'); +export const hpccJsCdnBaseUrl = /* @__PURE__ */ getUrl('@hpcc-js/wasm@2.13.0/dist/'); export const htmlToImageUrl = /* @__PURE__ */ getUrl('html-to-image@1.11.11/dist/html-to-image.js'); @@ -299,11 +299,11 @@ export const lunaObjViewerStylesUrl = /* @__PURE__ */ getUrl( 'luna-object-viewer@0.2.4/luna-object-viewer.css', ); -export const malinaBaseUrl = /* @__PURE__ */ getUrl(`malinajs@0.7.19/`); +export const malinaBaseUrl = /* @__PURE__ */ getUrl('malinajs@0.7.19/'); export const markedUrl = /* @__PURE__ */ getUrl('marked@13.0.2/marked.min.js'); -export const mermaidCdnUrl = /* @__PURE__ */ getUrl('mermaid@10.2.2/dist/mermaid.esm.mjs'); +export const mermaidCdnUrl = /* @__PURE__ */ getUrl('mermaid@10.2.2/dist/mermaid.min.js'); export const metaPngUrl = /* @__PURE__ */ getUrl('meta-png@1.0.6/dist/meta-png.umd.js'); @@ -335,7 +335,7 @@ export const opalBaseUrl = /* @__PURE__ */ getUrl('https://cdn.opalrb.com/opal/1 export const parinferUrl = /* @__PURE__ */ getUrl('parinfer@3.13.1/parinfer.js'); -export const pgliteUrl = /* @__PURE__ */ getUrl('@electric-sql/pglite@0.1.5/dist/index.js'); +export const pgliteBaseUrl = /* @__PURE__ */ getUrl('@electric-sql/pglite@0.1.5/dist/'); export const pintoraUrl = /* @__PURE__ */ getUrl( '@pintora/standalone@0.6.2/lib/pintora-standalone.umd.js', @@ -395,7 +395,18 @@ export const reasonReactUrl = /* @__PURE__ */ getUrl( export const reasonStdLibBaseUrl = /* @__PURE__ */ getUrl('@rescript/std@9.1.3/lib/es6/'); -export const rescriptCdnBaseUrl = /* @__PURE__ */ getUrl('https://cdn.rescript-lang.org/v11.1.2/'); +export const rescriptCdnUrl1 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/compiler.js', +); +export const rescriptCdnUrl2 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/compiler-builtins/cmij.js', +); +export const rescriptCdnUrl3 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/%40rescript/react/cmij.js', +); +export const rescriptCdnUrl4 = /* @__PURE__ */ getUrl( + 'https://cdn.rescript-lang.org/v11.1.2/%40rescript/core/cmij.js', +); export const rescriptStdLibBaseUrl = /* @__PURE__ */ getUrl('@rescript/std@11.1.2/lib/es6/'); @@ -449,19 +460,19 @@ export const tesseractUrl = /* @__PURE__ */ getUrl('tesseract.js@6.0.1/dist/tess export const twigUrl = /* @__PURE__ */ getUrl('twig@1.17.1/twig.min.js'); -export const typescriptUrl = /* @__PURE__ */ getUrl(`typescript@5.6.2/lib/typescript.js`); +export const typescriptUrl = /* @__PURE__ */ getUrl('typescript@5.6.2/lib/typescript.js'); export const typescriptVfsUrl = /* @__PURE__ */ getUrl('@typescript/vfs@1.5.3/dist/vfs.esm.js'); export const uniterUrl = /* @__PURE__ */ getUrl('uniter@2.18.0/dist/uniter.js'); -export const vegaCdnUrl = /* @__PURE__ */ getUrl('vega@5.25.0/build/vega.js'); +export const vegaCdnUrl = /* @__PURE__ */ getUrl('vega@5.25.0/build/vega.min.js'); -export const vegaLiteCdnUrl = /* @__PURE__ */ getUrl('vega-lite@5.9.3/build/vega-lite.js'); +export const vegaLiteCdnUrl = /* @__PURE__ */ getUrl('vega-lite@5.9.3/build/vega-lite.min.js'); -export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3.5.17/dist/vue.global.js'); +export const vue3CdnUrl = /* @__PURE__ */ getUrl('vue@3.5.17/dist/vue.global.prod.js'); -export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2.7.16/dist/vue.js'); +export const vue2CdnUrl = /* @__PURE__ */ getUrl('vue@2.7.16/dist/vue.min.js'); export const vueRuntimeUrl = /* @__PURE__ */ getUrl('vue@3/dist/vue.runtime.esm-browser.prod.js'); From 21f79cab5e2c8b39097c6e0f665290b8b1ddae1a Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 71/94] change fonts to use BaseUrl --- src/livecodes/editor/fonts.ts | 76 +++++++++---------- .../python-wasm/lang-python-wasm-script.ts | 4 +- src/livecodes/vendors.ts | 56 ++++++-------- 3 files changed, 64 insertions(+), 72 deletions(-) diff --git a/src/livecodes/editor/fonts.ts b/src/livecodes/editor/fonts.ts index d5bdd7f612..d161468621 100644 --- a/src/livecodes/editor/fonts.ts +++ b/src/livecodes/editor/fonts.ts @@ -1,36 +1,36 @@ import { - fontAnonymousProUrl, - fontAstigmataUrl, - fontCascadiaCodeUrl, + fontAnonymousProBaseUrl, + fontAstigmataBaseUrl, + fontCascadiaCodeBaseUrl, fontCodeNewRomanUrl, - fontComicMonoUrl, - fontCourierPrimeUrl, + fontComicMonoBaseUrl, + fontCourierPrimeBaseUrl, fontDECTerminalModernUrl, - fontDejaVuMonoUrl, - fontFantasqueUrl, - fontFiraCodeUrl, + fontDejaVuMonoBaseUrl, + fontFantasqueBaseUrl, + fontFiraCodeBaseUrl, fontFixedsysUrl, - fontHackUrl, - fontHermitUrl, + fontHackBaseUrl, + fontHermitBaseUrl, fontIBMPlexMonoUrl, fontInconsolataUrl, - fontIosevkaUrl, - fontJetbrainsMonoUrl, + fontIosevkaBaseUrl, + fontJetbrainsMonoBaseUrl, fontMenloUrl, fontMonaspaceBaseUrl, fontMonofurUrl, - fontMonoidUrl, + fontMonoidBaseUrl, fontNotoUrl, fontNovaMonoUrl, - fontOpenDyslexicUrl, + fontOpenDyslexicBaseUrl, fontProFontWindowsUrl, - fontRobotoMonoUrl, + fontRobotoMonoBaseUrl, fontSFMonoUrl, - fontSourceCodeProUrl, - fontSpaceMonoUrl, + fontSourceCodeProBaseUrl, + fontSpaceMonoBaseUrl, fontSudoVarUrl, - fontUbuntuMonoUrl, - fontVictorMonoUrl, + fontUbuntuMonoBaseUrl, + fontVictorMonoBaseUrl, } from '../vendors'; export interface Font { @@ -44,17 +44,17 @@ export const fonts: Font[] = [ { id: 'anonymous-pro', name: 'Anonymous Pro', - url: fontAnonymousProUrl, + url: fontAnonymousProBaseUrl + 'index.css', }, { id: 'astigmata', name: 'Astigmata', - url: fontAstigmataUrl, + url: fontAstigmataBaseUrl + 'index.css', }, { id: 'cascadia-code', name: 'Cascadia Code', - url: fontCascadiaCodeUrl, + url: fontCascadiaCodeBaseUrl + 'index.css', }, { id: 'comic-mono', @@ -64,12 +64,12 @@ export const fonts: Font[] = [ { id: 'comic-mono', name: 'Comic Mono', - url: fontComicMonoUrl, + url: fontComicMonoBaseUrl + 'index.css', }, { id: 'courier-prime', name: 'Courier Prime', - url: fontCourierPrimeUrl, + url: fontCourierPrimeBaseUrl + 'index.css', }, { id: 'dec-terminal-modern', @@ -79,18 +79,18 @@ export const fonts: Font[] = [ { id: 'dejavu-mono', name: 'DejaVu Mono', - url: fontDejaVuMonoUrl, + url: fontDejaVuMonoBaseUrl + 'index.css', }, { id: 'fantasque-sans-mono', name: 'TypoPRO Fantasque Sans Mono', label: 'Fantasque Sans Mono', - url: fontFantasqueUrl, + url: fontFantasqueBaseUrl + 'TypoPRO-FantasqueSansMono.css', }, { id: 'fira-code', name: 'Fira Code', - url: fontFiraCodeUrl, + url: fontFiraCodeBaseUrl + 'fira_code.css', }, { id: 'fixedsys', @@ -101,12 +101,12 @@ export const fonts: Font[] = [ { id: 'hack', name: 'Hack', - url: fontHackUrl, + url: fontHackBaseUrl + 'hack.css', }, { id: 'hermit', name: 'Hermit', - url: fontHermitUrl, + url: fontHermitBaseUrl + 'index.css', }, { id: 'ibm-plex-mono', @@ -121,12 +121,12 @@ export const fonts: Font[] = [ { id: 'iosevka', name: 'Iosevka', - url: fontIosevkaUrl, + url: fontIosevkaBaseUrl + 'index.css', }, { id: 'jetbrains-mono', name: 'JetBrains Mono', - url: fontJetbrainsMonoUrl, + url: fontJetbrainsMonoBaseUrl + 'index.css', }, { id: 'menlo', @@ -167,7 +167,7 @@ export const fonts: Font[] = [ id: 'monoid', name: 'TypoPRO Monoid', label: 'Monoid', - url: fontMonoidUrl, + url: fontMonoidBaseUrl + 'TypoPRO-Monoid.css', }, { id: 'noto-sans-mono', @@ -182,7 +182,7 @@ export const fonts: Font[] = [ { id: 'opendyslexic', name: 'OpenDyslexic', - url: fontOpenDyslexicUrl, + url: fontOpenDyslexicBaseUrl + 'index.css', }, { id: 'profontwindows', @@ -193,7 +193,7 @@ export const fonts: Font[] = [ { id: 'roboto-mono', name: 'Roboto Mono', - url: fontRobotoMonoUrl, + url: fontRobotoMonoBaseUrl + 'index.css', }, { id: 'sf-mono', @@ -203,12 +203,12 @@ export const fonts: Font[] = [ { id: 'source-code-pro', name: 'Source Code Pro', - url: fontSourceCodeProUrl, + url: fontSourceCodeProBaseUrl + 'index.css', }, { id: 'space-mono', name: 'Space Mono', - url: fontSpaceMonoUrl, + url: fontSpaceMonoBaseUrl + 'index.css', }, { id: 'sudo-var', @@ -218,12 +218,12 @@ export const fonts: Font[] = [ { id: 'ubuntu-mono', name: 'Ubuntu Mono', - url: fontUbuntuMonoUrl, + url: fontUbuntuMonoBaseUrl + 'index.css', }, { id: 'victor-mono', name: 'Victor Mono', - url: fontVictorMonoUrl, + url: fontVictorMonoBaseUrl + 'index.css', }, ]; diff --git a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts index e0ec66b066..1c21931348 100644 --- a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts +++ b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { fontAwesomeUrl, pyodideBaseUrl } from '../../vendors'; +import { fontAwesomeBaseUrl, pyodideBaseUrl } from '../../vendors'; declare const loadPyodide: any; @@ -53,7 +53,7 @@ window.addEventListener('load', async () => { // needed for matplotlib icons const stylesheet = document.createElement('link'); stylesheet.rel = 'stylesheet'; - stylesheet.href = fontAwesomeUrl; + stylesheet.href = fontAwesomeBaseUrl + 'css/font-awesome.min.css'; document.head.append(stylesheet); await pyodideReady; diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 23b5e91860..a91bbc0794 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -135,49 +135,43 @@ export const fflateUrl = /* @__PURE__ */ getUrl('fflate@0.8.1/esm/browser.js'); export const flexSearchUrl = /* @__PURE__ */ getUrl('flexsearch@0.7.21/dist/flexsearch.bundle.js'); -export const fontAnonymousProUrl = /* @__PURE__ */ getUrl( - '@fontsource/anonymous-pro@4.5.9/index.css', -); +export const fontAnonymousProBaseUrl = /* @__PURE__ */ getUrl('@fontsource/anonymous-pro@4.5.9/'); -export const fontAstigmataUrl = /* @__PURE__ */ getUrl( - 'gh:hatemhosny/astigmata-font@6d0ee00a07fb1932902f0b81a504d075d47bd52f/index.css', +export const fontAstigmataBaseUrl = /* @__PURE__ */ getUrl( + 'gh:hatemhosny/astigmata-font@6d0ee00a07fb1932902f0b81a504d075d47bd52f/', ); -export const fontAwesomeUrl = /* @__PURE__ */ getUrl('font-awesome@4.7.0/css/font-awesome.min.css'); +export const fontAwesomeBaseUrl = /* @__PURE__ */ getUrl('font-awesome@4.7.0/'); -export const fontCascadiaCodeUrl = /* @__PURE__ */ getUrl( - '@fontsource/cascadia-code@4.2.1/index.css', -); +export const fontCascadiaCodeBaseUrl = /* @__PURE__ */ getUrl('@fontsource/cascadia-code@4.2.1/'); export const fontCodeNewRomanUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/code-new-roman-2?style.css', ); -export const fontComicMonoUrl = /* @__PURE__ */ getUrl('comic-mono@0.0.1/index.css'); +export const fontComicMonoBaseUrl = /* @__PURE__ */ getUrl('comic-mono@0.0.1/'); -export const fontCourierPrimeUrl = /* @__PURE__ */ getUrl( - '@fontsource/courier-prime@4.5.9/index.css', -); +export const fontCourierPrimeBaseUrl = /* @__PURE__ */ getUrl('@fontsource/courier-prime@4.5.9/'); export const fontDECTerminalModernUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/dec-terminal-modern?style.css', ); -export const fontDejaVuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/dejavu-mono@4.5.4/index.css'); +export const fontDejaVuMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/dejavu-mono@4.5.4/'); -export const fontFantasqueUrl = /* @__PURE__ */ getUrl( - '@typopro/web-fantasque-sans-mono@3.7.5/TypoPRO-FantasqueSansMono.css', +export const fontFantasqueBaseUrl = /* @__PURE__ */ getUrl( + '@typopro/web-fantasque-sans-mono@3.7.5/', ); -export const fontFiraCodeUrl = /* @__PURE__ */ getUrl('firacode@6.2.0/distr/fira_code.css'); +export const fontFiraCodeBaseUrl = /* @__PURE__ */ getUrl('firacode@6.2.0/distr/'); export const fontFixedsysUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/fixedsys-62?style.css', ); -export const fontHackUrl = /* @__PURE__ */ getUrl('hack-font@3.3.0/build/web/hack.css'); +export const fontHackBaseUrl = /* @__PURE__ */ getUrl('hack-font@3.3.0/build/web/'); -export const fontHermitUrl = /* @__PURE__ */ getUrl('typeface-hermit@0.0.44/index.css'); +export const fontHermitBaseUrl = /* @__PURE__ */ getUrl('typeface-hermit@0.0.44/'); export const fontIBMPlexMonoUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap&style.css', @@ -191,10 +185,10 @@ export const fontInterUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css?family=Inter:300,400,500&style.css', ); -export const fontIosevkaUrl = /* @__PURE__ */ getUrl('@fontsource/iosevka@4.5.4/index.css'); +export const fontIosevkaBaseUrl = /* @__PURE__ */ getUrl('@fontsource/iosevka@4.5.4/'); -export const fontJetbrainsMonoUrl = /* @__PURE__ */ getUrl( - '@fontsource/jetbrains-mono@4.5.11/index.css', +export const fontJetbrainsMonoBaseUrl = /* @__PURE__ */ getUrl( + '@fontsource/jetbrains-mono@4.5.11/', ); export const fontMaterialIconsUrl = /* @__PURE__ */ getUrl( @@ -211,7 +205,7 @@ export const fontMonofurUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/monofur?style.css', ); -export const fontMonoidUrl = /* @__PURE__ */ getUrl('@typopro/web-monoid@3.7.5/TypoPRO-Monoid.css'); +export const fontMonoidBaseUrl = /* @__PURE__ */ getUrl('@typopro/web-monoid@3.7.5/'); export const fontNotoUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css2?family=Noto+Sans+Mono&display=swap&style.css', @@ -221,33 +215,31 @@ export const fontNovaMonoUrl = /* @__PURE__ */ getUrl( 'https://fonts.googleapis.com/css2?family=Nova+Mono&display=swap&style.css', ); -export const fontOpenDyslexicUrl = /* @__PURE__ */ getUrl( - '@fontsource/opendyslexic@4.5.4/index.css', -); +export const fontOpenDyslexicBaseUrl = /* @__PURE__ */ getUrl('@fontsource/opendyslexic@4.5.4/'); export const fontProFontWindowsUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/profontwindows?style.css', ); -export const fontRobotoMonoUrl = /* @__PURE__ */ getUrl('@fontsource/roboto-mono@4.5.8/index.css'); +export const fontRobotoMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/roboto-mono@4.5.8/'); export const fontSFMonoUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/sf-mono?style.css', ); -export const fontSourceCodeProUrl = /* @__PURE__ */ getUrl( - '@fontsource/source-code-pro@4.5.12/index.css', +export const fontSourceCodeProBaseUrl = /* @__PURE__ */ getUrl( + '@fontsource/source-code-pro@4.5.12/', ); -export const fontSpaceMonoUrl = /* @__PURE__ */ getUrl('@fontsource/space-mono@4.5.10/index.css'); +export const fontSpaceMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/space-mono@4.5.10/'); export const fontSudoVarUrl = /* @__PURE__ */ getUrl( 'https://fonts.cdnfonts.com/css/sudo-var?style.css', ); -export const fontUbuntuMonoUrl = /* @__PURE__ */ getUrl('@fontsource/ubuntu-mono@4.5.11/index.css'); +export const fontUbuntuMonoBaseUrl = /* @__PURE__ */ getUrl('@fontsource/ubuntu-mono@4.5.11/'); -export const fontVictorMonoUrl = /* @__PURE__ */ getUrl('victormono@1.5.4/dist/index.css'); +export const fontVictorMonoBaseUrl = /* @__PURE__ */ getUrl('victormono@1.5.4/dist/'); export const fscreenUrl = /* @__PURE__ */ getUrl('fscreen@1.2.0/dist/fscreen.esm.js'); From 833cfe69e7271f4a27d73d9ab7319b127d548244 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 72/94] upgrade pyodide to v0.28.0 --- docs/docs/languages/python-wasm.mdx | 2 +- .../languages/python-wasm/lang-python-wasm-script.ts | 8 +------- src/livecodes/vendors.ts | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/docs/languages/python-wasm.mdx b/docs/docs/languages/python-wasm.mdx index a174a2b301..e98f819e3b 100644 --- a/docs/docs/languages/python-wasm.mdx +++ b/docs/docs/languages/python-wasm.mdx @@ -101,7 +101,7 @@ Check the [starter template](#starter-template) for an example. ### Version -Pyodide v0.25.1, running Python v3.11.3 +Pyodide v0.28.0, running Python v3.13.2 ## Code Formatting diff --git a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts index 1c21931348..b4a378516b 100644 --- a/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts +++ b/src/livecodes/languages/python-wasm/lang-python-wasm-script.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { fontAwesomeBaseUrl, pyodideBaseUrl } from '../../vendors'; +import { pyodideBaseUrl } from '../../vendors'; declare const loadPyodide: any; @@ -50,12 +50,6 @@ window.addEventListener('load', async () => { } async function prepareEnv() { - // needed for matplotlib icons - const stylesheet = document.createElement('link'); - stylesheet.rel = 'stylesheet'; - stylesheet.href = fontAwesomeBaseUrl + 'css/font-awesome.min.css'; - document.head.append(stylesheet); - await pyodideReady; const patchInput = ` from js import prompt diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index a91bbc0794..8f8ab58a91 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -358,7 +358,7 @@ export const prismThemesLaserWaveUrl = /* @__PURE__ */ getUrl( ); export const pyodideBaseUrl = /* @__PURE__ */ getUrl( - 'https://cdn.jsdelivr.net/pyodide/v0.25.1/full/', + 'https://cdn.jsdelivr.net/pyodide/v0.28.0/full/', ); export const qrcodeUrl = /* @__PURE__ */ getUrl('easyqrcodejs@4.6.1/dist/easy.qrcode.min.js'); From b558ed391f4bbb53458d8a0a1840ff9ec4f7fc60 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 73/94] update opal to use gh CDN --- src/livecodes/vendors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 8f8ab58a91..6e55138c73 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -323,7 +323,7 @@ export const normalizeCssUrl = /* @__PURE__ */ getUrl('normalize.css@8.0.1/norma export const nunjucksBaseUrl = /* @__PURE__ */ getUrl('nunjucks@3.2.4/browser/'); -export const opalBaseUrl = /* @__PURE__ */ getUrl('https://cdn.opalrb.com/opal/1.8.2/'); +export const opalBaseUrl = /* @__PURE__ */ getUrl('gh:opal/opal-cdn@v1.8.2/opal/1.8.2/'); export const parinferUrl = /* @__PURE__ */ getUrl('parinfer@3.13.1/parinfer.js'); From 1775f04b6cda12e7780306697d0ad32bf722bda5 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 74/94] build(self-hosting): allow downloading modules during build --- eslint.config.mjs | 1 + package.json | 1 + scripts/build.js | 5 ++ scripts/download-modules.js | 143 ++++++++++++++++++++++++++++++++++++ src/livecodes/vendors.ts | 7 +- 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 scripts/download-modules.js diff --git a/eslint.config.mjs b/eslint.config.mjs index aab8c872fb..1b84e6be5e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,6 +31,7 @@ export default [ '**/.docusaurus', '**/.jest', '**/.storybook', + '**/.cache', 'functions/vendors', ], }, diff --git a/package.json b/package.json index 3ba5a73613..b95d18203e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "build:docs": "cd docs && npm run build", "build:storybook": "cd storybook && npm run build", "copy:assets": "recursive-delete build/livecodes/assets && mkdirp build/livecodes/assets && recursive-copy src/livecodes/assets build/livecodes/assets", + "download-modules": "node ./scripts/download-modules.js", "typedocs": "run-s typedocs:*", "typedocs:livecodes": "typedoc src/livecodes/main.ts src/livecodes/app.ts src/livecodes/embed.ts src/livecodes/_modules.ts --out build/typedocs/livecodes --exclude **/*.spec.ts --excludeExternals", "typedocs:sdk": "typedoc src/sdk/livecodes.ts --out build/typedocs/sdk --exclude **/*.spec.ts --excludeExternals", diff --git a/scripts/build.js b/scripts/build.js index 60b1976642..aba45cd21d 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -3,6 +3,7 @@ const { minify: minifyHTML, default: minifyHTMLPlugin } = require('esbuild-plugi const fs = require('fs'); const path = require('path'); +const { downloadModules } = require('./download-modules'); const { applyHash } = require('./hash'); const { injectCss } = require('./inject-css'); const { buildStyles } = require('./styles'); @@ -11,6 +12,7 @@ const { arrToObj, mkdir, uint8arrayToString, iife, getFileNames, getEnvVars } = const args = process.argv.slice(2); const devMode = args.includes('--dev'); +const localModules = args.includes('--download-modules') || process.env.LOCAL_MODULES === 'true'; const root = path.resolve(__dirname + '/..'); const outDir = path.resolve(root, 'build'); @@ -317,6 +319,9 @@ const functionsBuild = () => const stylesBuild = () => buildStyles(devMode); prepareDir().then(async () => { + if (localModules) { + downloadModules(); + } await buildLocalePathLoader(); Promise.all([ esmBuild(), diff --git a/scripts/download-modules.js b/scripts/download-modules.js new file mode 100644 index 0000000000..d4cafce4ac --- /dev/null +++ b/scripts/download-modules.js @@ -0,0 +1,143 @@ +const fs = require('fs'); +const path = require('path'); +const sdkPkg = require('../src/sdk/package.sdk.json'); + +const downloadModules = async ({ dryRun = false } = {}) => { + const vendorsModule = 'src/livecodes/vendors.ts'; + const tempDir = '.cache/'; + const modulesDir = tempDir + '/modules/'; + const outputDir = 'build/modules/'; + + /** @type {string[]} */ + const modules = []; + /** @type {string[]} */ + const baseUrls = []; + /** @type {Array<{module: string; url: string}>} */ + const moduleUrls = []; + + fs.mkdirSync(modulesDir, { recursive: true }); + + const verdorModulesContent = + 'const modulesService = { getUrl: (mod) => mod };\n' + + fs + .readFileSync(vendorsModule, 'utf8') + .replace('import', '// import') + .replace('process.env.SDK_VERSION', `"${sdkPkg.version}"`); + fs.writeFileSync(tempDir + 'vendors.js', verdorModulesContent, 'utf8'); + + const vendorUrls = require('../' + tempDir + 'vendors.js'); + + // modules vs baseUrls + for (const [key, value] of Object.entries(vendorUrls)) { + if (key.includes('BaseUrl')) { + baseUrls.push(value); + } else { + modules.push(value); + } + } + + // get modules from baseUrls + for (const baseUrl of baseUrls) { + // https://unpkg.com/@seth0x41/doppio@1.0.0/ + if (baseUrl.startsWith('https://unpkg.com/')) { + baseUrl.replace('https://unpkg.com/', ''); + } + if (baseUrl.startsWith('https://')) continue; + const mod = getModuleName(baseUrl); + const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; + const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; + const modInfo = await fetch(modInfoUrl).then((res) => res.json()); + const files = modInfo.files; + if (!Array.isArray(files)) continue; + for (const file of files) { + if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { + modules.push(mod + file.name); + } + } + } + + // get moduleUrls + for (const module of modules) { + if (module.startsWith('http')) { + moduleUrls.push({ module, url: module }); + } else if (module.startsWith('gh:')) { + moduleUrls.push({ + module, + url: `https://cdn.jsdelivr.net/gh/${module.replace('gh:', '')}`, + }); + } else { + // use unpkg - no restriction on file types (e.g. jar) + moduleUrls.push({ module, url: `https://unpkg.com/${module}` }); + } + // TODO: handle modules hosted elsewhere: + // - https://cdn.jsdelivr.net/pyodide/v0.25.1/full/ -> https://pyodide.org/en/stable/usage/downloading-and-deploying.html#github-releases + // TODO: handle font absolute urls in css (in font CDNs) + } + + // download modules + for (const { module, url } of moduleUrls) { + const fullPath = + modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); + const dirPath = path.dirname(fullPath); + if (fs.existsSync(fullPath)) continue; + + let text = ''; + if (dryRun) { + text = url; + } else { + const res = await fetch(url); + if (!res.ok) { + console.warn(`Failed to fetch ${module}: ${res.statusText}`); + continue; + } + text = await res.text(); + } + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(fullPath, text); + } + + // copy to build directory + fs.mkdirSync(outputDir, { recursive: true }); + fs.promises.cp(modulesDir, outputDir, { recursive: true }); + + // cleanup + fs.rmSync(tempDir + 'vendors.js'); + + // utils + /** + * @param {string} module + */ + function getModuleName(module) { + if (module.startsWith('gh:')) { + return module.replace('gh:', ''); + } + const parts = module.split('/'); + if (parts[0].startsWith('@')) { + return parts[0] + '/' + parts[1]; + } else { + return parts[0]; + } + } + /** + * @param {string} module + */ + function shouldExclude(module) { + const includePackages = ['@live-codes/browser-compilers']; + const excludeExtensions = ['.map', '.md', '.txt', '.d.ts', 'package.json', 'package-lock.json']; + for (const pkg of includePackages) { + if (module.includes(pkg)) return false; + } + for (const extension of excludeExtensions) { + if (module.endsWith(extension)) { + return true; + } + } + return false; + } +}; + +module.exports = { downloadModules }; + +if (require.main === module) { + downloadModules({ dryRun: process.argv.includes('--dry-run') }); +} diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 6e55138c73..3039f1eda5 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -4,12 +4,13 @@ import { modulesService } from './services/modules'; // - always add full version and file extension // - minimize usage of baseUrls if possible // - if es module imports others, use baseUrl instead -// - after `vendorBaseUrl` the file is sorted alphabetically +// - excluding `vendorsBaseUrl`, the file is sorted alphabetically +// see scripts/download-modules.js const { getUrl } = modulesService; -export const vendorsBaseUrl = 'http://127.0.0.1:8081/'; -// /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.4/dist/'); +export const vendorsBaseUrl = // 'http://127.0.0.1:8081/'; + /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.4/dist/'); export const acornUrl = /* @__PURE__ */ getUrl('acorn@8.12.1/dist/acorn.js'); From 6cdd67d269941937bbac962f94484efd74a4ede1 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 75/94] fix --- scripts/download-modules.js | 2 +- src/livecodes/languages/clojurescript/lang-clojurescript.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index d4cafce4ac..866b3bdc16 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -123,7 +123,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { */ function shouldExclude(module) { const includePackages = ['@live-codes/browser-compilers']; - const excludeExtensions = ['.map', '.md', '.txt', '.d.ts', 'package.json', 'package-lock.json']; + const excludeExtensions = ['.map', '.md', '.d.ts', 'package.json', 'package-lock.json']; for (const pkg of includePackages) { if (module.includes(pkg)) return false; } diff --git a/src/livecodes/languages/clojurescript/lang-clojurescript.ts b/src/livecodes/languages/clojurescript/lang-clojurescript.ts index 60199edeee..6b438e8c5e 100644 --- a/src/livecodes/languages/clojurescript/lang-clojurescript.ts +++ b/src/livecodes/languages/clojurescript/lang-clojurescript.ts @@ -23,9 +23,9 @@ export const clojurescript: LanguageSpecs = { imports: { 'cherry-cljs': cherryCljsBaseUrl + 'index.js', 'cherry-cljs/cljs.core.js': cherryCljsBaseUrl + 'cljs.core.js', - 'cherry-cljs/lib/clojure.string.js': 'lib/clojure.string.js', - 'cherry-cljs/lib/clojure.set.js': 'lib/clojure.set.js', - 'cherry-cljs/lib/clojure.walk.js': 'lib/clojure.walk.js', + 'cherry-cljs/lib/clojure.string.js': cherryCljsBaseUrl + 'lib/clojure.string.js', + 'cherry-cljs/lib/clojure.set.js': cherryCljsBaseUrl + 'lib/clojure.set.js', + 'cherry-cljs/lib/clojure.walk.js': cherryCljsBaseUrl + 'lib/clojure.walk.js', 'squint-cljs': squintCljsBaseUrl + 'index.js', 'squint-cljs/core.js': squintCljsBaseUrl + 'core.js', 'squint-cljs/string.js': squintCljsBaseUrl + 'string.js', From b57c2c790190ccdf58f4c91bf179d3123705cee7 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 76/94] retry downloading failed modules --- scripts/download-modules.js | 60 ++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 866b3bdc16..437f5b9e99 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -3,6 +3,8 @@ const path = require('path'); const sdkPkg = require('../src/sdk/package.sdk.json'); const downloadModules = async ({ dryRun = false } = {}) => { + console.log(`Downloading modules...`); + const vendorsModule = 'src/livecodes/vendors.ts'; const tempDir = '.cache/'; const modulesDir = tempDir + '/modules/'; @@ -75,25 +77,43 @@ const downloadModules = async ({ dryRun = false } = {}) => { } // download modules - for (const { module, url } of moduleUrls) { - const fullPath = - modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); - const dirPath = path.dirname(fullPath); - if (fs.existsSync(fullPath)) continue; - - let text = ''; - if (dryRun) { - text = url; - } else { - const res = await fetch(url); - if (!res.ok) { - console.warn(`Failed to fetch ${module}: ${res.statusText}`); - continue; + const download = async (/** @type {Array<{module: string; url: string}>} */ moduleUrls) => { + /** @type {Array<{module: string; url: string; error: string}>} */ + const failedModuleUrls = []; + + for (const { module, url } of moduleUrls) { + const fullPath = + modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); + const dirPath = path.dirname(fullPath); + if (fs.existsSync(fullPath)) continue; + + let text = ''; + if (dryRun) { + text = url; + } else { + const res = await fetch(url); + if (!res.ok) { + failedModuleUrls.push({ module, url, error: res.statusText }); + continue; + } + text = await res.text(); + } + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(fullPath, text); + } + + return failedModuleUrls; + }; + + let failedModuleUrls = await download(moduleUrls); + if (failedModuleUrls.length) { + // retry + failedModuleUrls = await download(failedModuleUrls); + if (failedModuleUrls.length) { + for (const { module, error } of failedModuleUrls) { + console.error(`Failed to download module (${module}): ${error}`); } - text = await res.text(); } - fs.mkdirSync(dirPath, { recursive: true }); - fs.writeFileSync(fullPath, text); } // copy to build directory @@ -103,6 +123,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { // cleanup fs.rmSync(tempDir + 'vendors.js'); + // log + console.log(`Downloaded ${moduleUrls.length - failedModuleUrls.length} modules.`); + if (failedModuleUrls.length) { + console.log(`Failed to download ${failedModuleUrls.length} modules.`); + } + // utils /** * @param {string} module From 60c479d527136a9e69dc97fc9c9905e0905ea010 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:09:45 +0300 Subject: [PATCH 77/94] build(self-hosting): download modules --- package-lock.json | 1120 +++++++++++++++++++++++++++++++---- package.json | 29 +- scripts/download-modules.js | 105 +++- src/livecodes/vendors.ts | 3 +- 4 files changed, 1139 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74f1dc6bee..52fe28cb88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,8 @@ "comlink": "4.4.1", "conventional-changelog": "3.1.25", "cross-env": "7.0.3", + "decompress": "4.2.1", + "decompress-tarbz2": "4.1.1", "dts-bundle": "0.7.3", "esbuild": "0.20.2", "esbuild-plugin-minify-html": "0.1.2", @@ -5360,6 +5362,27 @@ "node": ">=0.10.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -5403,6 +5426,17 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5494,6 +5528,66 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -5800,16 +5894,47 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -7146,6 +7271,196 @@ "node": ">=0.10" } }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -7484,6 +7799,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -7534,6 +7864,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -7616,13 +7956,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -7643,10 +7981,11 @@ "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -8929,6 +9268,16 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -8956,6 +9305,16 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -9158,12 +9517,19 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/for-in": { @@ -9328,6 +9694,13 @@ ], "peer": true }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -9415,16 +9788,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -9472,6 +9851,20 @@ "node": ">=10" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stdin": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", @@ -9808,12 +10201,13 @@ "dev": true }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9918,10 +10312,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -10186,6 +10581,27 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -10571,6 +10987,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -10737,12 +11160,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -13809,6 +14233,16 @@ "node": ">= 12" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -15550,6 +15984,13 @@ "through": "~2.3" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -17017,6 +17458,20 @@ "node": ">=v12.22.7" } }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, "node_modules/selenium-webdriver": { "version": "4.0.0-rc-1", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz", @@ -17924,6 +18379,16 @@ "node": ">=4" } }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -18605,6 +19070,25 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/teeny-request": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz", @@ -18692,6 +19176,49 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -19009,14 +19536,15 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -19201,6 +19729,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -19604,15 +20143,18 @@ "peer": true }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -19792,6 +20334,17 @@ "node": ">=10" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yjs": { "version": "13.5.40", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.40.tgz", @@ -23760,6 +24313,12 @@ } } }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -23797,6 +24356,16 @@ "file-uri-to-path": "1.0.0" } }, + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -23861,6 +24430,44 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -24092,16 +24699,35 @@ } }, "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "requires": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "callsites": { @@ -25128,6 +25754,148 @@ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, + "decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, "dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -25382,6 +26150,17 @@ } } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -25423,6 +26202,15 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -25493,13 +26281,10 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true }, "es-errors": { "version": "1.3.0", @@ -25514,9 +26299,9 @@ "dev": true }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "requires": { "es-errors": "^1.3.0" @@ -26485,6 +27270,15 @@ "bser": "2.1.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -26503,6 +27297,12 @@ "flat-cache": "^3.0.4" } }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -26662,12 +27462,12 @@ "dev": true }, "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "requires": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" } }, "for-in": { @@ -26780,6 +27580,12 @@ "dev": true, "peer": true }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -26839,16 +27645,21 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { @@ -26880,6 +27691,16 @@ } } }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stdin": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", @@ -27125,13 +27946,10 @@ "dev": true }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true }, "graceful-fs": { "version": "4.2.11", @@ -27200,9 +28018,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true }, "has-tostringtag": { @@ -27396,6 +28214,12 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==" }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, "ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -27673,6 +28497,12 @@ "is-extglob": "^2.1.1" } }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true + }, "is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -27779,12 +28609,12 @@ } }, "is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "requires": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" } }, "is-typedarray": { @@ -30131,6 +30961,12 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, "mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -31435,6 +32271,12 @@ "through": "~2.3" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -32480,6 +33322,15 @@ "xmlchars": "^2.2.0" } }, + "seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "requires": { + "commander": "^2.8.1" + } + }, "selenium-webdriver": { "version": "4.0.0-rc-1", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz", @@ -33217,6 +34068,15 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -33736,6 +34596,21 @@ } } }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, "teeny-request": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz", @@ -33810,6 +34685,31 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "requires": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -34021,14 +34921,14 @@ "dev": true }, "typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" } }, "typed-array-byte-length": { @@ -34158,6 +35058,16 @@ "which-boxed-primitive": "^1.0.2" } }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -34477,15 +35387,17 @@ "peer": true }, "which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "requires": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, @@ -34608,6 +35520,16 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yjs": { "version": "13.5.40", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.40.tgz", diff --git a/package.json b/package.json index b95d18203e..a8df6f6e09 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,8 @@ "comlink": "4.4.1", "conventional-changelog": "3.1.25", "cross-env": "7.0.3", + "decompress": "4.2.1", + "decompress-tarbz2": "4.1.1", "dts-bundle": "0.7.3", "esbuild": "0.20.2", "esbuild-plugin-minify-html": "0.1.2", @@ -157,15 +159,32 @@ "singleQuote": true, "trailingComma": "all", "printWidth": 100, - "plugins": ["prettier-plugin-organize-imports"] + "plugins": [ + "prettier-plugin-organize-imports" + ] }, "jest": { "preset": "ts-jest", "testEnvironment": "jsdom", - "setupFiles": ["/.jest/setup.ts"], - "testPathIgnorePatterns": ["/node_modules/", "/build/", "/src/modules/"], - "collectCoverageFrom": ["src/**/*.ts", "!**/build/**", "!**/vendor/**", "!src/modules/**"], - "coverageReporters": ["json", "html", "lcov"], + "setupFiles": [ + "/.jest/setup.ts" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/build/", + "/src/modules/" + ], + "collectCoverageFrom": [ + "src/**/*.ts", + "!**/build/**", + "!**/vendor/**", + "!src/modules/**" + ], + "coverageReporters": [ + "json", + "html", + "lcov" + ], "resolveJsonModule": true } } diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 437f5b9e99..797c9c2b7e 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -1,5 +1,8 @@ +const decompress = require('decompress'); +const decompressTarbz = require('decompress-tarbz2'); const fs = require('fs'); const path = require('path'); +const stream = require('stream'); const sdkPkg = require('../src/sdk/package.sdk.json'); const downloadModules = async ({ dryRun = false } = {}) => { @@ -16,6 +19,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { const baseUrls = []; /** @type {Array<{module: string; url: string}>} */ const moduleUrls = []; + let pyodideBaseUrl = ''; fs.mkdirSync(modulesDir, { recursive: true }); @@ -39,21 +43,40 @@ const downloadModules = async ({ dryRun = false } = {}) => { } // get modules from baseUrls - for (const baseUrl of baseUrls) { - // https://unpkg.com/@seth0x41/doppio@1.0.0/ - if (baseUrl.startsWith('https://unpkg.com/')) { - baseUrl.replace('https://unpkg.com/', ''); + for (let baseUrl of baseUrls) { + if (baseUrl.includes('@seth0x41/doppio')) { + baseUrl = baseUrl.replace('https://unpkg.com/', '').replace('unpkg:', ''); + } + if (baseUrl.includes('pyodide')) { + pyodideBaseUrl = baseUrl; + } + if (baseUrl.startsWith('https://')) { + continue; } - if (baseUrl.startsWith('https://')) continue; const mod = getModuleName(baseUrl); const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; const modInfo = await fetch(modInfoUrl).then((res) => res.json()); const files = modInfo.files; - if (!Array.isArray(files)) continue; - for (const file of files) { - if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { - modules.push(mod + file.name); + if (Array.isArray(files)) { + for (const file of files) { + if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { + modules.push(mod + file.name); + } + } + } else if (type === 'gh') { + // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB. + const [repo, version] = mod.split('@'); + const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; + const repoInfo = await fetch(filesUrl).then((res) => res.json()); + const files = repoInfo.tree; + if (Array.isArray(files)) { + const basePath = baseUrl.split(mod + '/')[1]; + for (const file of files) { + if (file.path.includes(basePath) && !shouldExclude(mod + '/' + file.path)) { + modules.push('gh:' + mod + '/' + file.path); + } + } } } } @@ -71,8 +94,6 @@ const downloadModules = async ({ dryRun = false } = {}) => { // use unpkg - no restriction on file types (e.g. jar) moduleUrls.push({ module, url: `https://unpkg.com/${module}` }); } - // TODO: handle modules hosted elsewhere: - // - https://cdn.jsdelivr.net/pyodide/v0.25.1/full/ -> https://pyodide.org/en/stable/usage/downloading-and-deploying.html#github-releases // TODO: handle font absolute urls in css (in font CDNs) } @@ -116,6 +137,36 @@ const downloadModules = async ({ dryRun = false } = {}) => { } } + // download Pyodide + if (pyodideBaseUrl) { + const pyodideVersion = pyodideBaseUrl.split('/v')[1].split('/')[0] || '0.28.0'; + const pyodideFiles = [ + `pyodide-${pyodideVersion}.tar.bz2`, + `pyodide-core-${pyodideVersion}.tar.bz2`, + `static-libraries-${pyodideVersion}.tar.bz2`, + `xbuildenv-${pyodideVersion}.tar.bz2`, + ]; + fs.mkdirSync(`${tempDir}pyodide/v${pyodideVersion}`, { recursive: true }); + await Promise.all( + pyodideFiles.map((file) => + (async () => { + const downloadPath = `${tempDir}pyodide/v${pyodideVersion}/${file}`; + const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; + if (!fs.existsSync(downloadPath)) { + await fetchAndSaveFile(url, downloadPath); + } + await decompress(downloadPath, `${outputDir}pyodide/v${pyodideVersion}/full`, { + plugins: [decompressTarbz()], + map: (file) => { + file.path = file.path.split('/').slice(1).join('/'); + return file; + }, + }); + })(), + ), + ); + } + // copy to build directory fs.mkdirSync(outputDir, { recursive: true }); fs.promises.cp(modulesDir, outputDir, { recursive: true }); @@ -124,7 +175,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.rmSync(tempDir + 'vendors.js'); // log - console.log(`Downloaded ${moduleUrls.length - failedModuleUrls.length} modules.`); + console.log(`Modules downloaded to: ${outputDir}`); if (failedModuleUrls.length) { console.log(`Failed to download ${failedModuleUrls.length} modules.`); } @@ -135,7 +186,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { */ function getModuleName(module) { if (module.startsWith('gh:')) { - return module.replace('gh:', ''); + return module.replace('gh:', '').split('/').slice(0, 2).join('/'); } const parts = module.split('/'); if (parts[0].startsWith('@')) { @@ -160,6 +211,34 @@ const downloadModules = async ({ dryRun = false } = {}) => { } return false; } + + /** + * @param {string | URL | Request} url + * @param {any} filePath + */ + async function fetchAndSaveFile(url, filePath) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + if (!response.body) { + throw new Error('Response body is empty.'); + } + const writer = fs.createWriteStream(filePath); + // @ts-ignore + const readableStream = stream.Readable.fromWeb(response.body); + readableStream.pipe(writer); + return /** @type {Promise} */ ( + new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }) + ); + } catch (error) { + console.error(`Error fetching or saving file: ${error.message}`); + } + } }; module.exports = { downloadModules }; diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 3039f1eda5..edf00b14e8 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -1,6 +1,7 @@ import { modulesService } from './services/modules'; // - only use `getUrl` or full URL (not `getModuleUrl`) +// - only use `gh:` or `unpkg: prefixes if required // - always add full version and file extension // - minimize usage of baseUrls if possible // - if es module imports others, use baseUrl instead @@ -118,7 +119,7 @@ export const ddietrCmThemesBaseUrl = /* @__PURE__ */ getUrl( '@ddietr/codemirror-themes@1.4.2/dist/theme/', ); -export const doppioJvmBaseUrl = 'https://unpkg.com/@seth0x41/doppio@1.0.0/'; +export const doppioJvmBaseUrl = /* @__PURE__ */ getUrl('unpkg:@seth0x41/doppio@1.0.0/'); export const dotUrl = /* @__PURE__ */ getUrl('dot@1.1.3/doT.js'); From c8545d4b2acaf8d4f12871ff5aa3beb5baa258d5 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:22 +0300 Subject: [PATCH 78/94] handle appCDN when using local modules --- .dockerignore | 1 - Dockerfile | 2 + docker-compose.yml | 2 + scripts/download-modules.js | 58 +++++++++++++++---- scripts/utils.js | 52 ++++++++++++++++- server/src/app.ts | 6 +- server/src/cache.ts | 13 +++++ src/_headers | 4 +- src/livecodes/compiler/compile.worker.ts | 5 +- src/livecodes/editor/codemirror/codemirror.ts | 5 +- .../editor/codemirror/editor-languages.ts | 4 +- src/livecodes/html/app.html | 5 +- src/livecodes/main.ts | 15 ++++- src/livecodes/services/modules.ts | 28 +++++++-- src/livecodes/vendors.ts | 4 +- 15 files changed, 171 insertions(+), 33 deletions(-) create mode 100644 server/src/cache.ts diff --git a/.dockerignore b/.dockerignore index 9a9e18fcdd..7b37e79d44 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,6 @@ docs/.docusaurus .jest build dist -.cache **/*.log .env docs/docs/api diff --git a/Dockerfile b/Dockerfile index 65402a492c..be4116e214 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ ARG SANDBOX_HOST_NAME ARG SANDBOX_PORT ARG FIREBASE_CONFIG ARG DOCS_BASE_URL +ARG LOCAL_MODULES RUN if [ "$DOCS_BASE_URL" == "null" ]; \ then npm run build:app; \ @@ -42,6 +43,7 @@ COPY server/package*.json ./ RUN npm ci +COPY --from=builder /app/.cache/ tmp/ COPY --from=builder /app/build/ build/ COPY functions/ functions/ diff --git a/docker-compose.yml b/docker-compose.yml index e8bb4ca09c..678950c2a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - SANDBOX_PORT=${SANDBOX_PORT:-8090} - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} + - LOCAL_MODULES=${LOCAL_MODULES:-true} restart: unless-stopped environment: - SELF_HOSTED=true @@ -28,6 +29,7 @@ services: - VALKEY_PORT=6379 volumes: - ./assets:/srv/build/assets + - ./.cache:/srv/.cache depends_on: - valkey diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 797c9c2b7e..43ac4f5379 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -17,6 +17,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { const modules = []; /** @type {string[]} */ const baseUrls = []; + /** @type {string[]} */ + const fontStylSheets = []; /** @type {Array<{module: string; url: string}>} */ const moduleUrls = []; let pyodideBaseUrl = ''; @@ -35,11 +37,17 @@ const downloadModules = async ({ dryRun = false } = {}) => { // modules vs baseUrls for (const [key, value] of Object.entries(vendorUrls)) { - if (key.includes('BaseUrl')) { + if (key.includes('BaseUrl') || key.includes('codeMirrorBasePath')) { baseUrls.push(value); } else { modules.push(value); } + if ( + value.includes('https://fonts.googleapis.com/') || + value.includes('https://fonts.cdnfonts.com/css') + ) { + fontStylSheets.push(value); + } } // get modules from baseUrls @@ -65,7 +73,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { } } } else if (type === 'gh') { - // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB. + // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). const [repo, version] = mod.split('@'); const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; const repoInfo = await fetch(filesUrl).then((res) => res.json()); @@ -73,7 +81,11 @@ const downloadModules = async ({ dryRun = false } = {}) => { if (Array.isArray(files)) { const basePath = baseUrl.split(mod + '/')[1]; for (const file of files) { - if (file.path.includes(basePath) && !shouldExclude(mod + '/' + file.path)) { + if ( + file.type === 'blob' && + file.path.includes(basePath) && + !shouldExclude(mod + '/' + file.path) + ) { modules.push('gh:' + mod + '/' + file.path); } } @@ -111,18 +123,38 @@ const downloadModules = async ({ dryRun = false } = {}) => { let text = ''; if (dryRun) { text = url; + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(fullPath, text); } else { - const res = await fetch(url); - if (!res.ok) { - failedModuleUrls.push({ module, url, error: res.statusText }); + const result = await fetchAndSaveFile(url, fullPath); + if (result instanceof Error) { + failedModuleUrls.push({ module, url, error: result.message }); continue; } - text = await res.text(); + const urlPattern = /https:\/\/[^'"\)]*/g; + + if (fullPath.includes('fonts.googleapis.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll('https://fonts.gstatic.com/', '../fonts.gstatic.com/'); + fs.writeFileSync(fullPath, patched); + } + if (fullPath.includes('fonts.cdnfonts.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll('https://fonts.cdnfonts.com/', '../'); + fs.writeFileSync(fullPath, patched); + } } - fs.mkdirSync(dirPath, { recursive: true }); - fs.writeFileSync(fullPath, text); } - return failedModuleUrls; }; @@ -169,7 +201,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { // copy to build directory fs.mkdirSync(outputDir, { recursive: true }); - fs.promises.cp(modulesDir, outputDir, { recursive: true }); + await fs.promises.cp(modulesDir, outputDir, { recursive: true }); // cleanup fs.rmSync(tempDir + 'vendors.js'); @@ -225,6 +257,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { if (!response.body) { throw new Error('Response body is empty.'); } + fs.mkdirSync(path.dirname(filePath), { recursive: true }); const writer = fs.createWriteStream(filePath); // @ts-ignore const readableStream = stream.Readable.fromWeb(response.body); @@ -236,7 +269,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { }) ); } catch (error) { - console.error(`Error fetching or saving file: ${error.message}`); + console.error(`Error downloading file (${url}): ${error.message}`); + return error; } } }; diff --git a/scripts/utils.js b/scripts/utils.js index 52fa646cf8..e58f0b1457 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -72,6 +72,7 @@ const getVars = (/** @type {boolean} */ devMode) => { const selfHostedSandboxHostName = process.env.SANDBOX_HOST_NAME || 'localhost'; const selfHostedSandboxPort = Number(process.env.SANDBOX_PORT) || 8090; const firebaseConfig = process.env.FIREBASE_CONFIG || 'null'; + const localModules = String(process.env.LOCAL_MODULES) === 'true'; return { appVersion, sdkVersion, @@ -86,6 +87,7 @@ const getVars = (/** @type {boolean} */ devMode) => { firebaseConfig, selfHostedSandboxHostName, selfHostedSandboxPort, + localModules, }; }; @@ -104,6 +106,7 @@ const getEnvVars = (/** @type {boolean} */ devMode) => { firebaseConfig, selfHostedSandboxHostName, selfHostedSandboxPort, + localModules, } = getVars(devMode); return { 'process.env.VERSION': `"${appVersion || ''}"`, @@ -119,8 +122,55 @@ const getEnvVars = (/** @type {boolean} */ devMode) => { 'process.env.SANDBOX_HOST_NAME': `"${selfHostedSandboxHostName}"`, 'process.env.SANDBOX_PORT': `"${selfHostedSandboxPort}"`, 'process.env.FIREBASE_CONFIG': `"${firebaseConfig}"`, + 'process.env.LOCAL_MODULES': `"${localModules}"`, define: 'undefined', // prevent using AMD (e.g. in lz-string), }; }; -module.exports = { arrToObj, mkdir, uint8arrayToString, iife, getFileNames, getEnvVars }; +const createAsyncQueue = (concurrency = 1) => { + /** @typedef {(() => Promise | void) | Promise} Task */ + + /** @type {Task[]} */ + const queue = []; + let running = 0; + + const add = (/** @type {Task} */ task) => { + queue.push(task); + processQueue(); + }; + + const processQueue = async () => { + if (running >= concurrency || queue.length === 0) { + return; + } + + running++; + const task = queue.shift(); + + try { + if (typeof task === 'function') { + await task(); + } else if (typeof task === 'object' && 'then' in task) { + await task; + } + } catch (error) { + console.error('Task failed:', error); + } finally { + running--; + processQueue(); + } + }; + return { + add, + }; +}; + +module.exports = { + arrToObj, + mkdir, + uint8arrayToString, + iife, + getFileNames, + getEnvVars, + createAsyncQueue, +}; diff --git a/server/src/app.ts b/server/src/app.ts index ba6c6b8f94..2aae023a8f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,6 +5,7 @@ import path from 'node:path'; import { onRequest as index } from '../../functions/index.ts'; import { onRequest as oembed } from '../../functions/oembed.ts'; import { broadcast } from './broadcast/index.ts'; +import { saveCache } from './cache.ts'; import { corsProxy } from './cors.ts'; import { sandbox } from './sandbox.ts'; import { share } from './share.ts'; @@ -40,7 +41,7 @@ app.use( setHeaders(res) { // match headers in: src/_headers const reqPath = res.req.path; - if (reqPath.startsWith('/assets/')) { + if (reqPath.startsWith('/modules/')) { res.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000, immutable'); } if (reqPath.startsWith('/livecodes/')) { @@ -79,3 +80,6 @@ if (process.env.SELF_HOSTED_BROADCAST === 'true') { userTokens: process.env.BROADCAST_TOKENS || '', }); } + +// save local modules cache to host +saveCache(); diff --git a/server/src/cache.ts b/server/src/cache.ts new file mode 100644 index 0000000000..fabc3aab18 --- /dev/null +++ b/server/src/cache.ts @@ -0,0 +1,13 @@ +import fs from 'fs'; +import path from 'path'; +import { dirname } from './utils.ts'; + +export const saveCache = async () => { + const srcDir = path.resolve(dirname, '../../tmp/'); + const dstDir = path.resolve(dirname, '../../.cache/'); + + if (fs.existsSync(srcDir)) { + fs.mkdirSync(dstDir, { recursive: true }); + await fs.promises.cp(srcDir, dstDir, { recursive: true }); + } +}; diff --git a/src/_headers b/src/_headers index d466dc3a5d..48b82f66a0 100644 --- a/src/_headers +++ b/src/_headers @@ -1,7 +1,7 @@ -/assets/* +/livecodes/* cache-control: public, max-age=31536000, s-maxage=31536000, immutable -/livecodes/* +/modules/* cache-control: public, max-age=31536000, s-maxage=31536000, immutable /livecodes/:file.map diff --git a/src/livecodes/compiler/compile.worker.ts b/src/livecodes/compiler/compile.worker.ts index 34180d33f4..93f4547494 100644 --- a/src/livecodes/compiler/compile.worker.ts +++ b/src/livecodes/compiler/compile.worker.ts @@ -9,8 +9,9 @@ import type { EditorLibrary, Language, } from '../models'; +import { getAppCDN, modulesService } from '../services'; import { doOnce, getErrorMessage, objectFilter } from '../utils/utils'; -import { codeMirrorBaseUrl, comlinkBaseUrl, vendorsBaseUrl } from '../vendors'; +import { codeMirrorBasePath, comlinkBaseUrl, vendorsBaseUrl } from '../vendors'; import { getAllCompilers } from './get-all-compilers'; import type { CompilerMessage, CompilerMessageEvent, LanguageOrProcessor } from './models'; declare const importScripts: (...args: string[]) => void; @@ -276,7 +277,7 @@ const initCodemirrorTS = doOnce(async () => { await loadTypeScript(); importScripts(comlinkBaseUrl + 'umd/comlink.js'); importScripts(typescriptVfsUrl); - importScripts(codeMirrorBaseUrl + 'codemirror-ts.worker.js'); + importScripts(modulesService.getUrl(codeMirrorBasePath, getAppCDN()) + 'codemirror-ts.worker.js'); const { createWorker } = worker.CodemirrorTsWorker; const { createDefaultMapFromCDN, createSystem, createVirtualTypeScriptEnvironment } = worker.typescriptVFS; diff --git a/src/livecodes/editor/codemirror/codemirror.ts b/src/livecodes/editor/codemirror/codemirror.ts index 199efa8e0b..2a525f7721 100644 --- a/src/livecodes/editor/codemirror/codemirror.ts +++ b/src/livecodes/editor/codemirror/codemirror.ts @@ -41,12 +41,15 @@ import type { Language, Theme, } from '../../models'; +import { getAppCDN, modulesService } from '../../services'; import { ctrl, debounce, getRandomString } from '../../utils/utils'; -import { codeMirrorBaseUrl, comlinkBaseUrl } from '../../vendors'; +import { codeMirrorBasePath, comlinkBaseUrl } from '../../vendors'; import { getEditorTheme } from '../themes'; import { codemirrorThemes, customThemes } from './codemirror-themes'; import { editorLanguages } from './editor-languages'; +const codeMirrorBaseUrl = modulesService.getUrl(codeMirrorBasePath, getAppCDN()); + export type CodeiumEditor = Pick & { editorId: EditorOptions['editorId']; }; diff --git a/src/livecodes/editor/codemirror/editor-languages.ts b/src/livecodes/editor/codemirror/editor-languages.ts index 1b7e807037..6640fd453c 100644 --- a/src/livecodes/editor/codemirror/editor-languages.ts +++ b/src/livecodes/editor/codemirror/editor-languages.ts @@ -12,11 +12,13 @@ import { javascript } from '@codemirror/lang-javascript'; import { json } from '@codemirror/lang-json'; import type { Language } from '../../models'; -import { codeMirrorBaseUrl } from '../../vendors'; +import { getAppCDN, modulesService } from '../../services'; +import { codeMirrorBasePath } from '../../vendors'; const legacy = (parser: StreamParser) => new LanguageSupport(StreamLanguage.define(parser)); +const codeMirrorBaseUrl = modulesService.getUrl(codeMirrorBasePath, getAppCDN()); const getPath = (mod: string) => codeMirrorBaseUrl + mod; const moduleUrls = { diff --git a/src/livecodes/html/app.html b/src/livecodes/html/app.html index b92bc6710c..e265916019 100644 --- a/src/livecodes/html/app.html +++ b/src/livecodes/html/app.html @@ -364,10 +364,7 @@
- + {{polyfillScript}} diff --git a/src/livecodes/main.ts b/src/livecodes/main.ts index b65d9c8f4a..d99136b19a 100644 --- a/src/livecodes/main.ts +++ b/src/livecodes/main.ts @@ -5,7 +5,7 @@ import appHTML from './html/app.html?raw'; import type { API, CDN, Config, CustomEvents, EmbedOptions } from './models'; import { modulesService } from './services/modules'; import { isInIframe } from './utils/utils'; -import { codeMirrorBaseUrl, esModuleShimsPath } from './vendors'; +import { codeMirrorBasePath, esModuleShimsPath } from './vendors'; export type { API, Config }; @@ -77,7 +77,7 @@ export const livecodes = (container: string, config: Partial = {}): Prom const loadApp = async () => { const appCDN = await modulesService.checkCDNs(esModuleShimsPath, params.get('appCDN') as CDN); - + const codeMirrorBaseUrl = modulesService.getUrl(codeMirrorBasePath, appCDN as CDN); const supportsImportMaps = HTMLScriptElement.supports ? HTMLScriptElement.supports('importmap') : false; @@ -104,6 +104,17 @@ export const livecodes = (container: string, config: Partial = {}): Prom import * as mod from '${baseUrl}{{hash:codemirror.js}}'; window['${baseUrl}{{hash:codemirror.js}}'] = mod; + `, + ) + .replace( + /{{polyfillScript}}/g, + process.env.LOCAL_MODULES === 'true' + ? '' + : ` + `, ) .replace(/{{codemirrorCoreUrl}}/g, `${codeMirrorBaseUrl}codemirror-core.js`) diff --git a/src/livecodes/services/modules.ts b/src/livecodes/services/modules.ts index 5ec3f1d25a..a55dcc8d51 100644 --- a/src/livecodes/services/modules.ts +++ b/src/livecodes/services/modules.ts @@ -1,6 +1,7 @@ import type { CDN } from '../models'; declare const globalThis: { appCDN: CDN }; +const localModules = process.env.LOCAL_MODULES === 'true'; const moduleCDNs: CDN[] = [ 'esm.sh', @@ -60,14 +61,16 @@ export const modulesService = { }, getUrl: (path: string, cdn?: CDN) => - path.startsWith('http') || path.startsWith('data:') - ? path - : getCdnUrl(path, false, cdn || getAppCDN()) || path, + path.startsWith('data:') ? path : getCdnUrl(path, false, cdn || getAppCDN()) || path, cdnLists: { npm: npmCDNs, module: moduleCDNs, gh: ghCDNs }, checkCDNs: async (testModule: string, preferredCDN?: CDN) => { - const cdns: CDN[] = [preferredCDN, ...modulesService.cdnLists.npm].filter(Boolean) as CDN[]; + const modulesBaseUrl = new URL('./modules/', location.href).href as CDN; + const localCDN = localModules ? modulesBaseUrl : undefined; + const cdns: CDN[] = [preferredCDN, localCDN, ...modulesService.cdnLists.npm].filter( + (x) => x != null, + ); for (const cdn of cdns) { try { const res = await fetch(modulesService.getUrl(testModule, cdn), { @@ -94,6 +97,10 @@ export const getAppCDN = (): CDN => { }; const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { + if (localModules && !isModule) { + return getLocalUrl(modName, defaultCDN); + } + if (modName.startsWith('http') || modName.startsWith('data:')) return modName; const post = isModule && modName.startsWith('unpkg:') ? '?module' : ''; if (modName.startsWith('gh:')) { modName = modName.replace('gh', ghCDNs[0]); @@ -110,6 +117,19 @@ const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { return null; }; +const getLocalUrl = (modName: string, modulesBaseUrl = '/modules/') => { + modName = modName + .replace('https://unpkg.com/', '') + .replace('unpkg:', '') + .replaceAll('https://', '') + .replaceAll(':', '_') + .replaceAll('?', '_'); + if (modName.includes('pyodide')) { + modName = modName.replace('cdn.jsdelivr.net/', ''); + } + return `${modulesBaseUrl}${modName}`; +}; + // based on https://github.com/neoascetic/rawgithack/blob/master/web/rawgithack.js const TEMPLATES: Array<[RegExp, string]> = [ [/^(esm\.sh:)(.+)/i, 'https://esm.sh/$2'], diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index edf00b14e8..46259f33e3 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -1,6 +1,6 @@ import { modulesService } from './services/modules'; -// - only use `getUrl` or full URL (not `getModuleUrl`) +// - only use `getUrl` (not `getModuleUrl` or plain URLs) - except `es-module-shims` and `codeMirrorBasePath` // - only use `gh:` or `unpkg: prefixes if required // - always add full version and file extension // - minimize usage of baseUrls if possible @@ -93,7 +93,7 @@ export const codeiumProviderUrl = /* @__PURE__ */ getUrl( '@live-codes/monaco-codeium-provider@0.2.2/dist/index.js', ); -export const codeMirrorBaseUrl = /* @__PURE__ */ getUrl('@live-codes/codemirror@0.3.2/build/'); +export const codeMirrorBasePath = '@live-codes/codemirror@0.3.2/build/'; export const coffeeScriptUrl = /* @__PURE__ */ getUrl( 'coffeescript@2.7.0/lib/coffeescript-browser-compiler-legacy/coffeescript.js', From 9d70ab7f1aff163131947263836bb1cb4809fad1 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:25 +0300 Subject: [PATCH 79/94] handle module cache by docker --- .dockerignore | 1 + Dockerfile | 13 +++++++--- docker-compose.yml | 1 - package.json | 1 + scripts/build.js | 5 ---- scripts/download-modules.js | 48 ++++++++++++++++++------------------- server/src/app.ts | 4 ---- server/src/cache.ts | 13 ---------- 8 files changed, 35 insertions(+), 51 deletions(-) delete mode 100644 server/src/cache.ts diff --git a/.dockerignore b/.dockerignore index 7b37e79d44..9a9e18fcdd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ docs/.docusaurus .jest build dist +.cache **/*.log .env docs/docs/api diff --git a/Dockerfile b/Dockerfile index be4116e214..64156be368 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,6 @@ COPY server/package*.json server/ RUN npm ci -COPY . . - ARG SELF_HOSTED ARG SELF_HOSTED_SHARE ARG SELF_HOSTED_BROADCAST @@ -23,6 +21,16 @@ ARG FIREBASE_CONFIG ARG DOCS_BASE_URL ARG LOCAL_MODULES +COPY scripts/download-modules.js scripts/ +COPY src/livecodes/vendors.ts src/livecodes/ +COPY src/sdk/package.sdk.json src/sdk/ + +RUN if [ "$LOCAL_MODULES" == "true" ]; \ + then npm run download-modules; \ + fi + +COPY . . + RUN if [ "$DOCS_BASE_URL" == "null" ]; \ then npm run build:app; \ else npm run build; \ @@ -43,7 +51,6 @@ COPY server/package*.json ./ RUN npm ci -COPY --from=builder /app/.cache/ tmp/ COPY --from=builder /app/build/ build/ COPY functions/ functions/ diff --git a/docker-compose.yml b/docker-compose.yml index 678950c2a2..0d581d8c54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,6 @@ services: - VALKEY_PORT=6379 volumes: - ./assets:/srv/build/assets - - ./.cache:/srv/.cache depends_on: - valkey diff --git a/package.json b/package.json index a8df6f6e09..e40a4ce3c1 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "build:docs": "cd docs && npm run build", "build:storybook": "cd storybook && npm run build", "copy:assets": "recursive-delete build/livecodes/assets && mkdirp build/livecodes/assets && recursive-copy src/livecodes/assets build/livecodes/assets", + "copy:modules": "recursive-delete build/modules && mkdirp .cache/modules && mkdirp build/modules && recursive-copy .cache/modules build/modules", "download-modules": "node ./scripts/download-modules.js", "typedocs": "run-s typedocs:*", "typedocs:livecodes": "typedoc src/livecodes/main.ts src/livecodes/app.ts src/livecodes/embed.ts src/livecodes/_modules.ts --out build/typedocs/livecodes --exclude **/*.spec.ts --excludeExternals", diff --git a/scripts/build.js b/scripts/build.js index aba45cd21d..60b1976642 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -3,7 +3,6 @@ const { minify: minifyHTML, default: minifyHTMLPlugin } = require('esbuild-plugi const fs = require('fs'); const path = require('path'); -const { downloadModules } = require('./download-modules'); const { applyHash } = require('./hash'); const { injectCss } = require('./inject-css'); const { buildStyles } = require('./styles'); @@ -12,7 +11,6 @@ const { arrToObj, mkdir, uint8arrayToString, iife, getFileNames, getEnvVars } = const args = process.argv.slice(2); const devMode = args.includes('--dev'); -const localModules = args.includes('--download-modules') || process.env.LOCAL_MODULES === 'true'; const root = path.resolve(__dirname + '/..'); const outDir = path.resolve(root, 'build'); @@ -319,9 +317,6 @@ const functionsBuild = () => const stylesBuild = () => buildStyles(devMode); prepareDir().then(async () => { - if (localModules) { - downloadModules(); - } await buildLocalePathLoader(); Promise.all([ esmBuild(), diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 43ac4f5379..374c6702fb 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -5,13 +5,24 @@ const path = require('path'); const stream = require('stream'); const sdkPkg = require('../src/sdk/package.sdk.json'); +const cacheDir = '.cache/'; +const modulesDir = cacheDir + '/modules/'; +const srcVendorsModule = 'src/livecodes/vendors.ts'; +const cacheVendorsModule = cacheDir + 'vendors.js'; + +const transformVendorsModule = (/** @type {string} */ content) => + 'const modulesService = { getUrl: (mod) => mod };\n' + + content.replace('import', '// import').replace('process.env.SDK_VERSION', `"${sdkPkg.version}"`); + const downloadModules = async ({ dryRun = false } = {}) => { - console.log(`Downloading modules...`); + const srcVendorsContent = fs.readFileSync(srcVendorsModule, 'utf-8'); + const cacheVendorsContent = fs.existsSync(cacheVendorsModule) + ? fs.readFileSync(cacheVendorsModule, 'utf-8') + : ''; + + if (srcVendorsContent === cacheVendorsContent) return; - const vendorsModule = 'src/livecodes/vendors.ts'; - const tempDir = '.cache/'; - const modulesDir = tempDir + '/modules/'; - const outputDir = 'build/modules/'; + console.log(`Downloading modules...`); /** @type {string[]} */ const modules = []; @@ -25,15 +36,9 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.mkdirSync(modulesDir, { recursive: true }); - const verdorModulesContent = - 'const modulesService = { getUrl: (mod) => mod };\n' + - fs - .readFileSync(vendorsModule, 'utf8') - .replace('import', '// import') - .replace('process.env.SDK_VERSION', `"${sdkPkg.version}"`); - fs.writeFileSync(tempDir + 'vendors.js', verdorModulesContent, 'utf8'); - - const vendorUrls = require('../' + tempDir + 'vendors.js'); + const verdorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); + fs.writeFileSync(cacheVendorsModule, verdorModulesContent, 'utf8'); + const vendorUrls = require('../' + cacheVendorsModule); // modules vs baseUrls for (const [key, value] of Object.entries(vendorUrls)) { @@ -178,16 +183,16 @@ const downloadModules = async ({ dryRun = false } = {}) => { `static-libraries-${pyodideVersion}.tar.bz2`, `xbuildenv-${pyodideVersion}.tar.bz2`, ]; - fs.mkdirSync(`${tempDir}pyodide/v${pyodideVersion}`, { recursive: true }); + fs.mkdirSync(`${cacheDir}pyodide/v${pyodideVersion}`, { recursive: true }); await Promise.all( pyodideFiles.map((file) => (async () => { - const downloadPath = `${tempDir}pyodide/v${pyodideVersion}/${file}`; + const downloadPath = `${cacheDir}pyodide/v${pyodideVersion}/${file}`; const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; if (!fs.existsSync(downloadPath)) { await fetchAndSaveFile(url, downloadPath); } - await decompress(downloadPath, `${outputDir}pyodide/v${pyodideVersion}/full`, { + await decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { plugins: [decompressTarbz()], map: (file) => { file.path = file.path.split('/').slice(1).join('/'); @@ -199,15 +204,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { ); } - // copy to build directory - fs.mkdirSync(outputDir, { recursive: true }); - await fs.promises.cp(modulesDir, outputDir, { recursive: true }); - - // cleanup - fs.rmSync(tempDir + 'vendors.js'); - // log - console.log(`Modules downloaded to: ${outputDir}`); + console.log(`Modules downloaded to: ${modulesDir}`); if (failedModuleUrls.length) { console.log(`Failed to download ${failedModuleUrls.length} modules.`); } diff --git a/server/src/app.ts b/server/src/app.ts index 2aae023a8f..f4ac9eab5b 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,7 +5,6 @@ import path from 'node:path'; import { onRequest as index } from '../../functions/index.ts'; import { onRequest as oembed } from '../../functions/oembed.ts'; import { broadcast } from './broadcast/index.ts'; -import { saveCache } from './cache.ts'; import { corsProxy } from './cors.ts'; import { sandbox } from './sandbox.ts'; import { share } from './share.ts'; @@ -80,6 +79,3 @@ if (process.env.SELF_HOSTED_BROADCAST === 'true') { userTokens: process.env.BROADCAST_TOKENS || '', }); } - -// save local modules cache to host -saveCache(); diff --git a/server/src/cache.ts b/server/src/cache.ts deleted file mode 100644 index fabc3aab18..0000000000 --- a/server/src/cache.ts +++ /dev/null @@ -1,13 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { dirname } from './utils.ts'; - -export const saveCache = async () => { - const srcDir = path.resolve(dirname, '../../tmp/'); - const dstDir = path.resolve(dirname, '../../.cache/'); - - if (fs.existsSync(srcDir)) { - fs.mkdirSync(dstDir, { recursive: true }); - await fs.promises.cp(srcDir, dstDir, { recursive: true }); - } -}; From a8814f00c869f6a9d420c8c6130dc7741ac8aed1 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:25 +0300 Subject: [PATCH 80/94] use self-hosted URL in build --- Dockerfile | 2 ++ docker-compose.yml | 2 ++ scripts/build.js | 21 +++++++++++++++++++-- src/404.html | 3 +-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 64156be368..1205ca3cda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ COPY server/package*.json server/ RUN npm ci ARG SELF_HOSTED +ARG HOST_NAME +ARG PORT ARG SELF_HOSTED_SHARE ARG SELF_HOSTED_BROADCAST ARG BROADCAST_PORT diff --git a/docker-compose.yml b/docker-compose.yml index 0d581d8c54..3b95be9569 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: dockerfile: Dockerfile args: - SELF_HOSTED=true + - HOST_NAME=${HOST_NAME:-livecodes.localhost} + - PORT=${PORT:-443} - SELF_HOSTED_SHARE=${SELF_HOSTED_SHARE:-true} - SELF_HOSTED_BROADCAST=${SELF_HOSTED_BROADCAST:-true} - BROADCAST_PORT=${BROADCAST_PORT:-3030} diff --git a/scripts/build.js b/scripts/build.js index 60b1976642..2f941158a8 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -36,7 +36,7 @@ const copyFile = async (filePath, outputName, replace) => { fs.writeFileSync(dist, minified, 'utf8'); }; -const addBaseUrl = (content) => { +const addBaseUrl = (/** @type {string} */ content) => { let baseUrl = process.env.BASE_URL; if (baseUrl && baseUrl !== '/') { if (!baseUrl.startsWith('/') && !baseUrl.startsWith('http')) { @@ -50,6 +50,14 @@ const addBaseUrl = (content) => { return content; }; +const useSelfHostedURL = (/** @type {string} */ content) => { + if (!process.env.HOST_NAME) return content; + const hostname = process.env.HOST_NAME; + const port = Number(process.env.PORT) || 443; + const appUrl = `https://${hostname}${port !== 443 ? ':' + port : ''}`; + return content.replaceAll('https://livecodes.io', `${appUrl}`); +}; + const prepareDir = async () => { mkdir(outDir); mkdir(outDir + '/livecodes/'); @@ -65,9 +73,18 @@ const prepareDir = async () => { copyFile('src/netlify.toml', 'netlify.toml'), copyFile('src/favicon.ico', 'favicon.ico'), copyFile('src/404.html', '404.html', addBaseUrl), - copyFile('src/index.html', 'index.html'), + copyFile('src/index.html', 'index.html', useSelfHostedURL), copyFile('src/livecodes/html/app-base.html', 'app.html'), ]); + await fs.promises + .readFile(path.resolve(root, 'src/livecodes/assets/site.webmanifest'), 'utf8') + .then((siteWebManifest) => + fs.promises.writeFile( + path.resolve(outDir, 'livecodes/assets/site.webmanifest'), + useSelfHostedURL(siteWebManifest), + 'utf8', + ), + ); }; /** @type {Partial} */ diff --git a/src/404.html b/src/404.html index 562664a1b0..0ec7ea8735 100644 --- a/src/404.html +++ b/src/404.html @@ -142,9 +142,8 @@ From 9cc2d1aaaf1ac96050929a84612a9b71df7b92e8 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:25 +0300 Subject: [PATCH 81/94] download modules in parallel --- scripts/download-modules.js | 371 +++++++++++++++++++++--------------- scripts/utils.js | 39 ---- 2 files changed, 219 insertions(+), 191 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 374c6702fb..7afa42dc1f 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -32,8 +32,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { const fontStylSheets = []; /** @type {Array<{module: string; url: string}>} */ const moduleUrls = []; + /** @type {Array<{module: string; url: string; error: string}>} */ + const failedModuleUrls = []; let pyodideBaseUrl = ''; + const downloadQueue = createAsyncQueue(10); + fs.mkdirSync(modulesDir, { recursive: true }); const verdorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); @@ -56,47 +60,48 @@ const downloadModules = async ({ dryRun = false } = {}) => { } // get modules from baseUrls - for (let baseUrl of baseUrls) { - if (baseUrl.includes('@seth0x41/doppio')) { - baseUrl = baseUrl.replace('https://unpkg.com/', '').replace('unpkg:', ''); - } - if (baseUrl.includes('pyodide')) { - pyodideBaseUrl = baseUrl; - } - if (baseUrl.startsWith('https://')) { - continue; - } - const mod = getModuleName(baseUrl); - const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; - const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; - const modInfo = await fetch(modInfoUrl).then((res) => res.json()); - const files = modInfo.files; - if (Array.isArray(files)) { - for (const file of files) { - if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { - modules.push(mod + file.name); - } + await Promise.all( + baseUrls.map(async (baseUrl) => { + if (baseUrl.includes('@seth0x41/doppio')) { + baseUrl = baseUrl.replace('https://unpkg.com/', '').replace('unpkg:', ''); + } + if (baseUrl.includes('pyodide')) { + pyodideBaseUrl = baseUrl; } - } else if (type === 'gh') { - // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). - const [repo, version] = mod.split('@'); - const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; - const repoInfo = await fetch(filesUrl).then((res) => res.json()); - const files = repoInfo.tree; + if (baseUrl.startsWith('https://')) return; + + const mod = getModuleName(baseUrl); + const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; + const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; + const modInfo = await fetch(modInfoUrl).then((res) => res.json()); + const files = modInfo.files; if (Array.isArray(files)) { - const basePath = baseUrl.split(mod + '/')[1]; for (const file of files) { - if ( - file.type === 'blob' && - file.path.includes(basePath) && - !shouldExclude(mod + '/' + file.path) - ) { - modules.push('gh:' + mod + '/' + file.path); + if ((mod + file.name).includes(baseUrl) && !shouldExclude(mod + file.name)) { + modules.push(mod + file.name); + } + } + } else if (type === 'gh') { + // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). + const [repo, version] = mod.split('@'); + const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; + const repoInfo = await fetch(filesUrl).then((res) => res.json()); + const files = repoInfo.tree; + if (Array.isArray(files)) { + const basePath = baseUrl.split(mod + '/')[1]; + for (const file of files) { + if ( + file.type === 'blob' && + file.path.includes(basePath) && + !shouldExclude(mod + '/' + file.path) + ) { + modules.push('gh:' + mod + '/' + file.path); + } } } } - } - } + }), + ); // get moduleUrls for (const module of modules) { @@ -111,14 +116,13 @@ const downloadModules = async ({ dryRun = false } = {}) => { // use unpkg - no restriction on file types (e.g. jar) moduleUrls.push({ module, url: `https://unpkg.com/${module}` }); } - // TODO: handle font absolute urls in css (in font CDNs) } // download modules - const download = async (/** @type {Array<{module: string; url: string}>} */ moduleUrls) => { - /** @type {Array<{module: string; url: string; error: string}>} */ - const failedModuleUrls = []; - + const download = ( + /** @type {Array<{module: string; url: string}>} */ moduleUrls, + isRetry = false, + ) => { for (const { module, url } of moduleUrls) { const fullPath = modulesDir + module.replaceAll('https://', '').replaceAll(':', '_').replaceAll('?', '_'); @@ -131,48 +135,48 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.mkdirSync(dirPath, { recursive: true }); fs.writeFileSync(fullPath, text); } else { - const result = await fetchAndSaveFile(url, fullPath); - if (result instanceof Error) { - failedModuleUrls.push({ module, url, error: result.message }); - continue; - } - const urlPattern = /https:\/\/[^'"\)]*/g; + downloadQueue.add(() => + fetchAndSaveFile(url, fullPath).then(async (result) => { + if (result instanceof Error) { + if (isRetry) { + failedModuleUrls.push({ module, url, error: result.message }); + console.error(`Failed to download module (${module}): ${result.message}`); + return; + } + download([{ module, url }], true); + return; + } + const urlPattern = /https:\/\/[^'"\)]*/g; - if (fullPath.includes('fonts.googleapis.com/css')) { - const content = fs.readFileSync(fullPath, 'utf8'); - const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); - for (const fontUrl of fontUrls) { - const fontPath = fontUrl.replace('https://', modulesDir); - await fetchAndSaveFile(fontUrl, fontPath); - } - const patched = content.replaceAll('https://fonts.gstatic.com/', '../fonts.gstatic.com/'); - fs.writeFileSync(fullPath, patched); - } - if (fullPath.includes('fonts.cdnfonts.com/css')) { - const content = fs.readFileSync(fullPath, 'utf8'); - const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); - for (const fontUrl of fontUrls) { - const fontPath = fontUrl.replace('https://', modulesDir); - await fetchAndSaveFile(fontUrl, fontPath); - } - const patched = content.replaceAll('https://fonts.cdnfonts.com/', '../'); - fs.writeFileSync(fullPath, patched); - } + if (fullPath.includes('fonts.googleapis.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll( + 'https://fonts.gstatic.com/', + '../fonts.gstatic.com/', + ); + fs.writeFileSync(fullPath, patched); + } + if (fullPath.includes('fonts.cdnfonts.com/css')) { + const content = fs.readFileSync(fullPath, 'utf8'); + const fontUrls = Array.from(content.matchAll(new RegExp(urlPattern))).flat(); + for (const fontUrl of fontUrls) { + const fontPath = fontUrl.replace('https://', modulesDir); + await fetchAndSaveFile(fontUrl, fontPath); + } + const patched = content.replaceAll('https://fonts.cdnfonts.com/', '../'); + fs.writeFileSync(fullPath, patched); + } + }), + ); } } - return failedModuleUrls; }; - - let failedModuleUrls = await download(moduleUrls); - if (failedModuleUrls.length) { - // retry - failedModuleUrls = await download(failedModuleUrls); - if (failedModuleUrls.length) { - for (const { module, error } of failedModuleUrls) { - console.error(`Failed to download module (${module}): ${error}`); - } - } - } + download(moduleUrls); // download Pyodide if (pyodideBaseUrl) { @@ -184,93 +188,156 @@ const downloadModules = async ({ dryRun = false } = {}) => { `xbuildenv-${pyodideVersion}.tar.bz2`, ]; fs.mkdirSync(`${cacheDir}pyodide/v${pyodideVersion}`, { recursive: true }); - await Promise.all( - pyodideFiles.map((file) => - (async () => { - const downloadPath = `${cacheDir}pyodide/v${pyodideVersion}/${file}`; - const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; - if (!fs.existsSync(downloadPath)) { - await fetchAndSaveFile(url, downloadPath); - } - await decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { - plugins: [decompressTarbz()], - map: (file) => { - file.path = file.path.split('/').slice(1).join('/'); - return file; - }, - }); - })(), - ), - ); - } - - // log - console.log(`Modules downloaded to: ${modulesDir}`); - if (failedModuleUrls.length) { - console.log(`Failed to download ${failedModuleUrls.length} modules.`); + pyodideFiles.forEach((file) => { + const downloadPath = `${cacheDir}pyodide/v${pyodideVersion}/${file}`; + const url = `https://github.com/pyodide/pyodide/releases/download/${pyodideVersion}/${file}`; + if (!fs.existsSync(downloadPath)) { + downloadQueue.add(() => + fetchAndSaveFile(url, downloadPath).then((result) => { + if (result instanceof Error) { + failedModuleUrls.push({ module: file, url, error: result.message }); + console.error(`Failed to download module (${module}): ${result.message}`); + return; + } + return decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { + plugins: [decompressTarbz()], + map: (file) => { + file.path = file.path.split('/').slice(1).join('/'); + return file; + }, + }); + }), + ); + } + }); } - // utils - /** - * @param {string} module - */ - function getModuleName(module) { - if (module.startsWith('gh:')) { - return module.replace('gh:', '').split('/').slice(0, 2).join('/'); + downloadQueue.onFinish(() => { + // log + console.log(`Modules downloaded to: ${modulesDir}`); + if (failedModuleUrls.length) { + console.log(`Failed to download ${failedModuleUrls.length} modules.`); } - const parts = module.split('/'); - if (parts[0].startsWith('@')) { - return parts[0] + '/' + parts[1]; - } else { - return parts[0]; + }); +}; + +// utils +/** + * @param {string} module + */ +function getModuleName(module) { + if (module.startsWith('gh:')) { + return module.replace('gh:', '').split('/').slice(0, 2).join('/'); + } + const parts = module.split('/'); + if (parts[0].startsWith('@')) { + return parts[0] + '/' + parts[1]; + } else { + return parts[0]; + } +} +/** + * @param {string} module + */ +function shouldExclude(module) { + const includePackages = ['@live-codes/browser-compilers']; + const excludeExtensions = ['.map', '.md', '.d.ts', 'package.json', 'package-lock.json']; + for (const pkg of includePackages) { + if (module.includes(pkg)) return false; + } + for (const extension of excludeExtensions) { + if (module.endsWith(extension)) { + return true; } } - /** - * @param {string} module - */ - function shouldExclude(module) { - const includePackages = ['@live-codes/browser-compilers']; - const excludeExtensions = ['.map', '.md', '.d.ts', 'package.json', 'package-lock.json']; - for (const pkg of includePackages) { - if (module.includes(pkg)) return false; + return false; +} + +/** + * @param {string | URL | Request} url + * @param {any} filePath + */ +async function fetchAndSaveFile(url, filePath) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - for (const extension of excludeExtensions) { - if (module.endsWith(extension)) { - return true; - } + if (!response.body) { + throw new Error('Response body is empty.'); } - return false; + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + const writer = fs.createWriteStream(filePath); + // @ts-ignore + const readableStream = stream.Readable.fromWeb(response.body); + readableStream.pipe(writer); + return /** @type {Promise} */ ( + new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }) + ); + } catch (error) { + console.error(`Error downloading file (${url}): ${error.message}`); + return error; } +} + +const createAsyncQueue = (concurrency = 1) => { + /** @typedef {(() => Promise | unknown) | Promise} Task */ + + /** @type {Task[]} */ + const queue = []; + /** @type {Array<() => unknown>} */ + const subscribers = []; + let running = 0; + + const add = (/** @type {Task} */ task) => { + queue.push(task); + processQueue(); + }; + + const processQueue = async () => { + if (running >= concurrency || queue.length === 0) { + await runSubscribers(); + return; + } + + running++; + const task = queue.shift(); - /** - * @param {string | URL | Request} url - * @param {any} filePath - */ - async function fetchAndSaveFile(url, filePath) { try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - if (!response.body) { - throw new Error('Response body is empty.'); + if (typeof task === 'function') { + await task(); + } else if (typeof task === 'object' && 'then' in task) { + await task; } - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - const writer = fs.createWriteStream(filePath); - // @ts-ignore - const readableStream = stream.Readable.fromWeb(response.body); - readableStream.pipe(writer); - return /** @type {Promise} */ ( - new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - }) - ); } catch (error) { - console.error(`Error downloading file (${url}): ${error.message}`); - return error; + console.error('Task failed:', error); + } finally { + running--; + processQueue(); } - } + }; + + const runSubscribers = async () => { + if (queue.length === 0 && running === 0 && subscribers.length > 0) { + const cb = subscribers.shift(); + if (typeof cb === 'function') { + await cb(); + } + await runSubscribers(); + } + }; + + const onFinish = (/** @type {() => unknown} */ cb) => { + subscribers.push(cb); + }; + + return { + add, + onFinish, + }; }; module.exports = { downloadModules }; diff --git a/scripts/utils.js b/scripts/utils.js index e58f0b1457..74091a6ec0 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -127,44 +127,6 @@ const getEnvVars = (/** @type {boolean} */ devMode) => { }; }; -const createAsyncQueue = (concurrency = 1) => { - /** @typedef {(() => Promise | void) | Promise} Task */ - - /** @type {Task[]} */ - const queue = []; - let running = 0; - - const add = (/** @type {Task} */ task) => { - queue.push(task); - processQueue(); - }; - - const processQueue = async () => { - if (running >= concurrency || queue.length === 0) { - return; - } - - running++; - const task = queue.shift(); - - try { - if (typeof task === 'function') { - await task(); - } else if (typeof task === 'object' && 'then' in task) { - await task; - } - } catch (error) { - console.error('Task failed:', error); - } finally { - running--; - processQueue(); - } - }; - return { - add, - }; -}; - module.exports = { arrToObj, mkdir, @@ -172,5 +134,4 @@ module.exports = { iife, getFileNames, getEnvVars, - createAsyncQueue, }; From cfa402c9cf646d583e6b44751f9400d1aa5a9aab Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:25 +0300 Subject: [PATCH 82/94] fix --- src/livecodes/editor/monaco/monaco.ts | 4 ++-- src/livecodes/vendors.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/livecodes/editor/monaco/monaco.ts b/src/livecodes/editor/monaco/monaco.ts index 338c985815..30166fa82d 100644 --- a/src/livecodes/editor/monaco/monaco.ts +++ b/src/livecodes/editor/monaco/monaco.ts @@ -24,7 +24,7 @@ import { monacoBaseUrl, monacoEmacsUrl, monacoVimUrl, - monacoVolarUrl, + monacoVolarBaseUrl, vendorsBaseUrl, } from '../../vendors'; import { getEditorTheme } from '../themes'; @@ -266,7 +266,7 @@ export const createEditor = async (options: EditorOptions): Promise const addVueSupport = async () => { if (vueRegistered) return; vueRegistered = true; - const { registerVue, registerHighlighter } = await import(monacoVolarUrl); + const { registerVue, registerHighlighter } = await import(monacoVolarBaseUrl + 'index.js'); const tsCompilerOptions = { ...getCompilerOptions('vue'), jsx: 'preserve' }; await registerVue({ editor, monaco, tsCompilerOptions, silent: true }); shikiThemes = registerHighlighter(monaco); diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 46259f33e3..8f21222fc7 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -311,9 +311,7 @@ export const monacoThemesBaseUrl = /* @__PURE__ */ getUrl('monaco-themes@0.4.4/t export const monacoVimUrl = /* @__PURE__ */ getUrl('monaco-vim@0.4.1/dist/monaco-vim.js'); -export const monacoVolarUrl = /* @__PURE__ */ getUrl( - '@live-codes/monaco-volar@0.1.0/dist/index.js', -); +export const monacoVolarBaseUrl = /* @__PURE__ */ getUrl('@live-codes/monaco-volar@0.1.0/dist/'); export const mustacheUrl = /* @__PURE__ */ getUrl('mustache@4.2.0/mustache.js'); From 72e8c8671e69a4fccbda5d7642c55b8cddf9a80a Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:25 +0300 Subject: [PATCH 83/94] add docs for local modules --- docs/docs/advanced/docker.mdx | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/docs/advanced/docker.mdx b/docs/docs/advanced/docker.mdx index 1e0188300a..e8a3a02a63 100644 --- a/docs/docs/advanced/docker.mdx +++ b/docs/docs/advanced/docker.mdx @@ -169,10 +169,56 @@ The hostname and many other options can be set using [environment variables](#en Runs code in a separate origin [sandboxed iframe](https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/) to prevent cross-site scripting. +- Local modules + + See [Local Modules](#local-modules) section for details. + - [404 page](https://livecodes.io/404) Custom 404 page for resources that are not found. +## Local Modules + +(EXPERIMENTAL) + +LiveCodes depends on a large number of external modules to support a wide range of features (e.g. code editors, compilers, formatters, etc). +These modules are loaded from CDNs (e.g. [jsDelivr](https://www.jsdelivr.com/), [unpkg](https://unpkg.com/), etc). +So, in spite of being a client-side app, LiveCodes requires an internet connection to load these modules. + +However, if you want to be able to run LiveCodes without an internet connection, you can download all these modules locally by setting the [environment variable](#environment-variables) `LOCAL_MODULES=true` (it is set to `false` by default). +In this case, all modules are downloaded during build (~ 1.5 GB), and are served locally. + +When working without internet connection, all resources have to be available locally. So, automatic [module resolution](../features/module-resolution.mdx) (e.g. of npm modules imported in user code) and loading type definitions for [intellisense](../features/intellisense.mdx) and auto-complete will not work out of the box. +However, you can provide the list of modules to load using the [`imports`](../configuration/configuration-object.mdx#imports) property of the [config](../configuration/configuration-object.mdx) object (see [Custom Module Resolution](../features/module-resolution.mdx#custom-module-resolution)). +Similarly, you can also provide the list of type definitions to load using the [`types`](../configuration/configuration-object.mdx#types) property (see [Custom Types](../features/intellisense.mdx#custom-types)). +These modules and type definitions should be prepared in advance and made available to the running app (e.g. in the `/assets` directory - see [Volumes](#volumes)). + +Example: + +To run [React starter template](https://livecodes.io/?template=react) locally without internet connection, you need to: + +(assuming that the app is running on this URL: https://livecodes.localhost/) + +- Add [these files](https://github.com/hatemhosny/custom-modules-demo/tree/main/modules) to the [`/assets` directory](#volumes). +- Open React starter template: https://livecodes.localhost/?template=react +- Add this code to custom settings (Project menu → Custom Settings): + +```json +{ + "imports": { + "react": "https://livecodes.localhost/assets/react.js", + "react/compiler-runtime": "https://livecodes.localhost/assets/react-compiler-runtime.js", + "react/jsx-runtime": "https://livecodes.localhost/assets/react-jsx-runtime.js", + "react-dom": "https://livecodes.localhost/assets/react-dom.js", + "react-dom/client": "https://livecodes.localhost/assets/react-dom-client.js", + "scheduler": "https://livecodes.localhost/assets/scheduler.js" + }, + "types": { + "react": "https://livecodes.localhost/assets/react.d.ts" + } +} +``` + ## Environment Variables The app can be customized by setting different environment variables. @@ -205,6 +251,7 @@ The following environment variables are supported: | `FIREBASE_CONFIG` | [Firebase config object](https://firebase.google.com/docs/web/learn-more#config-object) (JSON), used for [authentication](../features/github-integration.mdx) | | | `DOCS_BASE_URL` | [Base URL](../features/self-hosting.mdx#custom-build) of the documentation (e.g. `/docs/`) | `null` | | `LOG_URL` | Full URL to send [server-side analytics](https://github.com/live-codes/livecodes/blob/develop/functions/index.ts) (e.g. `https://api.website.com/log`) | `null` | +| `LOCAL_MODULES` | Download and use all modules locally (see [Local Modules](#local-modules)) | `false` | :::info note When running in a non-local environment (e.g. VPS), From 07c9dd6f3c501dfb3f362923357ec3ad3de9d446 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:25 +0300 Subject: [PATCH 84/94] set LOCAL_MODULES to false by default --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3b95be9569..bd16aaab02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: - SANDBOX_PORT=${SANDBOX_PORT:-8090} - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} - - LOCAL_MODULES=${LOCAL_MODULES:-true} + - LOCAL_MODULES=${LOCAL_MODULES:-false} restart: unless-stopped environment: - SELF_HOSTED=true From fbc7a7922f6a74da3211c1e7e12103adc4811b9d Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 85/94] fix --- src/livecodes/services/modules.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/livecodes/services/modules.ts b/src/livecodes/services/modules.ts index a55dcc8d51..308beaf637 100644 --- a/src/livecodes/services/modules.ts +++ b/src/livecodes/services/modules.ts @@ -68,9 +68,9 @@ export const modulesService = { checkCDNs: async (testModule: string, preferredCDN?: CDN) => { const modulesBaseUrl = new URL('./modules/', location.href).href as CDN; const localCDN = localModules ? modulesBaseUrl : undefined; - const cdns: CDN[] = [preferredCDN, localCDN, ...modulesService.cdnLists.npm].filter( + const cdns = [preferredCDN, localCDN, ...modulesService.cdnLists.npm].filter( (x) => x != null, - ); + ) as CDN[]; for (const cdn of cdns) { try { const res = await fetch(modulesService.getUrl(testModule, cdn), { From 19d24fe65019a8324858d1b0bc393734afb29dfb Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 86/94] upgrade to node v24.4.1 --- .nvmrc | 2 +- Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.nvmrc b/.nvmrc index e35b986d37..564e92d084 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.1.0 +v24.4.1 diff --git a/Dockerfile b/Dockerfile index 1205ca3cda..8e4a49017a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.1.0-alpine3.21 AS builder +FROM node:24.4.1-alpine3.22 AS builder RUN apk update --no-cache && apk add --no-cache git @@ -38,7 +38,7 @@ RUN if [ "$DOCS_BASE_URL" == "null" ]; \ else npm run build; \ fi -FROM node:24.1.0-alpine3.21 AS server +FROM node:24.4.1-alpine3.22 AS server RUN addgroup -S appgroup RUN adduser -S appuser -G appgroup From 8825cc31d2daa64f17f161de252c7bb717aa6d89 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 87/94] fix --- src/livecodes/services/modules.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/livecodes/services/modules.ts b/src/livecodes/services/modules.ts index 308beaf637..54f5e57a74 100644 --- a/src/livecodes/services/modules.ts +++ b/src/livecodes/services/modules.ts @@ -100,7 +100,7 @@ const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { if (localModules && !isModule) { return getLocalUrl(modName, defaultCDN); } - if (modName.startsWith('http') || modName.startsWith('data:')) return modName; + if (modName.startsWith('data:')) return modName; const post = isModule && modName.startsWith('unpkg:') ? '?module' : ''; if (modName.startsWith('gh:')) { modName = modName.replace('gh', ghCDNs[0]); @@ -114,6 +114,7 @@ const getCdnUrl = (modName: string, isModule: boolean, defaultCDN?: CDN) => { return modName.replace(pattern, template) + post; } } + if (modName.startsWith('http')) return modName; return null; }; From 27bd81d749348a7f577430f7442181f8a683ed0e Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 88/94] add in docker NODE_OPTIONS="--max-old-space-size=4096" --- Dockerfile | 1 + docker-compose.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8e4a49017a..65b79df75e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ ARG SANDBOX_PORT ARG FIREBASE_CONFIG ARG DOCS_BASE_URL ARG LOCAL_MODULES +ARG NODE_OPTIONS COPY scripts/download-modules.js scripts/ COPY src/livecodes/vendors.ts src/livecodes/ diff --git a/docker-compose.yml b/docker-compose.yml index bd16aaab02..801f8bfa11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} - LOCAL_MODULES=${LOCAL_MODULES:-false} + - NODE_OPTIONS="--max-old-space-size=4096" restart: unless-stopped environment: - SELF_HOSTED=true @@ -29,6 +30,7 @@ services: - LOG_URL=${LOG_URL:-null} - VALKEY_HOST=valkey - VALKEY_PORT=6379 + - NODE_OPTIONS="--max-old-space-size=4096" volumes: - ./assets:/srv/build/assets depends_on: From 6689efb98bd6b399d24224ea14ebc34a4db12afe Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 89/94] fix --- scripts/download-modules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 7afa42dc1f..50acfbafe6 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -36,7 +36,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { const failedModuleUrls = []; let pyodideBaseUrl = ''; - const downloadQueue = createAsyncQueue(10); + const downloadQueue = createAsyncQueue(5); fs.mkdirSync(modulesDir, { recursive: true }); From f98246c3546500b7b8820afbb5404a15085557c8 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 90/94] fix --- docker-compose.yml | 4 ++-- scripts/download-modules.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 801f8bfa11..dfcc8c54ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - FIREBASE_CONFIG=${FIREBASE_CONFIG:-} - DOCS_BASE_URL=${DOCS_BASE_URL:-null} - LOCAL_MODULES=${LOCAL_MODULES:-false} - - NODE_OPTIONS="--max-old-space-size=4096" + - NODE_OPTIONS=--max-old-space-size=4096 restart: unless-stopped environment: - SELF_HOSTED=true @@ -30,7 +30,7 @@ services: - LOG_URL=${LOG_URL:-null} - VALKEY_HOST=valkey - VALKEY_PORT=6379 - - NODE_OPTIONS="--max-old-space-size=4096" + - NODE_OPTIONS=--max-old-space-size=4096 volumes: - ./assets:/srv/build/assets depends_on: diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 50acfbafe6..0004773f2b 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -36,7 +36,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { const failedModuleUrls = []; let pyodideBaseUrl = ''; - const downloadQueue = createAsyncQueue(5); + const downloadQueue = createAsyncQueue(10); fs.mkdirSync(modulesDir, { recursive: true }); @@ -196,7 +196,7 @@ const downloadModules = async ({ dryRun = false } = {}) => { fetchAndSaveFile(url, downloadPath).then((result) => { if (result instanceof Error) { failedModuleUrls.push({ module: file, url, error: result.message }); - console.error(`Failed to download module (${module}): ${result.message}`); + console.error(`Failed to download module (${file}): ${result.message}`); return; } return decompress(downloadPath, `${modulesDir}pyodide/v${pyodideVersion}/full`, { From 2fbbafe2d338d433e31e883936c65600a38a8f46 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 91/94] avoid using node sync methods --- server/src/broadcast/index.ts | 8 ++++---- server/src/sandbox.ts | 16 +++++++--------- server/src/utils.ts | 9 ++++----- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/server/src/broadcast/index.ts b/server/src/broadcast/index.ts index ae46f28602..c2a30c59d2 100644 --- a/server/src/broadcast/index.ts +++ b/server/src/broadcast/index.ts @@ -106,7 +106,7 @@ export const broadcast = ({ }); }); - app.get('/channels/:id', (req, res) => { + app.get('/channels/:id', async (req, res) => { const channel = req.params.id; if (channels[channel]) { channels[channel].lastAccessed = Date.now(); @@ -114,9 +114,9 @@ export const broadcast = ({ const views = ['index', 'code', 'result'] as const; const view = req.query.view; const file = views.find((v) => v === view) || (hasData ? 'index' : 'result'); - const fileContent = fs - .readFileSync(path.join(broadcastDir, `/${file}.html`), 'utf-8') - .replaceAll('{{AppUrl}}', appUrl); + const fileContent = ( + await fs.promises.readFile(path.join(broadcastDir, `/${file}.html`), 'utf-8') + ).replaceAll('{{AppUrl}}', appUrl); res.status(200).send(fileContent); } else { res.status(404).send('Channel not found!'); diff --git a/server/src/sandbox.ts b/server/src/sandbox.ts index 553f1c5251..c252ff3e7f 100644 --- a/server/src/sandbox.ts +++ b/server/src/sandbox.ts @@ -5,7 +5,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { dirname } from './utils.ts'; -export const sandbox = ({ hostname, port }: { hostname: string; port: number }) => { +export const sandbox = async ({ hostname, port }: { hostname: string; port: number }) => { const app = express(); app.use(cors()); @@ -13,11 +13,12 @@ export const sandbox = ({ hostname, port }: { hostname: string; port: number }) const sandboxDir = path.resolve(dirname, 'sandbox'); let sandboxVersionDir = path.resolve(sandboxDir, 'v8'); - fs.readdirSync(sandboxDir).forEach((v) => { - if (fs.statSync(path.resolve(sandboxDir, v)).isDirectory()) { + const dirs = await fs.promises.readdir(sandboxDir); + for (const v of dirs) { + if ((await fs.promises.stat(path.resolve(sandboxDir, v))).isDirectory()) { sandboxVersionDir = path.resolve(sandboxDir, v); } - }); + } app.use('/', (req, res) => { if (req.path === '/') { @@ -32,11 +33,8 @@ export const sandbox = ({ hostname, port }: { hostname: string; port: number }) : req.path; res.set('Content-Type', 'text/html'); const filePath = path.resolve(dirname, 'sandbox' + reqPath); - if (fs.existsSync(filePath)) { - res.status(200).sendFile(filePath); - return; - } - res.status(404).sendFile(path.resolve(sandboxVersionDir, 'index.html')); + const onError = () => res.status(404).sendFile(path.resolve(sandboxVersionDir, 'index.html')); + res.status(200).sendFile(filePath, onError); }); app.listen(port, () => { diff --git a/server/src/utils.ts b/server/src/utils.ts index 1bc567acae..3d6b17b74e 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -10,7 +10,7 @@ const getDirname = (metaUrl: string) => path.dirname(fileURLToPath(metaUrl)); export const dirname = getDirname(import.meta.url); export const appDir = path.resolve(dirname, '../../build/'); -const getFileContent = async (fullUrl: string) => { +const getFileContent = (fullUrl: string): Promise => { let pathname: string; try { const url = new URL(fullUrl); @@ -22,10 +22,9 @@ const getFileContent = async (fullUrl: string) => { pathname = 'index.html'; } let filePath = path.resolve(appDir, pathname); - if (!fs.existsSync(filePath)) { - filePath = path.resolve(appDir, '404.html'); - } - return fs.promises.readFile(filePath, 'utf8'); + return fs.promises + .readFile(filePath, 'utf8') + .catch(() => fs.promises.readFile(path.resolve(appDir, '404.html'), 'utf8')); }; const convertToWebRequest = (req: express.Request) => { From 7b8a0eecd394ad2ba5ca41f6e68bbda05cb04cd7 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 92/94] fixes --- scripts/download-modules.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/download-modules.js b/scripts/download-modules.js index 0004773f2b..e828e668fb 100644 --- a/scripts/download-modules.js +++ b/scripts/download-modules.js @@ -20,7 +20,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { ? fs.readFileSync(cacheVendorsModule, 'utf-8') : ''; - if (srcVendorsContent === cacheVendorsContent) return; + const transformedSrcContent = transformVendorsModule(srcVendorsContent); + if (transformedSrcContent === cacheVendorsContent) return; console.log(`Downloading modules...`); @@ -40,8 +41,8 @@ const downloadModules = async ({ dryRun = false } = {}) => { fs.mkdirSync(modulesDir, { recursive: true }); - const verdorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); - fs.writeFileSync(cacheVendorsModule, verdorModulesContent, 'utf8'); + const vendorModulesContent = transformVendorsModule(fs.readFileSync(srcVendorsModule, 'utf8')); + fs.writeFileSync(cacheVendorsModule, vendorModulesContent, 'utf8'); const vendorUrls = require('../' + cacheVendorsModule); // modules vs baseUrls @@ -73,7 +74,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { const mod = getModuleName(baseUrl); const type = baseUrl.startsWith('gh:') ? 'gh' : 'npm'; const modInfoUrl = `https://data.jsdelivr.com/v1/package/${type}/${mod}/flat`; - const modInfo = await fetch(modInfoUrl).then((res) => res.json()); + const response = await fetch(modInfoUrl); + if (!response.ok) { + console.warn(`Failed to fetch module info for ${mod}: ${response.status}`); + return; + } + const modInfo = await response.json(); const files = modInfo.files; if (Array.isArray(files)) { for (const file of files) { @@ -85,7 +91,12 @@ const downloadModules = async ({ dryRun = false } = {}) => { // use GitHub API when jsDelivr errors: Package size exceeded the configured limit of 50 MB (e.g. opal). const [repo, version] = mod.split('@'); const filesUrl = `https://api.github.com/repos/${repo}/git/trees/${version}?recursive=1`; - const repoInfo = await fetch(filesUrl).then((res) => res.json()); + const response = await fetch(filesUrl); + if (!response.ok) { + console.warn(`Failed to fetch repo info for ${repo}: ${response.status}`); + return; + } + const repoInfo = await response.json(); const files = repoInfo.tree; if (Array.isArray(files)) { const basePath = baseUrl.split(mod + '/')[1]; From 4f6dd51cedab33b66f8f40e2bd09435cc0e727ba Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 93/94] fix color picker --- src/livecodes/styles/inc-menu.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livecodes/styles/inc-menu.scss b/src/livecodes/styles/inc-menu.scss index 98ecebe646..4b92adf7c2 100644 --- a/src/livecodes/styles/inc-menu.scss +++ b/src/livecodes/styles/inc-menu.scss @@ -570,7 +570,7 @@ i.arrow { width: var(--s16); &[for='theme-color-custom'] { - background: conic-gradient(in hsl longer hue, red 0 0); + background: conic-gradient(in hsl longer hue, red 0 100%); filter: contrast(0.5); } From ce9293bd793d51bb9cf9455514d903e21a9e81ef Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Sun, 28 Sep 2025 20:10:26 +0300 Subject: [PATCH 94/94] fix --- server/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/utils.ts b/server/src/utils.ts index 3d6b17b74e..890cd399fc 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -21,7 +21,7 @@ const getFileContent = (fullUrl: string): Promise => { if (!pathname.trim()) { pathname = 'index.html'; } - let filePath = path.resolve(appDir, pathname); + const filePath = path.resolve(appDir, pathname); return fs.promises .readFile(filePath, 'utf8') .catch(() => fs.promises.readFile(path.resolve(appDir, '404.html'), 'utf8'));