Svelte
+ ++ Click on the Svelte logo to learn more +
+diff --git a/README.md b/README.md index 4d0cb49cc9..ebdf21c220 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A [feature-rich](https://livecodes.io/docs/features/), open-source, **client-sid [](https://www.npmjs.com/package/livecodes) [](https://www.npmjs.com/package/livecodes) [](https://www.jsdelivr.com/package/npm/livecodes) -[](https://livecodes.io/docs/languages/) +[](https://livecodes.io/docs/languages/) [](https://livecodes.io/docs/) [](https://livecodes.io/docs/llms.txt) [](https://livecodes.io/docs/llms-full.txt) diff --git a/docs/docs/configuration/configuration-object.mdx b/docs/docs/configuration/configuration-object.mdx index e8a946dba2..4e9dfefb6c 100644 --- a/docs/docs/configuration/configuration-object.mdx +++ b/docs/docs/configuration/configuration-object.mdx @@ -141,6 +141,12 @@ An object that configures the language and content of the markup editor. This ca A URL to load `content` from. It has to be a valid URL that is CORS-enabled. The URL is only fetched if `content` property had no value. +- `hidden`: + Type: [`boolean | undefined`](../api/interfaces/Config.md#hidden) + Default: `""` + If `true`, the title of the code editor is hidden, however its code is still evaluated. + This can be useful in embedded playgrounds (e.g. for hiding unnecessary code). + - `hiddenContent`: Type: [`string | undefined`](../api/interfaces/Config.md#hiddencontent) Default: `undefined` @@ -153,18 +159,12 @@ An object that configures the language and content of the markup editor. This ca A URL to load `hiddenContent` from. It has to be a valid URL that is CORS-enabled. The URL is only fetched if `hiddenContent` property had no value. -- `foldedLines`: - Type: [`Array<{ from: number; to: number }> | undefined`](../api/interfaces/Config.md#foldedlines) - Default: `undefined` - Lines that get folded when the editor loads. The code can be unfolded by clicking on arrow beside the line. - This can be useful for less relevant code in embedded playgrounds. - - `title`: Type: [`string | undefined`](../api/interfaces/Config.md#title) Default: `""` If set, this is used as the title of the editor in the UI, overriding the default title set to the language name (e.g. "Python" can be used instead of "Py (Wasm)"). -- `hideTitle`: +- `hideTitle` (**deprecated**, use `hidden` instead): Type: [`boolean | undefined`](../api/interfaces/Config.md#hidetitle) Default: `""` If `true`, the title of the code editor is hidden, however its code is still evaluated. @@ -186,6 +186,12 @@ An object that configures the language and content of the markup editor. This ca The initial position of the cursor in the code editor. Example: `{lineNumber: 5, column: 10}` +- `foldedLines`: + Type: [`Array<{ from: number; to: number }> | undefined`](../api/interfaces/Config.md#foldedlines) + Default: `undefined` + Lines that get folded when the editor loads. The code can be unfolded by clicking on arrow beside the line. + This can be useful for less relevant code in embedded playgrounds. + ### `style` Type: [`Editor`](../api/interfaces/Config.md#style) diff --git a/docs/docs/features/import.mdx b/docs/docs/features/import.mdx index adc81f6c4b..bfbb1cbff5 100644 --- a/docs/docs/features/import.mdx +++ b/docs/docs/features/import.mdx @@ -81,7 +81,7 @@ Import is supported from any of the following: - Local file(s) - Code in zip file (Local or URL) - Code in image - OCR (Local or URL) -- Projects shared in official playgrounds of [TypeScript](https://www.typescriptlang.org/play) and [Vue](https://play.vuejs.org/) +- Projects shared in official playgrounds of [TypeScript](https://www.typescriptlang.org/play), [Vue](https://play.vuejs.org/), [Svelte](https://svelte.dev/playground), [Preact](https://preactjs.com/repl/) and [Solid](https://playground.solidjs.com/) - [Exported project JSON](./export.mdx) (single project and bulk import) Import sources are identified by URL patterns (e.g. origin, pathname and extension). @@ -90,34 +90,6 @@ Import sources are identified by URL patterns (e.g. origin, pathname and extensi Local files can be imported from the "Import Screen" or by dragging and dropping the file(s) in the editor. ::: -## File Selection - -For sources that provide multiple files (e.g. GitHub/GitLab directories, GitHub gists, GitLab snippets and local files), a best guess is tried to load files in respective editors. Best results are when there are 3 files and each file is in a language (identified by file extension) that can be loaded to a different editor, for example: - -- index.html, style.css, script.js -- default.pug, app.scss, main.ts - -The following file names are given higher priority: - -- Markup files starting with `index.` or `default.` -- Style files starting with `style.` or `styles.` -- Script files starting with `script.`, `app.`, `main.` or `index.` - -While README, markdown files and files with no extension are given lower priority. - -Alternatively, files can be specified using the `files` [query param](../configuration/query-params.mdx). It takes a **comma-separated list** of filenames. The first 3 found files are loaded. If 1 or 2 files are specified, only these will be loaded. The first matching file is shown by default in the active editor. - -The query params should have the following format: -`?x={url}&files={file1},{file2},{file3}` - -Example: -`?x={url}&files=Counter.tsx,counter.scss,counter.html` - -The active editor can be specified using the [`activeEditor`](../configuration/configuration-object.mdx#activeeditor) (or its alias `active`) [query param](../configuration/query-params.mdx). It takes the name of the editor (`markup`, `style` or `script`) or its ID (`0`, `1` or `2`) to be shown by default. - -Example: -`?x={url}&activeEditor=style` or `?x={url}&active=1` - ## Import Shared Projects [Shared Projects](./share.mdx) can be imported using the value of the query param `x` generated by the Share screen. This starts with either: diff --git a/docs/docs/languages/cssmodules.mdx b/docs/docs/languages/cssmodules.mdx index 11579dfd82..95df85639c 100644 --- a/docs/docs/languages/cssmodules.mdx +++ b/docs/docs/languages/cssmodules.mdx @@ -8,7 +8,7 @@ The selector names are unique to avoid naming collision. They can then be import CSS Modules can be enabled from the style editor menu. -Selectors added to the style editor (using any language e.g. CSS, SCSS, Less, etc.) are transformed to unique selectors. The transformed classes are then accessible in the script editor as a JSON object, and are injected into the HTML elements. +Selectors added to the style editor (using any language e.g. CSS, SCSS, Less, etc.) are transformed to unique selectors. The transformed classes are then accessible in the script editor as a JSON object, and can be injected into the HTML elements. **Example:** @@ -193,7 +193,7 @@ In addition, the following settings are available: - `addClassesToHTML` - Type: `boolean`. Default: `true`. + Type: `boolean`. Default: `false`. The generated classes are injected into the HTML elements, so the styles are applied without having to assign them using JavaScript. @@ -212,7 +212,7 @@ Please note that custom settings should be valid JSON (i.e. functions are not al "cssmodules": { "exportGlobals": true, "localsConvention": "camelCaseOnly", - "addClassesToHTML": false + "addClassesToHTML": true } } ``` @@ -234,16 +234,30 @@ If you get this working, [please create a pull request](https://github.com/live- import LiveCodes from '../../src/components/LiveCodes.tsx'; -export const params = { +export const config = { activeEditor: 'style', - html: '
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Dolore earum blanditiis quidem non beatae ipsam autem maiores ut et delectus unde repudiandae, repellendus aut. Aspernatur similique facere facilis minima tempora.
\nLorem, ipsum dolor sit amet consectetur adipisicing elit. Dolore earum blanditiis quidem non beatae ipsam autem maiores ut et delectus unde repudiandae, repellendus aut. Aspernatur similique facere facilis minima tempora.
\n => {
@@ -5435,7 +6109,7 @@ const createApi = (): API => {
if (!cacheIsValid(getCache(), getContentConfig(getConfig()))) {
await getResultPage({ forExport: true });
}
- return JSON.parse(JSON.stringify(getCachedCode()));
+ return cloneObject(getCachedCode());
};
const apiShow: API['show'] = async (
@@ -5459,7 +6133,7 @@ const createApi = (): API => {
split?.show('code', full);
} else if (panel === 'console' || panel === 'compiled' || panel === 'tests') {
split?.show('output');
- toolsPane?.setActiveTool(panel);
+ toolsPane?.setActiveTool(panel as 'console' | 'compiled' | 'tests');
if (full) {
toolsPane?.maximize();
} else {
@@ -5470,8 +6144,8 @@ const createApi = (): API => {
split?.show('code', full);
if (typeof line === 'number' && line > 0) {
const col = typeof column === 'number' && column > -1 ? column : 0;
- getActiveEditor().setPosition({ lineNumber: line, column: col });
- getActiveEditor().focus();
+ getActiveEditor()?.setPosition({ lineNumber: line, column: col });
+ getActiveEditor()?.focus();
}
} else {
throw new Error(window.deps.translateString('core.error.invalidPanelId', 'Invalid panel id'));
@@ -5573,7 +6247,7 @@ const createApi = (): API => {
};
};
-const initApp = async (config: Partial, baseUrl: string) => {
+const initApp = async (config: Partial, baseUrl: string) => {
window.deps = {
showMode,
translateString: translateStringMock,
@@ -5588,7 +6262,7 @@ const initApp = async (config: Partial, baseUrl: string) => {
return createApi();
};
-const initEmbed = async (config: Partial, baseUrl: string) => {
+const initEmbed = async (config: Partial, baseUrl: string) => {
window.deps = {
showMode,
translateString: translateStringMock,
@@ -5604,7 +6278,7 @@ const initEmbed = async (config: Partial, baseUrl: string) => {
return createApi();
};
-const initHeadless = async (config: Partial, baseUrl: string) => {
+const initHeadless = async (config: Partial, baseUrl: string) => {
window.deps = {
showMode: () => undefined,
translateString: translateStringMock,
diff --git a/src/livecodes/editor/binary-file-editor.ts b/src/livecodes/editor/binary-file-editor.ts
new file mode 100644
index 0000000000..046e0029e4
--- /dev/null
+++ b/src/livecodes/editor/binary-file-editor.ts
@@ -0,0 +1,120 @@
+import type { CodeEditor, EditorOptions } from '../models';
+import { createFakeEditor } from './fake-editor';
+
+export const createBinaryFileEditor = (options: EditorOptions): CodeEditor => {
+ if (!options.container) return createFakeEditor(options);
+ const container = document.createElement('div');
+ container.classList.add('binary-file-editor');
+ options.container.appendChild(container);
+
+ const editor = createFakeEditor(options);
+
+ type Listener = () => void;
+ const listeners: Listener[] = [];
+ const onContentChanged = (fn: Listener) => {
+ listeners.push(fn);
+ };
+
+ const setValue = (value: string = '') => {
+ editor.setValue(value);
+ listeners.forEach((fn) => fn());
+ if (value) {
+ showFile();
+ } else {
+ showFileSelector();
+ }
+ };
+
+ const fileSelector = document.createElement('input');
+ fileSelector.type = 'file';
+ fileSelector.onchange = (ev) => {
+ const file = (ev.target as HTMLInputElement).files?.[0];
+ if (!file) return;
+
+ // Max 2 MB allowed
+ const maxSizeAllowed = 2 * 1024 * 1024;
+ if (file.size > maxSizeAllowed) {
+ alert(
+ window.deps.translateString(
+ 'generic.error.exceededSize',
+ 'Error: Exceeded size {{size}} MB',
+ { size: 2 },
+ ),
+ );
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = () => {
+ setValue(reader.result as string);
+ showFile();
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const showFileSelector = () => {
+ const btn = document.createElement('button');
+ btn.innerHTML = window.deps.translateString('app.binaryFileEditor.selectFile', 'Select file');
+ btn.onclick = () => fileSelector.click();
+ container.innerHTML = '';
+ container.appendChild(btn);
+ };
+
+ const showFile = () => {
+ const src = editor.getValue();
+
+ if (!src) {
+ showFileSelector();
+ return;
+ }
+
+ let display;
+ if (src.startsWith('data:video')) {
+ display = document.createElement('video');
+ display.src = src;
+ } else {
+ display = document.createElement('img');
+ if (src.startsWith('data:image')) {
+ display.src = src;
+ } else if (src.startsWith('data:audio')) {
+ display.src = options.baseUrl + 'assets/images/audio.svg';
+ display.classList.add('icon');
+ } else if (src.startsWith('data:font')) {
+ display.src = options.baseUrl + 'assets/images/font.svg';
+ display.classList.add('icon');
+ } else {
+ display.src = options.baseUrl + 'assets/images/file.svg';
+ display.classList.add('icon');
+ }
+ }
+
+ const link = document.createElement('a');
+ link.title = window.deps.translateString('app.binaryFileEditor.selectFile', 'Select file');
+ link.onclick = (ev) => {
+ ev.preventDefault();
+ fileSelector.click();
+ };
+ link.appendChild(display);
+
+ container.innerHTML = '';
+ container.appendChild(link);
+ };
+
+ if (!editor.getValue()) {
+ showFileSelector();
+ } else {
+ showFile();
+ }
+
+ const destroy = () => {
+ listeners.length = 0;
+ container.remove();
+ };
+
+ return {
+ ...editor,
+ setValue,
+ onContentChanged,
+ destroy,
+ };
+};
diff --git a/src/livecodes/editor/codejar/codejar.ts b/src/livecodes/editor/codejar/codejar.ts
index 182496bfec..383ec5ab97 100644
--- a/src/livecodes/editor/codejar/codejar.ts
+++ b/src/livecodes/editor/codejar/codejar.ts
@@ -10,6 +10,7 @@ import 'prismjs/components/prism-typescript';
import 'prismjs/plugins/autoloader/prism-autoloader';
import 'prismjs/plugins/line-numbers/prism-line-numbers';
+import { getFileLanguage } from '../../languages';
import type {
CodeEditor,
CodejarTheme,
@@ -32,13 +33,17 @@ Prism.manual = true;
Prism.plugins.autoloader.languages_path = prismBaseUrl;
export const createEditor = async (options: EditorOptions): Promise => {
- const { container, mode, editorId, readonly, isEmbed, getFormatterConfig, getFontFamily } =
- options;
+ const { container, mode, readonly, isEmbed, getFormatterConfig, getFontFamily } = options;
if (!container) throw new Error('editor container not found');
- let { value, language } = options;
+ let { value, language, editorId } = options;
let currentPosition: EditorPosition = { lineNumber: 1 };
- const mapLanguage = (lang: Language) => options.mapLanguage?.(lang, 'codejar');
+ const mapLanguage = (lang: Language) => {
+ if (!lang) return 'html';
+ if (editorId.endsWith('.js')) return 'javascript';
+ if (editorId.endsWith('.ts')) return 'typescript';
+ return options.mapLanguage?.(lang, 'codejar') || lang;
+ };
let mappedLanguage = mapLanguage(language);
let editorOptions: ReturnType;
@@ -135,6 +140,13 @@ export const createEditor = async (options: EditorOptions): Promise
// codejar?.onPaste(handleUpdate);
const getEditorId = () => editorId;
+ const setEditorId = (filename: string, lang?: Language) => {
+ editorId = filename;
+ const newLang = lang || getFileLanguage(filename, {});
+ if (newLang && newLang !== language) {
+ setLanguage(newLang);
+ }
+ };
const getValue = () => (codejar ? codejar.toString() : value);
const setValue = (newValue = '\n') => {
value = newValue;
@@ -157,6 +169,7 @@ export const createEditor = async (options: EditorOptions): Promise
const getLanguage = () => language;
const setLanguage = (lang: Language, newValue?: string) => {
+ if (!lang) return;
language = lang;
mappedLanguage = mapLanguage(language);
codeElement.className = 'language-' + mappedLanguage;
@@ -427,6 +440,7 @@ export const createEditor = async (options: EditorOptions): Promise
getLanguage,
setLanguage,
getEditorId,
+ setEditorId,
focus,
getPosition,
setPosition: (position) => setPosition(position),
diff --git a/src/livecodes/editor/codemirror/codemirror.ts b/src/livecodes/editor/codemirror/codemirror.ts
index 4e1543029d..ccaf3fba27 100644
--- a/src/livecodes/editor/codemirror/codemirror.ts
+++ b/src/livecodes/editor/codemirror/codemirror.ts
@@ -29,7 +29,7 @@ import { colorPicker } from '@replit/codemirror-css-color-picker';
// these are imported normally
import { getEditorModeNode } from '../../UI/selectors';
-import { getLanguageSpecs } from '../../languages';
+import { getFileLanguage, getLanguageSpecs } from '../../languages';
import type {
CodeEditor,
CodemirrorTheme,
@@ -46,7 +46,6 @@ import { ctrl, debounce, getRandomString } from '../../utils/utils';
import { codeMirrorBaseUrl, codemirrorMinimapUrl, comlinkBaseUrl } from '../../vendors';
import { getEditorTheme } from '../themes';
import { codemirrorThemes, customThemes } from './codemirror-themes';
-import { editorLanguages } from './editor-languages';
// export type CodeiumEditor = Pick & {
// editorId: EditorOptions['editorId'];
@@ -57,23 +56,15 @@ let tabFocusMode = false;
const changeTabFocusMode = debounce(() => (tabFocusMode = !tabFocusMode), 50);
export const createEditor = async (options: EditorOptions): Promise => {
- const {
- container,
- readonly,
- isEmbed,
- editorId,
- getFormatterConfig,
- getFontFamily,
- getLanguageExtension,
- } = options;
+ const { container, readonly, isEmbed, getFormatterConfig, getFontFamily, getLanguageExtension } =
+ options;
+ let { editorId, language } = options;
let editorSettings: EditorConfig = { ...options };
if (!container) throw new Error('editor container not found');
const getLanguageSupport = async (lang: Language): Promise => {
const langSupport = getLanguageSpecs(lang)?.editorSupport?.codemirror?.languageSupport;
- if (!langSupport) {
- return editorLanguages[lang]?.() || editorLanguages.html?.() || [];
- }
+ if (!langSupport) return [];
const loadLanguage: () => Promise =
typeof langSupport === 'string'
? (await import(langSupport)).default
@@ -83,10 +74,10 @@ export const createEditor = async (options: EditorOptions): Promise
return loadLanguage();
};
- const mapLanguage = (lang: Language) => {
- if (lang.startsWith('vue')) return 'vue';
- if (lang.startsWith('svelte')) return 'svelte';
- if (lang === 'liquid') return 'liquid';
+ const mapLanguage = (lang: Language | undefined) => {
+ if (!lang) return 'html';
+ if (editorId.endsWith('.ts')) return 'typescript';
+ if (editorId.endsWith('.js')) return 'javascript';
return options.mapLanguage?.(lang, 'codemirror') || lang;
};
@@ -103,7 +94,6 @@ export const createEditor = async (options: EditorOptions): Promise
const defaultThemes: Record = { dark: 'one-dark', light: 'cm-light' };
const getActiveTheme = () => themes[theme] || themes[defaultThemes[options.theme]] || [];
- let language = options.language;
let mappedLanguage = mapLanguage(language);
let mappedLanguageSupport = await getLanguageSupport(mappedLanguage);
let theme: CodemirrorTheme = await loadTheme(options.theme, options.editorTheme);
@@ -171,7 +161,7 @@ export const createEditor = async (options: EditorOptions): Promise
mappedLanguage === 'typescript' && !ext?.endsWith('ts') && !ext?.endsWith('tsx')
? ext + '.tsx'
: ext;
- const path = `/${editorId}.${random}.${extension}`;
+ const path = editorId.includes('.') ? `/${editorId}` : `/${editorId}.${random}.${extension}`;
codemirrorTS = codemirrorTS || [
tsFacetWorker.of({ worker: tsWorker, path }),
@@ -333,6 +323,14 @@ export const createEditor = async (options: EditorOptions): Promise
showEditorMode(options.editorMode);
const getEditorId = () => editorId;
+ const setEditorId = (filename: string, lang?: Language) => {
+ editorId = filename;
+ const newLang = lang || getFileLanguage(filename, {});
+ if (newLang && newLang !== language) {
+ setLanguage(newLang);
+ }
+ tsLoaded.then(() => loadTS(true));
+ };
const getValue = () => view.state.doc.toString();
const setValue = (value = '', newState = true) => {
if (newState) {
@@ -350,6 +348,7 @@ export const createEditor = async (options: EditorOptions): Promise
const focus = () => view.focus();
const getLanguage = () => language;
const setLanguage = (lang: Language, value?: string) => {
+ if (!lang) return;
language = lang;
mappedLanguage = mapLanguage(language);
getLanguageSupport(mappedLanguage).then((langSupport) => {
@@ -358,9 +357,7 @@ export const createEditor = async (options: EditorOptions): Promise
effects: [languageExtension.reconfigure(mappedLanguageSupport)],
});
});
- tsLoaded.then(() => {
- loadTS(true);
- });
+ tsLoaded.then(() => loadTS(true));
if (value != null) {
setValue(value);
}
@@ -552,6 +549,7 @@ export const createEditor = async (options: EditorOptions): Promise
getLanguage,
setLanguage,
getEditorId,
+ setEditorId,
focus,
getPosition,
setPosition,
diff --git a/src/livecodes/editor/codemirror/editor-languages.ts b/src/livecodes/editor/codemirror/editor-languages.ts
deleted file mode 100644
index b921ceb36b..0000000000
--- a/src/livecodes/editor/codemirror/editor-languages.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-disable import/no-unresolved */
-// @ts-ignore
-import type { LanguageSupport } from '@codemirror/language';
-// @ts-ignore
-import { json } from '@codemirror/lang-json';
-
-import type { Language } from '../../models';
-
-export const editorLanguages: Partial<{ [key in Language]: () => Promise }> = {
- json: async () => json(),
-};
diff --git a/src/livecodes/editor/create-editor.ts b/src/livecodes/editor/create-editor.ts
index 5fa27664b0..85dffde055 100644
--- a/src/livecodes/editor/create-editor.ts
+++ b/src/livecodes/editor/create-editor.ts
@@ -1,5 +1,7 @@
+import { getFileLanguage } from '../languages/utils';
import type { CodeEditor, Config, EditorOptions } from '../models';
import { isMobile, loadStylesheet } from '../utils';
+import { createBinaryFileEditor } from './binary-file-editor';
import { createFakeEditor } from './fake-editor';
import { fonts } from './fonts';
@@ -30,21 +32,23 @@ const selectEditor = (options: EditorOptions & { activeEditor?: Config['activeEd
const { editor, mode, editorId, activeEditor, isLite, isHeadless } = options;
const auto = isMobile() ? 'codemirror' : 'monaco';
return (
- (isHeadless
- ? 'fake'
- : mode === 'result' && editorId !== 'console' && editorId !== 'compiled'
+ (getFileLanguage(editorId, {}) === 'binary'
+ ? 'binary'
+ : isHeadless
? 'fake'
- : mode === 'simple' && editorId !== activeEditor
+ : mode === 'result' && editorId !== 'console' && editorId !== 'compiled'
? 'fake'
- : ['codemirror', 'monaco', 'codejar'].includes(editor || '')
- ? editor
- : editor === 'auto'
- ? auto
- : mode === 'simple' && editorId === activeEditor
- ? 'codemirror'
- : mode === 'codeblock' || isLite
- ? 'codejar'
- : auto) || 'monaco'
+ : mode === 'simple' && editorId !== activeEditor
+ ? 'fake'
+ : ['codemirror', 'monaco', 'codejar'].includes(editor || '')
+ ? editor
+ : editor === 'auto'
+ ? auto
+ : mode === 'simple' && editorId === activeEditor
+ ? 'codemirror'
+ : mode === 'codeblock' || isLite
+ ? 'codejar'
+ : auto) || 'monaco'
);
};
@@ -93,9 +97,11 @@ export const createEditor = async (
if (!options) throw new Error();
const editorOptions = getEditorOptions(options);
+ if (!options.language) return createFakeEditor(editorOptions);
const editorName = selectEditor(editorOptions);
if (editorName === 'fake') return createFakeEditor(editorOptions);
+ if (editorName === 'binary') return createBinaryFileEditor(editorOptions);
if (editorOptions.fontFamily) {
loadFont(editorOptions.fontFamily);
diff --git a/src/livecodes/editor/fake-editor.ts b/src/livecodes/editor/fake-editor.ts
index ef354afcf3..862a6ffbd4 100644
--- a/src/livecodes/editor/fake-editor.ts
+++ b/src/livecodes/editor/fake-editor.ts
@@ -1,9 +1,8 @@
-import { getLanguageEditorId } from '../languages';
+import { getFileLanguage } from '../languages';
import type { CodeEditor, EditorOptions } from '../models';
export const createFakeEditor = (options: EditorOptions): CodeEditor => {
- let value = options.value;
- let language = options.language;
+ let { value, language, editorId } = options;
return {
getValue: () => value,
setValue: (v = '') => {
@@ -16,7 +15,11 @@ export const createFakeEditor = (options: EditorOptions): CodeEditor => {
value = v;
}
},
- getEditorId: () => getLanguageEditorId(language) || 'markup',
+ getEditorId: () => editorId,
+ setEditorId: (fileName) => {
+ editorId = fileName;
+ language = getFileLanguage(fileName, {}) || language;
+ },
focus: () => undefined,
getPosition: () => ({ lineNumber: 1, column: 1 }),
setPosition: () => undefined,
diff --git a/src/livecodes/editor/monaco/monaco-languages.ts b/src/livecodes/editor/monaco/monaco-languages.ts
new file mode 100644
index 0000000000..5135d08be5
--- /dev/null
+++ b/src/livecodes/editor/monaco/monaco-languages.ts
@@ -0,0 +1,38 @@
+import type * as Monaco from 'monaco-editor';
+
+import type { Language } from '../../models';
+import { monacoLanguagesBaseUrl } from '../../vendors';
+
+export interface CustomLanguageDefinition {
+ config?: Monaco.languages.LanguageConfiguration;
+ tokens?: Monaco.languages.IMonarchLanguage;
+ completions?: Monaco.languages.CompletionItemProvider;
+ definitions?: Monaco.languages.DefinitionProvider;
+ init?: (monaco: typeof Monaco) => void;
+}
+
+export const customLanguages: Partial> = {
+ astro: monacoLanguagesBaseUrl + 'astro.js',
+ clio: monacoLanguagesBaseUrl + 'clio.js',
+ imba: monacoLanguagesBaseUrl + 'imba.js',
+ json5: monacoLanguagesBaseUrl + 'json5.js',
+ minizinc: monacoLanguagesBaseUrl + 'minizinc.js',
+ prolog: monacoLanguagesBaseUrl + 'prolog.js',
+ // sql: monacoLanguagesBaseUrl + 'sql.js', // TODO: add autocomplete
+ vue: monacoLanguagesBaseUrl + 'vue.js',
+ svelte: monacoLanguagesBaseUrl + 'svelte.js',
+ wat: monacoLanguagesBaseUrl + 'wat.js',
+
+ dotenv: {
+ config: { comments: { lineComment: '#' } },
+ tokens: {
+ tokenizer: {
+ root: [
+ [/#.*$/, 'comment'],
+ [/([a-zA-Z_][a-zA-Z0-9_]*)(=)([^#]*)(#.*)$/, ['key', 'delimiter', 'value', 'comment']],
+ [/([a-zA-Z_][a-zA-Z0-9_]*)(=)(.*)$/, ['key', 'delimiter', 'value']],
+ ],
+ },
+ },
+ },
+};
diff --git a/src/livecodes/editor/monaco/monaco.ts b/src/livecodes/editor/monaco/monaco.ts
index 0cda24d149..72ef1f0750 100644
--- a/src/livecodes/editor/monaco/monaco.ts
+++ b/src/livecodes/editor/monaco/monaco.ts
@@ -1,8 +1,9 @@
import type * as Monaco from 'monaco-editor';
-import { getEditorModeNode } from '../../UI/selectors';
+import { getEditorModeNode, getEditorTab } from '../../UI/selectors';
import { getImports } from '../../compiler/import-map';
import { getLanguageSpecs, hasJsx } from '../../languages';
+import { getFileLanguage } from '../../languages/utils';
import type {
APIError,
CodeEditor,
@@ -52,14 +53,20 @@ export const createEditor = async (options: EditorOptions): Promise
editorTheme,
isEmbed,
getLanguageExtension,
- mapLanguage,
getFormatterConfig,
getFontFamily,
} = options;
- let language = options.language;
+ let { editorId, language } = options;
if (!container) throw new Error('editor container not found');
+ const mapLanguage = (lang: Language | undefined) => {
+ if (!lang) return 'html';
+ if (editorId.endsWith('.ts')) return 'typescript';
+ if (editorId.endsWith('.js')) return 'javascript';
+ return options.mapLanguage?.(lang, 'monaco') || lang;
+ };
+
const loadMonaco = () => import(monacoBaseUrl + 'monaco.js');
let editorMode: any | undefined;
@@ -186,7 +193,6 @@ export const createEditor = async (options: EditorOptions): Promise
...consoleOptions,
};
- const editorId = options.editorId;
const initOptions =
editorId === 'console'
? consoleOptions
@@ -202,10 +208,7 @@ export const createEditor = async (options: EditorOptions): Promise
const configureTypeScriptFeatures = () => {
const JSLangs = ['javascript', 'jsx', 'react', 'flow', 'solid', 'react-native'];
const isJSLang = JSLangs.includes(language);
- if (
- !['script', 'tests', 'editorSettings'].includes(editorId) ||
- !['javascript', 'typescript'].includes(mapLanguage(language, 'monaco'))
- ) {
+ if (!['javascript', 'typescript'].includes(mapLanguage(language))) {
return;
}
@@ -257,7 +260,7 @@ export const createEditor = async (options: EditorOptions): Promise
const getOrCreateModel = (value: string, lang: string | undefined, uri: Monaco.Uri) => {
const model = monaco.editor.getModel(uri);
if (model) {
- if (model.getLanguageId() === mapLanguage(lang as Language, 'monaco')) {
+ if (model.getLanguageId() === mapLanguage(lang as Language)) {
model.setValue(value);
return model;
}
@@ -275,20 +278,14 @@ export const createEditor = async (options: EditorOptions): Promise
const random = getRandomString();
const ext = getLanguageExtension(language);
const extension =
- mapLanguage(language, 'monaco') === 'typescript' &&
- !ext?.endsWith('ts') &&
- !ext?.endsWith('tsx')
+ mapLanguage(language) === 'typescript' && !ext?.endsWith('ts') && !ext?.endsWith('tsx')
? ext + '.tsx'
: ext;
modelUri = editorId.includes('.')
? `file:///${editorId}`
: `file:///${editorId}.${random}.${extension}`;
const oldModel = editor.getModel();
- const model = getOrCreateModel(
- value || '',
- mapLanguage(language, 'monaco'),
- monaco.Uri.parse(modelUri),
- );
+ const model = getOrCreateModel(value || '', mapLanguage(language), monaco.Uri.parse(modelUri));
editor.setModel(model);
setTimeout(() => oldModel?.dispose(), 1000); // avoid race https://github.com/microsoft/monaco-editor/issues/1715
updateListeners();
@@ -297,9 +294,9 @@ export const createEditor = async (options: EditorOptions): Promise
const editor = monaco.editor.create(container, {
...editorOptions,
- language: mapLanguage(language, 'monaco'),
+ language: mapLanguage(language),
});
- setModel(editor, options.value, mapLanguage(language, 'monaco'));
+ setModel(editor, options.value, mapLanguage(language));
if (editorId.includes('.')) {
editors.push(editor);
@@ -317,6 +314,11 @@ export const createEditor = async (options: EditorOptions): Promise
}
const getEditorId = () => editorId;
+ const setEditorId = (filename: string, lang?: Language) => {
+ editorId = filename;
+ language = lang || getFileLanguage(filename, {}) || language;
+ setModel(editor, editor.getValue(), language);
+ };
const getValue = () => editor.getValue();
const setValue = (value = '') => {
editor.getModel()?.setValue(value);
@@ -504,7 +506,7 @@ export const createEditor = async (options: EditorOptions): Promise
const registerFormatter = (formatFn: FormatFn | undefined) => {
if (!formatFn) return;
- monaco.languages.registerDocumentFormattingEditProvider(mapLanguage(language, 'monaco'), {
+ monaco.languages.registerDocumentFormattingEditProvider(mapLanguage(language), {
provideDocumentFormattingEdits: async (model) => {
if (!model || model.isDisposed()) return [];
const val = model.getValue() || '';
@@ -681,8 +683,8 @@ export const createEditor = async (options: EditorOptions): Promise
const model = editor.getModel();
if (
!model ||
- !addCloseLanguages.includes(mapLanguage(language, 'monaco')) ||
- (mapLanguage(language, 'monaco') === 'typescript' && !hasJsx(language)) || // avoid autocompleting TS generics
+ !addCloseLanguages.includes(mapLanguage(language)) ||
+ (mapLanguage(language) === 'typescript' && !hasJsx(language)) || // avoid autocompleting TS generics
editorOptions.autoClosingBrackets === 'never'
) {
return;
@@ -871,8 +873,8 @@ export const createEditor = async (options: EditorOptions): Promise
const targetEditorId = resource.path.slice(1); // remove leading slash
if (targetEditorId) {
- // const targetEditorTab = getEditorTab(targetEditorId);
- // targetEditorTab?.click();
+ const targetEditorTab = getEditorTab(targetEditorId);
+ targetEditorTab?.click();
if (monaco.Range.isIRange(selectionOrPosition)) {
targetEditor?.revealRangeInCenterIfOutsideViewport(
@@ -908,6 +910,7 @@ export const createEditor = async (options: EditorOptions): Promise
getLanguage,
setLanguage,
getEditorId,
+ setEditorId,
focus,
getPosition,
setPosition,
diff --git a/src/livecodes/export/export-src.ts b/src/livecodes/export/export-src.ts
index 7e4c085ff0..4159d01a91 100644
--- a/src/livecodes/export/export-src.ts
+++ b/src/livecodes/export/export-src.ts
@@ -1,4 +1,4 @@
-import type { getLanguageExtension as getLanguageExtensionFn } from '../languages';
+import { getFileLanguage, type getLanguageExtension as getLanguageExtensionFn } from '../languages';
import type { Config, EditorId } from '../models';
import { downloadFile, loadScript } from '../utils/utils';
import { jsZipUrl } from '../vendors';
@@ -22,7 +22,12 @@ export const exportSrc = async (
const files = getFilesFromConfig(config, deps);
(Object.keys(files) as EditorId[]).forEach((filename) => {
- zip.file(filename, files[filename]?.content);
+ const content = files[filename]?.content || '';
+ if (getFileLanguage(filename, config) === 'binary') {
+ zip.file(filename, content.split('base64,')[1] || '', { base64: true });
+ } else {
+ zip.file(filename, content);
+ }
});
zip.file('result.html', html);
zip.file('livecodes.json', JSON.stringify(config, null, 2));
diff --git a/src/livecodes/export/utils.ts b/src/livecodes/export/utils.ts
index b08352c5f6..32e516246e 100644
--- a/src/livecodes/export/utils.ts
+++ b/src/livecodes/export/utils.ts
@@ -1,4 +1,5 @@
import { replaceImports } from '../compiler/import-map';
+import { getSource, isEditorId } from '../config/utils';
import type {
getLanguageCompiler as getLanguageCompilerFn,
getLanguageExtension as getLanguageExtensionFn,
@@ -22,16 +23,28 @@ export const getFilesFromConfig = (
style: 'style',
script: 'script',
};
- const codeFiles = (Object.keys(filenames) as EditorId[]).reduce((files, editorId) => {
- const filename = filenames[editorId];
- const language = config[editorId].language;
- const extension = getLanguageExtension?.(language) || 'md';
- const content = config[editorId].content || '';
- return {
- ...files,
- ...(content ? { [filename + '.' + extension]: { content } } : {}),
- };
- }, {});
+ const codeFiles =
+ config.files.length > 0
+ ? config.files.reduce(
+ (files, file) => ({
+ ...files,
+ [file.filename]: { content: file.content },
+ }),
+ {},
+ )
+ : (Object.keys(filenames) as EditorId[]).reduce((files, editorId) => {
+ if (!isEditorId(editorId)) {
+ return files;
+ }
+ const filename = filenames[editorId];
+ const language = config[editorId].language;
+ const extension = getLanguageExtension?.(language) || 'md';
+ const content = config[editorId].content || '';
+ return {
+ ...files,
+ ...(content ? { [filename + '.' + extension]: { content } } : {}),
+ };
+ }, {});
const externalStyles =
config.stylesheets.length > 0
@@ -106,10 +119,10 @@ export const getCompilerScripts = ({
supportedLanguages: { [key in EditorId]: Language[] };
getLanguageCompiler: typeof getLanguageCompilerFn;
}) => {
- if (supportedLanguages[editorId].includes(config[editorId].language)) return [];
- const compilerScripts = getLanguageCompiler?.(config[editorId].language)?.scripts;
- const compiledCode =
- config[editorId].language === 'python' ? config[editorId].content || '' : compiled[editorId];
+ const src = getSource(editorId, config);
+ if (!src || supportedLanguages[editorId].includes(src.language)) return [];
+ const compilerScripts = getLanguageCompiler?.(src.language)?.scripts;
+ const compiledCode = src.language === 'python' ? src.content || '' : compiled[editorId];
const scripts =
typeof compilerScripts === 'function'
? compilerScripts({ compiled: compiledCode, baseUrl, config })
@@ -126,7 +139,7 @@ export const getContent = ({
}: {
editorId: EditorId;
config: Config;
- compiled: { [key in EditorId]: string };
+ compiled: Partial<{ [key in EditorId]: string }>;
supportedLanguages: { [key in EditorId]: Language[] };
getLanguageCompiler: typeof getLanguageCompilerFn;
}) => {
@@ -137,19 +150,23 @@ export const getContent = ({
const content = {
markup: ['html', ...supportedLanguages.markup].includes(config.markup.language)
? config.markup.content
- : compiled.markup,
+ : compiled.markup || '',
style: ['css', ...supportedLanguages.style].includes(config.style.language)
? config.style.content
- : compiled.style,
+ : compiled.style || '',
script:
config.script.language === 'php'
? config.script.content?.replace(/<\?php/g, '') || ''
: config.script.language === 'python'
? config.script.content
: replaceImports(
- (isScriptSupported ? config.script.content : compiled.script) || '',
+ (isScriptSupported ? config.script.content : compiled.script || '') || '',
config,
),
+ files: config.files.map((file) => ({
+ ...file,
+ content: replaceImports(file.content, config),
+ })),
};
const scriptType = getLanguageCompiler?.(config.script.language)?.scriptType;
@@ -171,5 +188,5 @@ ${escapeScript(content.script || '')}
return '';
}
}
- return content[editorId] || '';
+ return getSource(editorId, content as any)?.content || '';
};
diff --git a/src/livecodes/handlers/keyboard-shortcuts.ts b/src/livecodes/handlers/keyboard-shortcuts.ts
index 4791bcba1e..516e7b6ea5 100644
--- a/src/livecodes/handlers/keyboard-shortcuts.ts
+++ b/src/livecodes/handlers/keyboard-shortcuts.ts
@@ -1,5 +1,6 @@
import type { createSplitPanes } from '../UI';
import * as UI from '../UI/selectors';
+import { getSource } from '../config/utils';
import type { CodeEditor, Config, EditorId, EventsManager, ToolsPane } from '../models';
import { ctrl } from '../utils';
@@ -8,7 +9,7 @@ import { ctrl } from '../utils';
*/
export interface KeyboardShortcutDeps {
eventsManager: EventsManager;
- getActiveEditor: () => CodeEditor;
+ getActiveEditor: () => CodeEditor | undefined;
getConfig: () => Config;
showEditor: (editorId: EditorId) => void;
run: () => Promise;
@@ -27,7 +28,7 @@ let lastkeys = '';
*/
const createCommandPaletteHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) => {
const activeEditor = deps.getActiveEditor();
- if (ctrl(e) && e.code === 'KeyP' && activeEditor.monaco) {
+ if (ctrl(e) && e.code === 'KeyP' && activeEditor?.monaco) {
e.preventDefault();
activeEditor.monaco.trigger('anyString', 'editor.action.quickCommand');
lastkeys = 'Ctrl + P';
@@ -108,7 +109,7 @@ const createZoomToggleHandler = () => (e: KeyboardEvent) => {
const createFocusEditorHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) => {
if (ctrl(e) && e.altKey && e.code === 'KeyE') {
e.preventDefault();
- deps.getActiveEditor().focus();
+ deps.getActiveEditor()?.focus();
lastkeys = 'Ctrl + Alt + E';
return true;
}
@@ -149,25 +150,26 @@ const createEscapeHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) =
// Ctrl + Alt + (1-3) activates editor 1-3
// Ctrl + Alt + (ArrowLeft/ArrowRight) activates previous/next editor
const createEditorSwitchHandler = (deps: KeyboardShortcutDeps) => (e: KeyboardEvent) => {
- if (ctrl(e) && e.altKey && ['1', '2', '3', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
- const editorIds = (['markup', 'style', 'script'] as const).filter(
- (id) => deps.getConfig()[id].hideTitle !== true,
- );
-
+ const config = deps.getConfig();
+ const editorIds = (
+ config.files.length
+ ? config.files.map((f) => f.filename)
+ : (['markup', 'style', 'script'] as EditorId[])
+ ).filter((id) => getSource(id, config)?.hidden !== true);
+ const editorNumbers = editorIds.map((_, id) => String(id + 1));
+
+ if (ctrl(e) && e.altKey && [...editorNumbers, 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault();
deps.split?.show('code');
-
- const index = ['1', '2', '3'].includes(e.key)
+ const index = editorNumbers.includes(e.key)
? Number(e.key) - 1
: e.key === 'ArrowLeft'
- ? editorIds.findIndex((id) => id === deps.getConfig().activeEditor) - 1 || 0
+ ? editorIds.findIndex((id) => id === config.activeEditor) - 1 || 0
: e.key === 'ArrowRight'
- ? editorIds.findIndex((id) => id === deps.getConfig().activeEditor) + 1 || 0
+ ? editorIds.findIndex((id) => id === config.activeEditor) + 1 || 0
: 0;
-
const editorIndex =
index === editorIds.length ? 0 : index === -1 ? editorIds.length - 1 : index;
-
deps.showEditor(editorIds[editorIndex]);
lastkeys = 'Ctrl + Alt + ' + e.key;
return true;
diff --git a/src/livecodes/html/app.html b/src/livecodes/html/app.html
index a5764a655b..067d88e71a 100644
--- a/src/livecodes/html/app.html
+++ b/src/livecodes/html/app.html
@@ -132,7 +132,15 @@
-
+
+
+
+
+
+
+
+
+ -
+ Loading multi-file templates...
+
+
+
+
Loading user templates...
diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json
index a8b3bafb45..5333300054 100644
--- a/src/livecodes/i18n/locales/en/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/en/translation.lokalise.json
@@ -100,6 +100,10 @@
"notes": "",
"translation": "SDK Permanent URL"
},
+ "app.binaryFileEditor.selectFile": {
+ "notes": "",
+ "translation": "Select file"
+ },
"app.changeTheme.hint": {
"notes": "",
"translation": "Change Theme"
@@ -932,6 +936,10 @@
"notes": "",
"translation": "Loading {{lang}}. This may take a while!"
},
+ "core.confirm.deleteFile": {
+ "notes": "",
+ "translation": "Delete file: {{filename}}?"
+ },
"core.copy.copied": {
"notes": "",
"translation": "Code copied to clipboard"
@@ -1024,6 +1032,18 @@
"notes": "",
"translation": "Creating a public GitHub gist..."
},
+ "core.file.exists": {
+ "notes": "",
+ "translation": "File already exists!"
+ },
+ "core.file.invalidName": {
+ "notes": "",
+ "translation": "Invalid file name!"
+ },
+ "core.file.invalidType": {
+ "notes": "",
+ "translation": "Invalid file type!"
+ },
"core.fork.success": {
"notes": "",
"translation": "Forked as a new project"
@@ -1040,10 +1060,6 @@
"notes": "",
"translation": "Generating..."
},
- "core.import.loading": {
- "notes": "",
- "translation": "Loading Project..."
- },
"core.layout.horizontal": {
"notes": "",
"translation": "Horizontal layout"
@@ -1646,7 +1662,7 @@
},
"import.code.desc": {
"notes": "### ###\n
\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n
\n\n### ###\n\n\n",
- "translation": "Supported Sources: GitHub gist GitHub file Directory in a GitHub repo Gitlab snippet Gitlab file Directory in a Gitlab repo JS Bin Raw code Code in web page DOM Code in zip file Code in image (OCR) Official playgrounds (TypeScript and Vue) Please visit the documentations for details."
+ "translation": "Supported Sources: GitHub gist GitHub file Directory in a GitHub repo Gitlab snippet Gitlab file Directory in a Gitlab repo JS Bin Raw code Code in web page DOM Code in zip file Code in image (OCR) Official playgrounds (TypeScript, Vue, Svelte, Preact and Solid) Please visit the documentations for details."
},
"import.code.fromFile": {
"notes": "",
@@ -2528,6 +2544,58 @@
"notes": "",
"translation": "New Project"
},
+ "templates.multifile.basic": {
+ "notes": "",
+ "translation": "Basic Template"
+ },
+ "templates.multifile.blank": {
+ "notes": "",
+ "translation": "Blank Template"
+ },
+ "templates.multifile.heading": {
+ "notes": "",
+ "translation": "Multi-file Templates"
+ },
+ "templates.multifile.javascript": {
+ "notes": "",
+ "translation": "JavaScript Template"
+ },
+ "templates.multifile.jest": {
+ "notes": "",
+ "translation": "Jest Template"
+ },
+ "templates.multifile.lit": {
+ "notes": "",
+ "translation": "Lit Template"
+ },
+ "templates.multifile.loading": {
+ "notes": "",
+ "translation": "Loading multi-file templates..."
+ },
+ "templates.multifile.preact": {
+ "notes": "",
+ "translation": "Preact Template"
+ },
+ "templates.multifile.react": {
+ "notes": "",
+ "translation": "React Template"
+ },
+ "templates.multifile.solid": {
+ "notes": "",
+ "translation": "Solid Template"
+ },
+ "templates.multifile.svelte": {
+ "notes": "",
+ "translation": "Svelte Template"
+ },
+ "templates.multifile.typescript": {
+ "notes": "",
+ "translation": "TypeScript Template"
+ },
+ "templates.multifile.vue": {
+ "notes": "",
+ "translation": "Vue Template"
+ },
"templates.noUserTemplates.desc": {
"notes": "### ###\n \n\n",
"translation": "You can save a project as a template from (App menu > Save as > Template)."
diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts
index 9b0e91d6e2..7713048877 100644
--- a/src/livecodes/i18n/locales/en/translation.ts
+++ b/src/livecodes/i18n/locales/en/translation.ts
@@ -57,6 +57,9 @@ const translation = {
},
},
app: {
+ binaryFileEditor: {
+ selectFile: 'Select file',
+ },
changeTheme: {
hint: 'Change Theme',
},
@@ -377,6 +380,9 @@ const translation = {
hint: 'Change Language',
message: 'Loading {{lang}}. This may take a while!',
},
+ confirm: {
+ deleteFile: 'Delete file: {{filename}}?',
+ },
copy: {
copied: 'Code copied to clipboard',
copiedAsDataURL: 'Code copied as data URL',
@@ -406,6 +412,11 @@ const translation = {
export: {
gist: 'Creating a public GitHub gist...',
},
+ file: {
+ exists: 'File already exists!',
+ invalidName: 'Invalid file name!',
+ invalidType: 'Invalid file type!',
+ },
fork: {
success: 'Forked as a new project',
},
@@ -414,9 +425,6 @@ const translation = {
exit: 'Exit Full Screen',
},
generating: 'Generating...',
- import: {
- loading: 'Loading Project...',
- },
layout: {
horizontal: 'Horizontal layout',
responsive: 'Responsive layout',
@@ -642,7 +650,7 @@ const translation = {
started: 'Bulk import started...',
},
code: {
- desc: 'Supported Sources: <1> <2>GitHub gist2> <3>GitHub file3> <4>Directory in a GitHub repo4> <5>Gitlab snippet5> <6>Gitlab file6> <7>Directory in a Gitlab repo7> <8>JS Bin8> <9>Raw code9> <10>Code in web page DOM10> <11>Code in zip file11> <12>Code in image (OCR)12> <13>Official playgrounds<14>14>(TypeScript and Vue)13> 1> Please visit the <15>documentations15> for details.',
+ desc: 'Supported Sources: <1> <2>GitHub gist2> <3>GitHub file3> <4>Directory in a GitHub repo4> <5>Gitlab snippet5> <6>Gitlab file6> <7>Directory in a Gitlab repo7> <8>JS Bin8> <9>Raw code9> <10>Code in web page DOM10> <11>Code in zip file11> <12>Code in image (OCR)12> <13>Official playgrounds<14>14>(TypeScript, Vue, Svelte, Preact and Solid)13> 1> Please visit the <15>documentations15> for details.',
fromFile: 'Import local files',
fromURL: 'Import from URL',
heading: 'Import Code',
@@ -973,6 +981,21 @@ const translation = {
},
templates: {
heading: 'New Project',
+ multifile: {
+ basic: 'Basic Template',
+ blank: 'Blank Template',
+ heading: 'Multi-file Templates',
+ javascript: 'JavaScript Template',
+ jest: 'Jest Template',
+ lit: 'Lit Template',
+ loading: 'Loading multi-file templates...',
+ preact: 'Preact Template',
+ react: 'React Template',
+ solid: 'Solid Template',
+ svelte: 'Svelte Template',
+ typescript: 'TypeScript Template',
+ vue: 'Vue Template',
+ },
noUserTemplates: {
desc: 'You can save a project as a template from <1>1>(App menu > Save as > Template).',
heading: 'You have no saved templates.',
diff --git a/src/livecodes/import/check-src.ts b/src/livecodes/import/check-src.ts
index 59ff522eca..d5c2c7d6b8 100644
--- a/src/livecodes/import/check-src.ts
+++ b/src/livecodes/import/check-src.ts
@@ -14,7 +14,9 @@ export const hostPatterns = {
jsbin: /^(?:(?:(?:http|https):\/\/)?(?:\w+.)?)?jsbin\.com\/((\w)+(\/\d+)?)(?:.*)/g,
typescriptPlayground: /^(?:(?:http|https):\/\/)?(?:www\.)?typescriptlang\.org\/play(?:.*)/g,
vuePlayground: /^(?:(?:http|https):\/\/)?play\.vuejs\.org(?:.*)/g,
- sveltePlayground: /^(?:(?:http|https):\/\/)?svelte\.dev\/repl\/(?:.*)/g,
+ sveltePlayground: /^(?:(?:http|https):\/\/)?svelte\.dev\/playground\/(?:.*)/g,
+ preactPlayground: /^(?:(?:http|https):\/\/)?preactjs\.com\/repl(?:.*)/g,
+ solidPlayground: /^(?:(?:http|https):\/\/)?playground\.solidjs\.com\/(?:.*)/g,
};
export const isCompressedCode = (url: string) => url.startsWith('code/');
@@ -109,3 +111,13 @@ export const isSveltePlayground = (
url: string,
pattern = new RegExp(hostPatterns.sveltePlayground),
) => pattern.test(url);
+
+export const isPreactPlayground = (
+ url: string,
+ pattern = new RegExp(hostPatterns.preactPlayground),
+) => pattern.test(url);
+
+export const isSolidPlayground = (
+ url: string,
+ pattern = new RegExp(hostPatterns.solidPlayground),
+) => pattern.test(url);
diff --git a/src/livecodes/import/code.ts b/src/livecodes/import/code.ts
index 1490e9311e..5ef18e89d4 100644
--- a/src/livecodes/import/code.ts
+++ b/src/livecodes/import/code.ts
@@ -1,11 +1,11 @@
-import type { Config } from '../models';
+import type { SDKConfig } from '../models';
import { decompress } from '../utils/compression';
import { isCompressedCode } from './check-src';
export const importCompressedCode = (url: string) => {
if (!isCompressedCode(url)) return {};
const code = url.slice('code/'.length);
- let config: Partial;
+ let config: Partial;
try {
config = JSON.parse(decompress(code) || '{}');
} catch (error) {
diff --git a/src/livecodes/import/dom.ts b/src/livecodes/import/dom.ts
index fd7f2a7451..57c78b7659 100644
--- a/src/livecodes/import/dom.ts
+++ b/src/livecodes/import/dom.ts
@@ -1,3 +1,4 @@
+import { isEditorId } from '../config/utils';
import { getLanguageByAlias, getLanguageEditorId } from '../languages';
import type { Config, EditorId, Language } from '../models';
import { decodeHTML } from '../utils';
@@ -70,7 +71,7 @@ export const importFromDom = async (
{} as { [key: string]: string },
);
- const configSelectors = (['markup', 'style', 'script'] as EditorId[]).reduce(
+ const configSelectors = (['markup', 'style', 'script'] as const).reduce(
(selectors: Selectors, editorId) => {
if (config[editorId].language && config[editorId].selector) {
return {
@@ -89,11 +90,11 @@ export const importFromDom = async (
const defaultSelectors = getLanguageSelectors(defaultParams);
const paramSelectors = getLanguageSelectors(selectorParams);
- const languageSelectors: Selectors = {
+ const languageSelectors = {
...defaultSelectors,
...configSelectors,
...paramSelectors,
- };
+ } as Selectors;
const selectedCode = (Object.keys(languageSelectors) as EditorId[]).reduce(
(selectedCodeConfig: Partial, editorId) => {
@@ -119,7 +120,7 @@ export const importFromDom = async (
const defaults = Object.keys(defaultParams).reduce(
(defaultsConfig: Partial, language: string) => {
const editorId = getLanguageEditorId(language as Language);
- if (!editorId || selectedCode[editorId]) return defaultsConfig;
+ if (!editorId || !isEditorId(editorId) || selectedCode[editorId]) return defaultsConfig;
const code = extractCodeFromHTML(dom, defaultParams[language]);
if (code === undefined) return defaultsConfig;
diff --git a/src/livecodes/import/files.ts b/src/livecodes/import/files.ts
index 61365e835f..2cd87c9f19 100644
--- a/src/livecodes/import/files.ts
+++ b/src/livecodes/import/files.ts
@@ -1,59 +1,238 @@
-import type { ContentConfig, EventsManager } from '../models';
+import { getFileLanguage } from '../languages/utils';
import { importFromImage } from './image';
-import type { SourceFile, populateConfig as populateConfigFn } from './utils';
+import { populateConfig } from './utils';
import { importFromZip } from './zip';
export const importFromFiles = async (
- files: FileList,
- populateConfig: typeof populateConfigFn,
- eventsManager: EventsManager,
+ // Use DataTransferItemList interface for folder support
+ entries: { files: FileList; items?: DataTransferItemList | undefined },
+ multiFile = false,
) => {
- const loadFiles = (files: FileList) =>
- new Promise>((resolve, reject) => {
- const sourceFiles: SourceFile[] = [];
-
- for (const file of files) {
- // Max 100 MB allowed
- const maxSizeAllowed = 100 * 1024 * 1024;
- if (file.size > maxSizeAllowed) {
- reject('Error: Exceeded size 100 MB');
- return;
+ if (!entries.items?.length && !entries.files?.length) return {};
+
+ if (entries.files?.length === 1 && !multiFile) {
+ const file = entries.files[0];
+ if (file.type.startsWith('image/') && file.type !== 'image/svg+xml') {
+ return importFromImage(file);
+ } else if (file.name.endsWith('.zip')) {
+ return importFromZip(file, populateConfig);
+ }
+ }
+
+ const localFiles = filterFiles(await getLocalFiles(entries));
+
+ if (!localFiles?.length) return {};
+ return populateConfig(localFiles, {});
+};
+
+export const filterFiles = (files: T[]) => {
+ const maxSizeAllowed = 100 * 1024 * 1024; // 100 MB
+ const localFiles = files.filter(
+ (file) =>
+ !file.path.startsWith('.') && // e.g. .git, .vscode
+ !file.path.includes('node_modules') &&
+ !file.path.startsWith('dist') &&
+ !file.filename.endsWith('-lock.json') &&
+ !file.filename.endsWith('.zip') &&
+ (!file.size || file.size <= maxSizeAllowed) &&
+ getFileLanguage(file.filename, {}) != null,
+ );
+ localFiles.sort((a, b) => {
+ if (a.filename.endsWith('.md')) return 1;
+ if (b.filename.endsWith('.md')) return -1;
+
+ if (!a.path.includes('/') && a.filename.endsWith('.json')) return 1;
+ if (!b.path.includes('/') && b.filename.endsWith('.json')) return -1;
+
+ if (!a.path.includes('/') && a.filename.includes('.config.')) return 1;
+ if (!b.path.includes('/') && b.filename.includes('.config.')) return -1;
+
+ if (a.path.startsWith('public/')) return 1;
+ if (b.path.startsWith('public/')) return -1;
+
+ return 0;
+ });
+
+ return localFiles;
+};
+
+interface LocalFile {
+ filename: string;
+ content: string;
+ path: string;
+ size?: number;
+ error?: boolean;
+}
+
+export const getLocalFiles = async (
+ entries: { items: DataTransferItemList | undefined } | { files: FileList | undefined },
+): Promise => {
+ interface FileInfo {
+ file: File | FileSystemEntry;
+ path: string;
+ }
+
+ if ('items' in entries) {
+ if (!entries.items?.length) return [];
+ return handleItems(entries.items);
+ } else if ('files' in entries) {
+ if (!entries.files?.length) return [];
+ return handleFiles(entries.files);
+ } else {
+ return [];
+ }
+
+ async function handleItems(items: DataTransferItemList) {
+ const entries: Array = [];
+
+ for (const item of items) {
+ // Use webkitGetAsEntry for folder support (works in most modern browsers)
+ if (item.webkitGetAsEntry) {
+ const entry = item.webkitGetAsEntry();
+ if (entry) {
+ entries.push(entry);
}
+ } else if (item.getAsFile) {
+ // Fallback for browsers without webkitGetAsEntry
+ const file = item.getAsFile();
+ if (file) {
+ entries.push(file);
+ }
+ }
+ }
- const reader = new FileReader();
- eventsManager.addEventListener(reader, 'load', (event: any) => {
- const text = (event.target?.result as string) || '';
- sourceFiles.push({
- filename: file.name,
- content: text,
- });
-
- if (sourceFiles.length === files.length) {
- resolve(populateConfig(sourceFiles, {}));
- }
+ if (entries.length === 0) return [];
+ try {
+ const files = await getAllFiles(entries);
+ return processFiles(files);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('Error processing files:', error);
+ return [];
+ }
+ }
+
+ async function handleFiles(files: FileList) {
+ const fileData = [...files].map((file) => ({
+ file,
+ path: file.webkitRelativePath || file.name,
+ }));
+
+ return processFiles(fileData);
+ }
+
+ async function getAllFiles(entries: Array) {
+ const files: Array<{ file: File; path: string }> = [];
+
+ async function traverseEntry(entry: FileInfo['file'], path = '') {
+ if (isFile(entry)) {
+ return new Promise((resolve) => {
+ entry.file(
+ (file) => {
+ files.push({
+ file,
+ path: path + file.name,
+ });
+ resolve();
+ },
+ (error) => {
+ // eslint-disable-next-line no-console
+ console.error('Error reading file:', error);
+ resolve();
+ },
+ );
});
+ } else if (isDir(entry)) {
+ const dirReader = entry.createReader();
+
+ return new Promise((resolve) => {
+ const readEntries = () => {
+ dirReader.readEntries(
+ async (entries: FileSystemEntry[]) => {
+ if (entries.length === 0) {
+ resolve();
+ return;
+ }
- eventsManager.addEventListener(reader, 'error', () => {
- reject('Error: Failed to read file');
+ for (const childEntry of entries) {
+ await traverseEntry(childEntry, path + entry.name + '/');
+ }
+
+ // Continue reading (directories might have more than 100 entries)
+ readEntries();
+ },
+ (error: Error) => {
+ // eslint-disable-next-line no-console
+ console.error('Error reading directory:', error);
+ resolve();
+ },
+ );
+ };
+
+ readEntries();
});
+ } else if (entry instanceof File) {
+ // Fallback for File objects
+ files.push({
+ file: entry,
+ path: entry.name,
+ });
+ }
+ }
+ for (const entry of entries) {
+ await traverseEntry(entry);
+ }
+
+ return files;
+ }
+
+ async function processFiles(fileData: Array<{ file: File; path: string }>) {
+ const processedFiles = [];
+ for (const { file, path } of fileData) {
+ try {
+ const content = await readFileContent(file);
+ processedFiles.push({
+ filename: file.name,
+ content,
+ path,
+ size: file.size,
+ });
+ } catch (error: any) {
+ // eslint-disable-next-line no-console
+ console.error(`Error reading file: ${path}.`, error.message || error);
+ }
+ }
+ // Sort by path
+ processedFiles.sort((a, b) => a.path.localeCompare(b.path));
+ return processedFiles;
+ }
+
+ function readFileContent(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ let content = reader.result || '';
+ if (typeof content !== 'string') {
+ const decoder = new TextDecoder('utf-8');
+ content = decoder.decode(content);
+ }
+ resolve(content);
+ };
+ reader.onerror = () => reject(reader.error);
+ if (getFileLanguage(file.name, {}) === 'binary') {
+ reader.readAsDataURL(file);
+ } else {
reader.readAsText(file);
}
});
+ }
- const loadZipFile = (files: FileList) => importFromZip(files[0], populateConfig);
- const loadImage = (files: FileList) => importFromImage(files[0]);
-
- if (!files?.length) return {};
-
- const getConfigFromFiles =
- files?.length > 1
- ? loadFiles
- : files[0].name.endsWith('.zip')
- ? loadZipFile
- : files[0].type.startsWith('image/') && files[0].type !== 'image/svg+xml'
- ? loadImage
- : loadFiles;
+ function isFile(x: any): x is FileSystemFileEntry {
+ return 'isFile' in x && x.isFile;
+ }
- return getConfigFromFiles(files);
+ function isDir(x: any): x is FileSystemDirectoryEntry {
+ return 'isDirectory' in x && x.isDirectory;
+ }
};
diff --git a/src/livecodes/import/github-dir.ts b/src/livecodes/import/github-dir.ts
index f468dc78fc..e59caaaaac 100644
--- a/src/livecodes/import/github-dir.ts
+++ b/src/livecodes/import/github-dir.ts
@@ -1,9 +1,17 @@
import { decode } from 'js-base64';
import type { User } from '../models';
import { getGithubHeaders } from '../services/github';
-import { modifyMarkup } from './github';
+import { filterFiles } from './files';
import { populateConfig } from './utils';
+interface GitHubFile {
+ path: string;
+ type: 'blob' | 'tree';
+ size: number;
+ sha: string;
+ url: string;
+}
+
export const importFromGithubDir = async (
url: string,
params: { [key: string]: string },
@@ -38,7 +46,7 @@ export const importFromGithubDir = async (
}
const apiURL = `https://api.github.com/repos/${user}/${repository}/git/trees/${branch}?recursive=true`;
- const tree = await fetch(apiURL, {
+ const tree: GitHubFile[] = await fetch(apiURL, {
...(loggedInUser ? { headers: getGithubHeaders(loggedInUser) } : {}),
})
.then((res) => {
@@ -47,53 +55,69 @@ export const importFromGithubDir = async (
})
.then((data) => data.tree);
- const dirFiles = tree.filter((node: any) =>
- rootDir
- ? node.type === 'blob'
- : node.type === 'blob' &&
- node.path.startsWith(decodeURIComponent(dir)) &&
- node.path.split('/').length === dir.split('/').length + 1,
- );
+ const maxFiles = 20;
+ const dirFiles = filterFiles(
+ tree
+ .filter((node) =>
+ rootDir
+ ? node.type === 'blob'
+ : node.type === 'blob' && node.path.startsWith(decodeURIComponent(dir) + '/'),
+ )
+ // comply to file shape expected by filterFiles
+ .map((file) => ({
+ ...file,
+ filename: file.path.split('/')[file.path.split('/').length - 1],
+ size: file.size,
+ content: '',
+ })),
+ ).filter((_file, index: number) => index < maxFiles);
const files = await Promise.all(
- Object.values(dirFiles).map(async (file: any) => {
+ dirFiles.map(async (file) => {
const filename = decodeURIComponent(file.path.split('/')[file.path.split('/').length - 1]);
- const content = decode(
- await fetch(file.url, {
- ...(loggedInUser ? { headers: getGithubHeaders(loggedInUser) } : {}),
+ const encodedContent = await fetch(file.url, {
+ ...(loggedInUser ? { headers: getGithubHeaders(loggedInUser) } : {}),
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error('Cannot fetch: ' + file.url);
+ return res.json();
})
- .then((res) => {
- if (!res.ok) throw new Error('Cannot fetch: ' + file.url);
- return res.json();
- })
- .then((data) => data.content),
- );
-
+ .then((data) => {
+ const extension = file.path.split('.')[file.path.split('.').length - 1];
+ if (
+ [
+ 'png',
+ 'jpg',
+ 'jpeg',
+ 'gif',
+ 'webp',
+ 'bmp',
+ 'tif',
+ 'tiff',
+ 'ico',
+ 'ttf',
+ 'otf',
+ 'woff',
+ 'woff2',
+ ].includes(extension)
+ ) {
+ return `data:${getBinaryMimeType(extension)};charset=UTF-8;base64,${data.content}`;
+ }
+ return data.content;
+ });
+ const content = encodedContent.startsWith('data:')
+ ? encodedContent.replaceAll('\n', '')
+ : decode(encodedContent);
+ const relativePath = dir ? file.path.replace(`${dir}/`, '') : file.path;
return {
filename,
content,
- path: file.path,
+ path: relativePath,
};
}),
);
- const config = populateConfig(files, params);
-
- return modifyMarkup(
- config,
- files
- .filter((f) =>
- [config.markup?.content, config.style?.content, config.script?.content].includes(
- f.content,
- ),
- )
- .map((f) => ({
- user,
- repo: repository,
- ref: branch,
- path: f.path,
- })),
- );
+ return populateConfig(files, params);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Cannot fetch directory: ' + url);
@@ -102,3 +126,12 @@ export const importFromGithubDir = async (
return {};
}
};
+
+const getBinaryMimeType = (extension: string) => {
+ let type = 'image';
+ if (extension === 'ico') return `${type}/x-icon`;
+ if (extension === 'jpg') return `${type}/jpeg`;
+ if (extension === 'tif') return `${type}/tiff`;
+ if (['ttf', 'otf', 'woff', 'woff2'].includes(extension)) type = 'font';
+ return `${type}/${extension}`;
+};
diff --git a/src/livecodes/import/github-gist.ts b/src/livecodes/import/github-gist.ts
index b81306abfe..2f339bb003 100644
--- a/src/livecodes/import/github-gist.ts
+++ b/src/livecodes/import/github-gist.ts
@@ -1,4 +1,4 @@
-import { getLanguageByAlias } from '../languages';
+import { getFileExtension, getLanguageByAlias } from '../languages';
import { getValidUrl } from './check-src';
import { populateConfig } from './utils';
@@ -18,7 +18,7 @@ export const importFromGithubGist = async (url: string, params: { [key: string]:
.then((files) =>
Object.values(files).map((file: any) => {
const lang = file.language;
- const extension = file.filename.split('.')[file.filename.split('.').length - 1];
+ const extension = getFileExtension(file.filename);
const language = getLanguageByAlias(extension) || getLanguageByAlias(lang);
return {
...file,
diff --git a/src/livecodes/import/github.ts b/src/livecodes/import/github.ts
index 1f4c695433..29e686f2ad 100644
--- a/src/livecodes/import/github.ts
+++ b/src/livecodes/import/github.ts
@@ -1,8 +1,7 @@
import { decode } from 'js-base64';
-import { getLanguageByAlias, getLanguageEditorId, getLanguageExtension } from '../languages/utils';
-import type { Config, EditorId, Language, User } from '../models';
+import { getFileExtension, getLanguageByAlias, getLanguageEditorId } from '../languages/utils';
+import type { Config, Language, User } from '../models';
import { getGithubHeaders } from '../services/github';
-import { modulesService } from '../services/modules';
const getValidUrl = (url: string) =>
url.startsWith('https://') ? new URL(url) : new URL('https://' + url);
@@ -15,7 +14,7 @@ const getFileData = (urlObj: URL): FileData => {
const ref = pathSplit[4];
const filePath = pathSplit.slice(5, pathSplit.length).join('/');
const filename = filePath.split('/')[filePath.split('/').length - 1];
- const extension = (filename.split('.')[filename.split('.').length - 1] || 'md') as Language;
+ const extension = getFileExtension(filename) as Language;
const lineSplit = urlObj.hash.split('-');
const startLine = urlObj.hash !== '' ? Number(lineSplit[0].replace('#L', '')) : -1;
const endLine =
@@ -69,6 +68,18 @@ const getContent = async (
}
}
+ if (!(startLine > 0)) {
+ return {
+ files: [
+ {
+ filename: fileData.filename,
+ language: getLanguageByAlias(extension) || 'html',
+ content: fileContent,
+ },
+ ],
+ };
+ }
+
const content =
startLine > 0
? fileContent
@@ -78,14 +89,13 @@ const getContent = async (
: fileContent;
const language = getLanguageByAlias(extension) || 'html';
const editorId = getLanguageEditorId(language) || 'markup';
- const config = {
+ return {
[editorId]: {
language,
content,
},
activeEditor: editorId,
};
- return modifyMarkup(config, [fileData]);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Cannot fetch: ' + apiUrl);
@@ -101,80 +111,3 @@ export const importFromGithub = (
const fileData = getFileData(validUrl);
return getContent(fileData, loggedInUser);
};
-
-/**
- * Adds base tag for relative links.
- * Removes , , and `,
- 'g',
- );
- const htmlTagPattern = new RegExp(
- `(?:\\s*?[\\n\\r]*)|(?:]{1,200}?)>\\s*?[\\n\\r]*)|(?:\\s*<\/html>)`,
- 'g',
- );
- if (tag === 'html') {
- const result = [...markupContent.matchAll(htmlTagPattern)];
- for (const match of result) {
- if (match[1]) {
- htmlAttrs = match[1];
- }
- }
- }
- const pattern = tag === 'html' ? htmlTagPattern : tag === 'link' ? linkPattern : scriptPattern;
- return markupContent.replace(pattern, '');
- };
-
- let content = removeTags(config.markup.content || '', 'html');
- content = removeTags(content, 'link');
- content = removeTags(content, 'script');
-
- return {
- ...config,
- ...(htmlAttrs ? { htmlAttrs } : {}),
- head: `${baseTag}\n${config.head || ''}`,
- markup: {
- ...config.markup,
- content,
- },
- };
-};
diff --git a/src/livecodes/import/gitlab-dir.ts b/src/livecodes/import/gitlab-dir.ts
index 460c1aeeac..7e51e0c6c8 100644
--- a/src/livecodes/import/gitlab-dir.ts
+++ b/src/livecodes/import/gitlab-dir.ts
@@ -22,16 +22,24 @@ export const importFromGitlabDir = async (url: string, params: { [key: string]:
const branch = pathSplit[5] || repoInfo.default_branch;
const projectId = repoInfo.id;
const dir = pathSplit.slice(6, pathSplit.length).join('/');
- const apiURL = `${urlObj.origin}/api/v4/projects/${projectId}/repository/tree?per_page=100&ref=${branch}&path=${dir}`;
- const dirFiles = await fetch(apiURL)
- .then((res) => {
- if (!res.ok) throw new Error('Cannot fetch: ' + apiURL);
- return res.json();
- })
- .then((data) => data.filter((node: any) => node.type === 'blob'));
+ const getAllFiles = async (dirPath: string) => {
+ const apiURL = `${urlObj.origin}/api/v4/projects/${projectId}/repository/tree?per_page=100&ref=${branch}&path=${dirPath}`;
+ const dirFiles: Array<{ name: string; path: string; type: string }> = await fetch(apiURL)
+ .then((res) => {
+ if (!res.ok) throw new Error('Cannot fetch: ' + apiURL);
+ return res.json();
+ })
+ .then((data) =>
+ Promise.all(
+ data.map((node: any) => (node.type === 'blob' ? node : getAllFiles(node.path))),
+ ),
+ );
+ return dirFiles.flat();
+ };
+ const allFiles = await getAllFiles(dir);
const files = await Promise.all(
- Object.values(dirFiles).map(async (file: any) => {
+ Object.values(allFiles).map(async (file: any) => {
const filename = file.path.split('/')[file.path.split('/').length - 1];
const rawURL = `${
urlObj.origin
@@ -42,10 +50,12 @@ export const importFromGitlabDir = async (url: string, params: { [key: string]:
if (!res.ok) throw new Error('Cannot fetch: ' + file.url);
return res.text();
});
+ const relativePath = dir ? file.path.replace(`${dir}/`, '') : file.path;
return {
filename,
content,
+ path: relativePath,
};
}),
);
diff --git a/src/livecodes/import/image.ts b/src/livecodes/import/image.ts
index 1e13b10abf..c795a98ca8 100644
--- a/src/livecodes/import/image.ts
+++ b/src/livecodes/import/image.ts
@@ -1,5 +1,5 @@
import { detectLanguage, getLanguageByAlias, getLanguageEditorId } from '../languages';
-import type { ContentConfig } from '../models';
+import type { SDKConfig } from '../models';
import { blobToBase64, loadScript } from '../utils/utils';
import { metaPngUrl, tesseractUrl } from '../vendors';
import { importCompressedCode } from './code';
@@ -86,7 +86,7 @@ const cleanUpCode = async (code: string) => {
return code;
};
-export const importFromImage = async (blob: Blob): Promise> => {
+export const importFromImage = async (blob: Blob): Promise> => {
try {
const metaPng: any = await loadScript(metaPngUrl, 'MetaPNG');
const arrayBuffer = await blob.arrayBuffer();
diff --git a/src/livecodes/import/import-src.ts b/src/livecodes/import/import-src.ts
index 27ca5ba69d..880bec326d 100644
--- a/src/livecodes/import/import-src.ts
+++ b/src/livecodes/import/import-src.ts
@@ -7,6 +7,9 @@ export { importFromGitlab } from './gitlab';
export { importFromGitlabDir } from './gitlab-dir';
export { importFromGitlabSnippet } from './gitlab-snippet';
export { importFromJsbin } from './jsbin';
+export { importPreactPlayground } from './preact-playground';
+export { importSolidPlayground } from './solid-playground';
+export { importSveltePlayground } from './svelte-playground';
export { importTypescriptPlayground } from './typescript-playground';
export { importFromUrl } from './url';
export { importVuePlayground } from './vue-playground';
diff --git a/src/livecodes/import/import.ts b/src/livecodes/import/import.ts
index 9bbbbdf352..baa565a35e 100644
--- a/src/livecodes/import/import.ts
+++ b/src/livecodes/import/import.ts
@@ -1,4 +1,4 @@
-import type { Config, User } from '../models';
+import type { Config, SDKConfig, User } from '../models';
import { getValidUrl } from '../utils/utils';
import {
isCodepen,
@@ -11,7 +11,10 @@ import {
isGitlabSnippet,
isGitlabUrl,
isJsbin,
+ isPreactPlayground,
isProjectId,
+ isSolidPlayground,
+ isSveltePlayground,
isTypescriptPlayground,
isVuePlayground,
} from './check-src';
@@ -24,7 +27,7 @@ export const importCode = async (
config: Config,
user: User | null | void,
baseUrl: string,
-): Promise> => {
+): Promise> => {
if (isCompressedCode(url)) {
return importCompressedCode(url);
}
@@ -45,6 +48,9 @@ export const importCode = async (
importFromJsbin,
importTypescriptPlayground,
importVuePlayground,
+ importPreactPlayground,
+ importSolidPlayground,
+ importSveltePlayground,
importFromUrl,
} = importSrc;
@@ -81,6 +87,15 @@ export const importCode = async (
if (isVuePlayground(url)) {
return importVuePlayground(url);
}
+ if (isPreactPlayground(url)) {
+ return importPreactPlayground(url);
+ }
+ if (isSolidPlayground(url)) {
+ return importSolidPlayground(url);
+ }
+ if (isSveltePlayground(url)) {
+ return importSveltePlayground(url);
+ }
if (getValidUrl(url)) {
return importFromUrl(url, params, config);
}
diff --git a/src/livecodes/import/preact-playground.ts b/src/livecodes/import/preact-playground.ts
new file mode 100644
index 0000000000..575dc23332
--- /dev/null
+++ b/src/livecodes/import/preact-playground.ts
@@ -0,0 +1,93 @@
+import type { Config } from '../models';
+import { modulesService } from '../services';
+
+// https://github.com/preactjs/preact-www/blob/master/src/components/controllers/repl/query-encode.js
+function base64ToText(base64: string) {
+ const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
+ return new TextDecoder().decode(bytes);
+}
+
+// https://github.com/preactjs/preact-www/blob/master/src/components/controllers/repl/examples/index.js
+const EXAMPLES = [
+ {
+ slug: 'counter',
+ file: 'counters/counter.txt',
+ },
+ {
+ slug: 'counter-hooks',
+ file: 'counters/counter-hooks.txt',
+ },
+ {
+ slug: 'counter-signals',
+ file: 'counters/counter-signals.txt',
+ },
+ {
+ slug: 'counter-htm',
+ file: 'counters/counter-htm.txt',
+ },
+ {
+ slug: 'todo',
+ file: 'todo-lists/todo-list.txt',
+ },
+ {
+ slug: 'todo-signals',
+ file: 'todo-lists/todo-list-signals.txt',
+ },
+ {
+ slug: 'github-repo-list',
+ file: 'github-repo-list.txt',
+ },
+ {
+ slug: 'context',
+ file: 'context.txt',
+ },
+ {
+ slug: 'spiral',
+ file: 'spiral.txt',
+ },
+];
+
+const getExample = (slug: string) => EXAMPLES.find((e) => e.slug === slug)?.file;
+
+export const importPreactPlayground = async (url: string): Promise> => {
+ const exampleQuery = '?example=';
+ const codeQuery = '?code=';
+ const query = url.includes(exampleQuery)
+ ? exampleQuery
+ : url.includes(codeQuery)
+ ? codeQuery
+ : null;
+ if (!query) return {};
+ const code = url.split(query)[1];
+ if (!code?.trim()) return {};
+ const contentUrl =
+ query === exampleQuery
+ ? modulesService.getModuleUrl(
+ `gh:preactjs/preact-www@master/src/components/controllers/repl/examples/` +
+ getExample(code),
+ )
+ : undefined;
+ const content = query === codeQuery ? base64ToText(decodeURIComponent(code)) : undefined;
+ if (!contentUrl && !content) return {};
+ return {
+ activeEditor: 'script',
+ markup: {
+ language: 'html',
+ content: `
+
+`,
+ },
+ style: {
+ language: 'css',
+ contentUrl: modulesService.getModuleUrl(
+ 'gh:preactjs/preact-www@master/src/components/controllers/repl/examples/style.css',
+ ),
+ },
+ script: {
+ language: 'jsx',
+ content,
+ contentUrl,
+ },
+ customSettings: { typescript: { jsxImportSource: 'preact' } },
+ };
+};
diff --git a/src/livecodes/import/solid-playground.ts b/src/livecodes/import/solid-playground.ts
new file mode 100644
index 0000000000..d8f8aaf95a
--- /dev/null
+++ b/src/livecodes/import/solid-playground.ts
@@ -0,0 +1,57 @@
+import type { Config } from '../models';
+
+interface PlaygroundFile {
+ name: string;
+ content: string;
+}
+
+export const importSolidPlayground = async (url: string): Promise> => {
+ const id = url.split('/').pop();
+ if (!id?.trim()) return {};
+ const data = await fetch(`https://api.solidjs.com/repl/${id}`).then((res) => res.json());
+ const files: Config['files'] = data.files
+ ?.filter((file: PlaygroundFile) => file.name !== 'import_map.json')
+ .map((file: PlaygroundFile) => ({
+ filename: file.name,
+ content: file.content,
+ }));
+ if (!files?.length) return {};
+ let imports = {};
+ try {
+ imports = JSON.parse(
+ data.files.find((file: PlaygroundFile) => file.name === 'import_map.json')?.content || '{}',
+ );
+ } catch {
+ // ignore
+ }
+ return {
+ activeEditor: files.find((f) => f.filename === 'main.tsx')?.filename,
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+
+
+
+
+
+
+
+`,
+ },
+ ...files,
+ ],
+ customSettings: {
+ fileLanguages: {
+ jsx: 'solid',
+ tsx: 'solid.tsx',
+ },
+ imports,
+ },
+ };
+};
diff --git a/src/livecodes/import/svelte-playground.ts b/src/livecodes/import/svelte-playground.ts
new file mode 100644
index 0000000000..0e85a3bfba
--- /dev/null
+++ b/src/livecodes/import/svelte-playground.ts
@@ -0,0 +1,84 @@
+/* eslint-disable import/no-internal-modules */
+import type { Config } from '../models';
+import { corsService } from '../services/cors';
+
+// https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/src/routes/(authed)/playground/%5Bid%5D/gzip.js
+async function decode_and_decompress_text(input: string) {
+ const decoded = atob(input.replaceAll('-', '+').replaceAll('_', '/'));
+ // putting it directly into the blob gives a corrupted file
+ const u8 = new Uint8Array(decoded.length);
+ for (let i = 0; i < decoded.length; i++) {
+ u8[i] = decoded.charCodeAt(i);
+ }
+ const stream = new Blob([u8]).stream().pipeThrough(new DecompressionStream('gzip'));
+ return new Response(stream).text();
+}
+
+export const importSveltePlayground = async (url: string): Promise> => {
+ const code = url.split('#')[1] || parent.location.hash.split('#')[1];
+ let files = [];
+ try {
+ if (code) {
+ const data = JSON.parse(await decode_and_decompress_text(code));
+ files = data.files
+ .filter((f: any) => f.type === 'file')
+ .map((f: any) => ({
+ filename: f.name,
+ content: f.contents,
+ }));
+ } else {
+ const id = url.split('?')[0].split('/').pop();
+ const dataUrl = `https://svelte.dev/playground/${id}/__data.json?x-sveltekit-invalidated=001`;
+ const res = await corsService.fetch(dataUrl);
+ if (!res.ok) return {};
+ const json = await res.json();
+ const data = json.nodes.find((n: any) => n.data).data;
+ files = data
+ .filter((e: any) => e && typeof e === 'object' && e.name && e.type && e.source)
+ .map((e: any) => ({
+ filename: data[e.name] + '.' + data[e.type],
+ content: data[e.source],
+ }));
+ }
+ } catch {
+ //
+ }
+
+ if (!files?.length) return {};
+ return {
+ activeEditor: files.find((f: any) => f.filename === 'App.svelte')?.filename,
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'main.ts',
+ language: 'typescript',
+ content: `import { mount } from "svelte";
+import App from "./App.svelte";
+
+const app = mount(App, {
+ target: document.getElementById("app"),
+});
+
+export default app;
+`,
+ },
+ ...files,
+ ],
+ };
+};
diff --git a/src/livecodes/import/typescript-playground.ts b/src/livecodes/import/typescript-playground.ts
index b0bec6a2ef..450f08ffdb 100644
--- a/src/livecodes/import/typescript-playground.ts
+++ b/src/livecodes/import/typescript-playground.ts
@@ -2,7 +2,7 @@ import { decompressFromEncodedURIComponent } from 'lz-string';
import type { Config } from '../models';
export const importTypescriptPlayground = async (url: string): Promise> => {
- const code = url.split('#code/')[1];
+ const code = url.split('#code/')[1] || parent.location.hash.split('#code/')[1];
if (!code?.trim()) return {};
const ts = decompressFromEncodedURIComponent(code);
if (!ts?.trim()) return {};
diff --git a/src/livecodes/import/url.ts b/src/livecodes/import/url.ts
index 2ab25c3afb..909679e68d 100644
--- a/src/livecodes/import/url.ts
+++ b/src/livecodes/import/url.ts
@@ -58,7 +58,7 @@ export const importFromUrl = async (
(url.startsWith('data:image/') && !url.startsWith('data:image/svg+xml'))
) {
const image = await res.blob();
- return importFromImage(image);
+ return importFromImage(image) as Promise>;
}
const fetchedContent = await res.text();
diff --git a/src/livecodes/import/utils.ts b/src/livecodes/import/utils.ts
index aa938bdb98..19e7acfd2c 100644
--- a/src/livecodes/import/utils.ts
+++ b/src/livecodes/import/utils.ts
@@ -1,19 +1,65 @@
-import { getLanguageByAlias, getLanguageEditorId } from '../languages';
+import { getMainFile, isEditorId } from '../config/utils';
+import { getFileLanguage, getLanguageByAlias, getLanguageEditorId } from '../languages';
import type { Config, EditorId, Language } from '../models';
export interface SourceFile {
filename: string;
content: string;
+ path?: string;
language?: Language;
editorId?: EditorId;
}
+const prepareFiles = (
+ config: { files?: Config['files'] },
+ params: { [key: string]: string },
+): Partial => {
+ if (!config.files?.length) return config;
+ const mainFile = getMainFile(config);
+ if (!mainFile || !config.files.find((f) => f.filename === mainFile)) {
+ return config;
+ }
+ const title = config.files
+ .find((f) => f.filename === mainFile)
+ ?.content.match(/(.*?)<\/title>/)?.[1];
+
+ config.files.sort((f1, f2) => (f1.filename === mainFile ? -1 : f2.filename === mainFile ? 1 : 0));
+ return {
+ ...(title ? { title } : {}),
+ mainFile,
+ activeEditor: String(params.activeEditor || params.active || mainFile),
+ ...detectFramework(config.files),
+ ...config,
+ };
+};
+
+const detectFramework = (files: SourceFile[]): Partial => {
+ const viteConfig = files.find((f) => f.filename.startsWith('vite.config.'));
+ if (viteConfig) {
+ if (viteConfig.content.includes('@vitejs/plugin-react')) {
+ return { customSettings: { fileLanguages: { jsx: 'react', tsx: 'react.tsx' } } };
+ }
+ if (viteConfig.content.includes('vite-plugin-solid')) {
+ return { customSettings: { fileLanguages: { jsx: 'solid', tsx: 'solid.tsx' } } };
+ }
+ if (viteConfig.content.includes('@preact/preset-vite')) {
+ return { customSettings: { typescript: { jsxImportSource: 'preact' } } };
+ }
+ }
+ return {};
+};
+
export const populateConfig = (
files: SourceFile[],
params: { [key: string]: string },
): Partial => {
if (files.length === 0) return {};
+ const commonDir = files[0]?.path?.split('/')[0];
+ if (commonDir && files.every((file) => file.path?.startsWith(`${commonDir}/`))) {
+ files = files.map((file) => ({ ...file, path: file.path?.replace(`${commonDir}/`, '') }));
+ }
+
const configFile = files.find(
(file) =>
file.filename.toLowerCase() === 'livecodes.json' ||
@@ -21,7 +67,16 @@ export const populateConfig = (
);
if (configFile) {
try {
- return JSON.parse(configFile.content);
+ const obj = JSON.parse(configFile.content);
+ if (
+ // if is LiveCodes config
+ obj.markup?.language ||
+ obj.style?.language ||
+ obj.script?.language ||
+ (obj.files?.[0]?.filename && obj.files?.[0]?.content)
+ ) {
+ return obj;
+ }
} catch {
// invalid JSON
}
@@ -30,28 +85,43 @@ export const populateConfig = (
// select files in query params (e.g. ?files=index.html,script.js)
const filesInParams = params.files;
if (filesInParams) {
- return filesInParams
+ const config = filesInParams
.split(',')
.map((filename) => filename.trim())
.reduce((output: Partial, filename: string) => {
- const extension = filename.split('.')[filename.split('.').length - 1];
- const language = getLanguageByAlias(extension);
- if (!language) return output;
const file = files.find((file) => file.filename === filename);
if (!file) return output;
-
- const editorId = getLanguageEditorId(language);
- if (!editorId || output[editorId]) return output;
-
+ const language = getFileLanguage(file.filename, {}) as Language;
return {
...output,
- activeEditor: output.activeEditor || editorId, // set first as active editor
- [editorId]: {
- language,
- content: file.content,
- },
+ files: [
+ ...(output.files || []),
+ {
+ filename: file.path || file.filename,
+ content: file.content,
+ language,
+ },
+ ],
};
}, {} as Partial);
+ return prepareFiles(config, params);
+ }
+
+ // external styles and scripts (e.g. github gist exported from codepen)
+ const stylesFile = files.find((file) => file.filename === 'styles');
+ const scriptsFile = files.find((file) => file.filename === 'scripts');
+
+ if (!stylesFile && !scriptsFile) {
+ return prepareFiles(
+ {
+ files: files.map((file) => ({
+ filename: file.path || file.filename,
+ content: file.content,
+ language: file.language || (getFileLanguage(file.filename, {}) as Language),
+ })),
+ },
+ params,
+ );
}
// select languages from files
@@ -148,7 +218,7 @@ export const populateConfig = (
}
// code
- if (!file.editorId || output[file.editorId]) return output;
+ if (!file.editorId || !isEditorId(file.editorId) || output[file.editorId]) return output;
return {
...output,
[file.editorId]: {
@@ -160,7 +230,6 @@ export const populateConfig = (
// extract external styles and scripts
const stylesheets: string[] = [];
- const stylesFile = files.find((file) => file.filename === 'styles');
if (stylesFile?.content) {
try {
const urls: string[] = [];
@@ -190,7 +259,6 @@ export const populateConfig = (
}
const scripts: string[] = [];
- const scriptsFile = files.find((file) => file.filename === 'scripts');
if (scriptsFile?.content) {
try {
const urls: string[] = [];
diff --git a/src/livecodes/import/vue-playground.ts b/src/livecodes/import/vue-playground.ts
index 00e561486c..5cc9c8aff8 100644
--- a/src/livecodes/import/vue-playground.ts
+++ b/src/livecodes/import/vue-playground.ts
@@ -2,7 +2,7 @@ import type { Config } from '../models';
import { fflateUrl } from '../vendors';
export const importVuePlayground = async (url: string): Promise> => {
- const code = url.split('#')[1];
+ const code = url.split('#')[1] || parent.location.hash.split('#')[1];
if (!code?.trim()) return {};
const { unzlibSync, strToU8, strFromU8 } = await import(fflateUrl);
@@ -22,15 +22,48 @@ export const importVuePlayground = async (url: string): Promise>
if (!str) return {};
try {
const json = JSON.parse(str);
- const file =
- json['App.vue'] ?? json[Object.keys(json).find((key) => key.endsWith('.vue')) || 'App.vue'];
- if (!file) return {};
+ const files: any = Object.keys(json)
+ .filter((filename) => filename !== 'tsconfig.json' && filename !== 'import-map.json')
+ .map((filename) => ({ filename, content: json[filename] }));
+ if (!files.length) return {};
+ let imports = {};
+ try {
+ imports = JSON.parse(json['import-map.json'] || '{}').imports;
+ } catch {
+ // ignore
+ }
return {
- activeEditor: 'script',
- script: {
- language: 'vue',
- content: file,
- },
+ activeEditor: files.find((f: any) => f.filename === 'App.vue')?.filename,
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'main.ts',
+ language: 'typescript',
+ content: `import { createApp } from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')
+`,
+ },
+ ...files,
+ ],
+ customSettings: { imports },
};
} catch {
return {};
diff --git a/src/livecodes/import/zip.ts b/src/livecodes/import/zip.ts
index aa57bc9dce..77f259224d 100644
--- a/src/livecodes/import/zip.ts
+++ b/src/livecodes/import/zip.ts
@@ -1,10 +1,15 @@
import type { ContentConfig } from '../models';
import { loadScript } from '../utils/utils';
import { jsZipUrl } from '../vendors';
-import type { populateConfig as populateConfigFn, SourceFile } from './utils';
+import { filterFiles } from './files';
+import type { populateConfig as populateConfigFn } from './utils';
export const importFromZip = async (blob: Blob, populateConfig: typeof populateConfigFn) =>
new Promise>(async (resolve, reject) => {
+ if (blob.size > 100 * 1024 * 1024) {
+ // > 100 MB
+ reject(new Error('File is too big'));
+ }
const JSZip: any = await loadScript(jsZipUrl, 'JSZip');
JSZip.loadAsync(blob)
@@ -20,20 +25,16 @@ export const importFromZip = async (blob: Blob, populateConfig: typeof populateC
return;
}
- const filesInSrcDir: any[] = zip.file(/((^src\/)|(\/src\/))/);
const allFiles: any[] = zip.file(/.*/);
- const rootFiles = allFiles.filter((file) => !file.name.includes('/'));
- const selectedFiles =
- filesInSrcDir.length > 0 ? filesInSrcDir : rootFiles.length > 0 ? rootFiles : allFiles;
-
- if (selectedFiles.length > 0) {
- const sourceFiles: SourceFile[] = await Promise.all(
- selectedFiles.map(async (file) => ({
- filename: file.name,
+ if (allFiles.length > 0) {
+ const sourceFiles = await Promise.all(
+ allFiles.map(async (file) => ({
+ filename: file.name.split('/').pop(),
content: await file.async('string'),
+ path: file.name,
})),
);
- resolve(populateConfig(sourceFiles, {}));
+ resolve(populateConfig(filterFiles(sourceFiles), {}));
return;
}
diff --git a/src/livecodes/languages/art-template/lang-art-template.ts b/src/livecodes/languages/art-template/lang-art-template.ts
index d499e9a5fc..529abf4d8f 100644
--- a/src/livecodes/languages/art-template/lang-art-template.ts
+++ b/src/livecodes/languages/art-template/lang-art-template.ts
@@ -22,4 +22,5 @@ export const artTemplate: LanguageSpecs = {
extensions: ['art', 'art-template'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/asciidoc/lang-asciidoc.ts b/src/livecodes/languages/asciidoc/lang-asciidoc.ts
index 85de07c8bd..09ecde27da 100644
--- a/src/livecodes/languages/asciidoc/lang-asciidoc.ts
+++ b/src/livecodes/languages/asciidoc/lang-asciidoc.ts
@@ -19,4 +19,5 @@ export const asciidoc: LanguageSpecs = {
},
extensions: ['adoc', 'asciidoc', 'asc'],
editor: 'markup',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/babel/lang-babel.ts b/src/livecodes/languages/babel/lang-babel.ts
index d23cf797cf..4f735e7947 100644
--- a/src/livecodes/languages/babel/lang-babel.ts
+++ b/src/livecodes/languages/babel/lang-babel.ts
@@ -40,4 +40,5 @@ export const babel: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/bbcode/lang-bbcode.ts b/src/livecodes/languages/bbcode/lang-bbcode.ts
index b7873e60cc..16918b533f 100644
--- a/src/livecodes/languages/bbcode/lang-bbcode.ts
+++ b/src/livecodes/languages/bbcode/lang-bbcode.ts
@@ -14,4 +14,5 @@ export const bbcode: LanguageSpecs = {
},
extensions: ['bbcode', 'bb'],
editor: 'markup',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/binary/index.ts b/src/livecodes/languages/binary/index.ts
new file mode 100644
index 0000000000..545e58bf0b
--- /dev/null
+++ b/src/livecodes/languages/binary/index.ts
@@ -0,0 +1 @@
+export * from './lang-binary';
diff --git a/src/livecodes/languages/binary/lang-binary.ts b/src/livecodes/languages/binary/lang-binary.ts
new file mode 100644
index 0000000000..dcf621f09d
--- /dev/null
+++ b/src/livecodes/languages/binary/lang-binary.ts
@@ -0,0 +1,66 @@
+import type { LanguageSpecs } from '../../models';
+
+export const binary: LanguageSpecs = {
+ name: 'binary',
+ title: 'Binary',
+ info: false,
+ compiler: {
+ factory: () => {
+ const getBinaryMimeType = (extension: string) => {
+ let type = 'image';
+ if (extension === 'ico') return `${type}/x-icon`;
+ if (extension === 'jpg') return `${type}/jpeg`;
+ if (extension === 'tif') return `${type}/tiff`;
+ if (['ttf', 'otf', 'woff', 'woff2'].includes(extension)) type = 'font';
+ if (['mp4', 'mpeg', 'webm', 'ogg'].includes(extension)) type = 'video';
+ if (['midi', 'wav'].includes(extension)) type = 'audio';
+ if (extension === 'ogv') return 'video/ogg';
+ if (extension === 'mov') return 'video/quicktime';
+ if (extension === 'oga') return 'audio/ogg';
+ if (extension === 'mp3') return 'audio/mpeg';
+ if (extension === 'mid') return 'audio/midi';
+ if (extension === 'm4a') return 'audio/mp4';
+ return `${type}/${extension}`;
+ };
+
+ return async (code, { options: { filename } }) => {
+ const extension = filename.split('.').pop() || '';
+ return code.startsWith('data:')
+ ? code
+ : `data:${getBinaryMimeType(extension)};base64,${code}`;
+ };
+ },
+ },
+ extensions: [
+ 'png',
+ 'jpg',
+ 'jpeg',
+ 'gif',
+ 'webp',
+ 'bmp',
+ 'tif',
+ 'tiff',
+ 'ico',
+
+ 'ttf',
+ 'otf',
+ 'woff',
+ 'woff2',
+
+ 'mp4',
+ 'mpeg',
+ 'webm',
+ 'ogv',
+ 'ogg',
+ 'mov',
+
+ 'mp3',
+ 'm4a',
+ 'wav',
+ 'oga',
+ 'mid',
+ 'midi',
+ ],
+ editor: '',
+ multiFileSupport: true,
+};
diff --git a/src/livecodes/languages/civet/lang-civet.ts b/src/livecodes/languages/civet/lang-civet.ts
index 87402756f8..6e11477209 100644
--- a/src/livecodes/languages/civet/lang-civet.ts
+++ b/src/livecodes/languages/civet/lang-civet.ts
@@ -13,4 +13,5 @@ export const civet: LanguageSpecs = {
extensions: ['civet'],
editor: 'script',
editorLanguage: 'coffeescript',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/clojurescript/lang-clojurescript.ts b/src/livecodes/languages/clojurescript/lang-clojurescript.ts
index 1b359ca540..f6e27f0280 100644
--- a/src/livecodes/languages/clojurescript/lang-clojurescript.ts
+++ b/src/livecodes/languages/clojurescript/lang-clojurescript.ts
@@ -43,4 +43,5 @@ export const clojurescript: LanguageSpecs = {
codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-clojure.js')).clojure),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/coffeescript/lang-coffeescript.ts b/src/livecodes/languages/coffeescript/lang-coffeescript.ts
index 8064d8b3d9..15c2c488a9 100644
--- a/src/livecodes/languages/coffeescript/lang-coffeescript.ts
+++ b/src/livecodes/languages/coffeescript/lang-coffeescript.ts
@@ -27,4 +27,5 @@ export const coffeescript: LanguageSpecs = {
),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/css/lang-css.ts b/src/livecodes/languages/css/lang-css.ts
index 4100e2ad6e..b5e8c3fdab 100644
--- a/src/livecodes/languages/css/lang-css.ts
+++ b/src/livecodes/languages/css/lang-css.ts
@@ -25,4 +25,5 @@ export const css: LanguageSpecs = {
},
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/diagrams/lang-diagrams.ts b/src/livecodes/languages/diagrams/lang-diagrams.ts
index 642cf2498a..5561765283 100644
--- a/src/livecodes/languages/diagrams/lang-diagrams.ts
+++ b/src/livecodes/languages/diagrams/lang-diagrams.ts
@@ -22,4 +22,5 @@ export const diagrams: LanguageSpecs = {
extensions: ['diagrams', 'diagram', 'graph', 'plt'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/dot/lang-dot.ts b/src/livecodes/languages/dot/lang-dot.ts
index 410f999dd3..a9b97b1fc3 100644
--- a/src/livecodes/languages/dot/lang-dot.ts
+++ b/src/livecodes/languages/dot/lang-dot.ts
@@ -21,4 +21,5 @@ export const dot: LanguageSpecs = {
extensions: ['dot'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/dotenv/index.ts b/src/livecodes/languages/dotenv/index.ts
new file mode 100644
index 0000000000..5c5962ff7d
--- /dev/null
+++ b/src/livecodes/languages/dotenv/index.ts
@@ -0,0 +1 @@
+export { dotenv } from './lang-dotenv';
diff --git a/src/livecodes/languages/dotenv/lang-dotenv.ts b/src/livecodes/languages/dotenv/lang-dotenv.ts
new file mode 100644
index 0000000000..24bb1362cf
--- /dev/null
+++ b/src/livecodes/languages/dotenv/lang-dotenv.ts
@@ -0,0 +1,32 @@
+import type { LanguageSpecs } from '../../models';
+import { vendorsBaseUrl } from '../../vendors';
+
+export const dotenv: LanguageSpecs = {
+ name: 'dotenv',
+ title: '.env',
+ info: false,
+ compiler: {
+ url: vendorsBaseUrl + 'dotenv/dotenv.js',
+ factory: () => async (code) => {
+ const processEnv = {};
+ try {
+ const parsed = (self as any).dotenv.parse(code);
+ (self as any).dotenv.expand({ parsed, processEnv });
+ } catch {
+ //
+ }
+ return JSON.stringify(processEnv, null, 2);
+ },
+ compiledCodeLanguage: 'json',
+ },
+ extensions: [
+ 'env',
+ 'env.local',
+ 'env.development',
+ 'env.production',
+ 'env.development.local',
+ 'env.production.local',
+ ],
+ editor: '',
+ multiFileSupport: true,
+};
diff --git a/src/livecodes/languages/ejs/lang-ejs.ts b/src/livecodes/languages/ejs/lang-ejs.ts
index 0d819b3e7e..09a9e7d2aa 100644
--- a/src/livecodes/languages/ejs/lang-ejs.ts
+++ b/src/livecodes/languages/ejs/lang-ejs.ts
@@ -21,4 +21,5 @@ export const ejs: LanguageSpecs = {
extensions: ['ejs'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/eta/lang-eta.ts b/src/livecodes/languages/eta/lang-eta.ts
index fd7c678034..8c068e25b2 100644
--- a/src/livecodes/languages/eta/lang-eta.ts
+++ b/src/livecodes/languages/eta/lang-eta.ts
@@ -21,4 +21,5 @@ export const eta: LanguageSpecs = {
extensions: ['eta'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/flow/lang-flow.ts b/src/livecodes/languages/flow/lang-flow.ts
index ee2e4c7aaf..f42a6d0e69 100644
--- a/src/livecodes/languages/flow/lang-flow.ts
+++ b/src/livecodes/languages/flow/lang-flow.ts
@@ -32,4 +32,5 @@ export const flow: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/haml/lang-haml.ts b/src/livecodes/languages/haml/lang-haml.ts
index d3c8384055..df498ae888 100644
--- a/src/livecodes/languages/haml/lang-haml.ts
+++ b/src/livecodes/languages/haml/lang-haml.ts
@@ -13,4 +13,5 @@ export const haml: LanguageSpecs = {
},
extensions: ['haml'],
editor: 'markup',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/handlebars/lang-handlebars.ts b/src/livecodes/languages/handlebars/lang-handlebars.ts
index ad29749bd4..e1fd35713b 100644
--- a/src/livecodes/languages/handlebars/lang-handlebars.ts
+++ b/src/livecodes/languages/handlebars/lang-handlebars.ts
@@ -24,4 +24,5 @@ export const handlebars: LanguageSpecs = {
extensions: ['hbs', 'handlebars'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/html/lang-html.ts b/src/livecodes/languages/html/lang-html.ts
index 3237932f26..3daf7acc1d 100644
--- a/src/livecodes/languages/html/lang-html.ts
+++ b/src/livecodes/languages/html/lang-html.ts
@@ -15,7 +15,7 @@ export const html: LanguageSpecs = {
compiler: {
factory: () => async (code) => code,
},
- extensions: ['html', 'htm'],
+ extensions: ['html', 'htm', 'svg'],
editor: 'markup',
editorSupport: {
codemirror: {
@@ -25,4 +25,5 @@ export const html: LanguageSpecs = {
},
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/imba/lang-imba.ts b/src/livecodes/languages/imba/lang-imba.ts
index df40e18c54..adfb7b2721 100644
--- a/src/livecodes/languages/imba/lang-imba.ts
+++ b/src/livecodes/languages/imba/lang-imba.ts
@@ -16,4 +16,5 @@ export const imba: LanguageSpecs = {
},
extensions: ['imba'],
editor: 'script',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/javascript/lang-javascript.ts b/src/livecodes/languages/javascript/lang-javascript.ts
index ef34679444..0746e01842 100644
--- a/src/livecodes/languages/javascript/lang-javascript.ts
+++ b/src/livecodes/languages/javascript/lang-javascript.ts
@@ -25,4 +25,5 @@ export const javascript: LanguageSpecs = {
},
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/jinja/lang-jinja.ts b/src/livecodes/languages/jinja/lang-jinja.ts
index 124c5db363..57b82fc218 100644
--- a/src/livecodes/languages/jinja/lang-jinja.ts
+++ b/src/livecodes/languages/jinja/lang-jinja.ts
@@ -27,4 +27,5 @@ export const jinja: LanguageSpecs = {
extensions: ['jinja'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/json/index.ts b/src/livecodes/languages/json/index.ts
new file mode 100644
index 0000000000..4904a7a48f
--- /dev/null
+++ b/src/livecodes/languages/json/index.ts
@@ -0,0 +1,3 @@
+export { json } from './lang-json';
+export { json5 } from './lang-json5';
+export { jsonc } from './lang-jsonc';
diff --git a/src/livecodes/languages/json/lang-json.ts b/src/livecodes/languages/json/lang-json.ts
new file mode 100644
index 0000000000..77715f46d9
--- /dev/null
+++ b/src/livecodes/languages/json/lang-json.ts
@@ -0,0 +1,29 @@
+import { codemirrorImports } from '../../editor/codemirror/utils';
+import type { LanguageSpecs } from '../../models';
+import { parserPlugins } from '../prettier';
+
+export const json: LanguageSpecs = {
+ name: 'json',
+ title: 'JSON',
+ info: false,
+ formatter: {
+ prettier: {
+ name: 'json',
+ pluginUrls: [parserPlugins.babel, parserPlugins.estree],
+ },
+ },
+ compiler: {
+ factory: () => async (code) => code,
+ },
+ extensions: ['json'],
+ editor: '',
+ editorSupport: {
+ codemirror: {
+ languageSupport: async () => {
+ const { json } = await import(codemirrorImports.json);
+ return json();
+ },
+ },
+ },
+ multiFileSupport: true,
+};
diff --git a/src/livecodes/languages/json/lang-json5.ts b/src/livecodes/languages/json/lang-json5.ts
new file mode 100644
index 0000000000..6a5fb8df62
--- /dev/null
+++ b/src/livecodes/languages/json/lang-json5.ts
@@ -0,0 +1,40 @@
+import type { LanguageSpecs } from '../../models';
+import { json5Url, monacoLanguagesBaseUrl } from '../../vendors';
+import { parserPlugins } from '../prettier';
+
+export const json5: LanguageSpecs = {
+ name: 'json5',
+ title: 'JSON5',
+ info: false,
+ formatter: {
+ prettier: {
+ name: 'json5',
+ pluginUrls: [parserPlugins.babel, parserPlugins.estree],
+ },
+ },
+ compiler: {
+ factory: () => {
+ let JSON5: { parse: (code: string) => any; stringify: (code: any) => string } | undefined;
+ return async (code) => {
+ if (!JSON5) {
+ (self as any).importScripts(json5Url);
+ JSON5 = (self as any).JSON5;
+ }
+ try {
+ return JSON.stringify(JSON5?.parse(code) || JSON.parse(code), null, 2);
+ } catch {
+ return code;
+ }
+ };
+ },
+ compiledCodeLanguage: 'json',
+ },
+ extensions: ['json5'],
+ editor: '',
+ editorSupport: {
+ monaco: { languageSupport: monacoLanguagesBaseUrl + 'json5.js' },
+ codemirror: { language: 'json' },
+ codejar: { language: 'json' },
+ },
+ multiFileSupport: true,
+};
diff --git a/src/livecodes/languages/json/lang-jsonc.ts b/src/livecodes/languages/json/lang-jsonc.ts
new file mode 100644
index 0000000000..750959d8d5
--- /dev/null
+++ b/src/livecodes/languages/json/lang-jsonc.ts
@@ -0,0 +1,20 @@
+import type { LanguageSpecs } from '../../models';
+import { parserPlugins } from '../prettier';
+import { json5 } from './lang-json5';
+
+export const jsonc: LanguageSpecs = {
+ ...json5,
+ name: 'jsonc',
+ title: 'JSONC',
+ formatter: {
+ prettier: {
+ name: 'jsonc',
+ pluginUrls: [parserPlugins.babel, parserPlugins.estree],
+ },
+ },
+ extensions: ['jsonc'],
+ editorSupport: {
+ ...json5.editorSupport,
+ monaco: { language: 'json5' },
+ },
+};
diff --git a/src/livecodes/languages/jsx/lang-jsx.ts b/src/livecodes/languages/jsx/lang-jsx.ts
index 94ecf9b00a..7b5580ad02 100644
--- a/src/livecodes/languages/jsx/lang-jsx.ts
+++ b/src/livecodes/languages/jsx/lang-jsx.ts
@@ -26,4 +26,5 @@ export const jsx: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/jsx/lang-tsx.ts b/src/livecodes/languages/jsx/lang-tsx.ts
index 7b8f52a5b9..57be7f3127 100644
--- a/src/livecodes/languages/jsx/lang-tsx.ts
+++ b/src/livecodes/languages/jsx/lang-tsx.ts
@@ -28,4 +28,5 @@ export const tsx: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts
index d642d73002..4a09cad9b3 100644
--- a/src/livecodes/languages/languages.ts
+++ b/src/livecodes/languages/languages.ts
@@ -5,6 +5,7 @@ import { assemblyscript } from './assemblyscript';
import { astro } from './astro';
import { babel } from './babel';
import { bbcode } from './bbcode';
+import { binary } from './binary';
import { blockly } from './blockly';
import { civet } from './civet';
import { clio } from './clio';
@@ -17,6 +18,7 @@ import { csharpWasm } from './csharp-wasm';
import { css } from './css';
import { diagrams } from './diagrams';
import { dot } from './dot';
+import { dotenv } from './dotenv/lang-dotenv';
import { ejs } from './ejs';
import { eta } from './eta';
import { fennel } from './fennel';
@@ -31,6 +33,7 @@ import { imba } from './imba';
import { java } from './java';
import { javascript } from './javascript';
import { jinja } from './jinja';
+import { json, json5, jsonc } from './json';
import { jsx, tsx } from './jsx';
import { julia } from './julia';
import { less } from './less';
@@ -74,12 +77,14 @@ import { sucrase } from './sucrase';
import { svelte, svelteApp } from './svelte';
import { tcl } from './tcl';
import { teal } from './teal';
+import { text } from './text';
import { twig } from './twig';
import { typescript } from './typescript';
import { vento } from './vento';
import { vue, vueApp } from './vue';
import { vue2 } from './vue2';
import { wat } from './wat';
+import { yaml } from './yaml';
export const languages: LanguageSpecs[] = [
html,
markdown,
@@ -169,4 +174,11 @@ export const languages: LanguageSpecs[] = [
prolog,
minizinc,
blockly,
+ text,
+ json,
+ json5,
+ jsonc,
+ yaml,
+ binary,
+ dotenv,
];
diff --git a/src/livecodes/languages/less/lang-less.ts b/src/livecodes/languages/less/lang-less.ts
index a1dc9fb8a1..4462137540 100644
--- a/src/livecodes/languages/less/lang-less.ts
+++ b/src/livecodes/languages/less/lang-less.ts
@@ -32,4 +32,5 @@ export const less: LanguageSpecs = {
codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-less.js')).less),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/liquid/lang-liquid.ts b/src/livecodes/languages/liquid/lang-liquid.ts
index d43f343d11..c387830883 100644
--- a/src/livecodes/languages/liquid/lang-liquid.ts
+++ b/src/livecodes/languages/liquid/lang-liquid.ts
@@ -27,4 +27,5 @@ export const liquid: LanguageSpecs = {
(await import(codeMirrorBaseUrl + 'codemirror-lang-liquid.js')).liquid(),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/livescript/lang-livescript.ts b/src/livecodes/languages/livescript/lang-livescript.ts
index ce13d30db7..d01ce73451 100644
--- a/src/livecodes/languages/livescript/lang-livescript.ts
+++ b/src/livecodes/languages/livescript/lang-livescript.ts
@@ -28,4 +28,5 @@ export const livescript: LanguageSpecs = {
),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/markdown/lang-markdown.ts b/src/livecodes/languages/markdown/lang-markdown.ts
index e22eb60c22..f945b62402 100644
--- a/src/livecodes/languages/markdown/lang-markdown.ts
+++ b/src/livecodes/languages/markdown/lang-markdown.ts
@@ -27,4 +27,5 @@ export const markdown: LanguageSpecs = {
(await import(codeMirrorBaseUrl + 'codemirror-lang-markdown.js')).markdown(),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/mdx/lang-mdx.ts b/src/livecodes/languages/mdx/lang-mdx.ts
index 2b7bd2de76..784514d67e 100644
--- a/src/livecodes/languages/mdx/lang-mdx.ts
+++ b/src/livecodes/languages/mdx/lang-mdx.ts
@@ -31,7 +31,8 @@ import { createRoot } from "react-dom/client";
${escapeCode(jsx, false)}
createRoot(document.querySelector('#__livecodes_mdx_root__')).render( ,);
`;
- const js = (await compileInCompiler(result, 'jsx', config, {}, worker)).code;
+ const js = (await compileInCompiler(result, 'jsx', config, { filename: 'markup' }, worker))
+ .code;
resolve(``);
});
@@ -52,4 +53,5 @@ export const mdx: LanguageSpecs = {
extensions: ['mdx'],
editor: 'markup',
editorLanguage: 'markdown',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/mjml/lang-mjml.ts b/src/livecodes/languages/mjml/lang-mjml.ts
index 08d6384c7a..ff4b6eccdb 100644
--- a/src/livecodes/languages/mjml/lang-mjml.ts
+++ b/src/livecodes/languages/mjml/lang-mjml.ts
@@ -34,4 +34,5 @@ export const mjml: LanguageSpecs = {
extensions: ['mjml'],
editor: 'markup',
editorLanguage: 'xml',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/mustache/lang-mustache.ts b/src/livecodes/languages/mustache/lang-mustache.ts
index 5cf63f2325..32b526c431 100644
--- a/src/livecodes/languages/mustache/lang-mustache.ts
+++ b/src/livecodes/languages/mustache/lang-mustache.ts
@@ -21,4 +21,5 @@ export const mustache: LanguageSpecs = {
extensions: ['mustache'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/nunjucks/lang-nunjucks.ts b/src/livecodes/languages/nunjucks/lang-nunjucks.ts
index 47e30c1f5f..d665aa14d5 100644
--- a/src/livecodes/languages/nunjucks/lang-nunjucks.ts
+++ b/src/livecodes/languages/nunjucks/lang-nunjucks.ts
@@ -24,4 +24,5 @@ export const nunjucks: LanguageSpecs = {
extensions: ['njk', 'nunjucks'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/postcss/postcss-plugins.ts b/src/livecodes/languages/postcss/postcss-plugins.ts
index f22e2c694f..f11377cc21 100644
--- a/src/livecodes/languages/postcss/postcss-plugins.ts
+++ b/src/livecodes/languages/postcss/postcss-plugins.ts
@@ -139,19 +139,28 @@ export const cssModules: ProcessorSpecs = {
localsConvention: 'camelCase',
...customSettings,
getJSON(_cssFileName: string, json: Record, _outputFileName: string) {
- const addClasses = customSettings.addClassesToHTML !== false;
+ const isMultiFileProject = options.filename !== 'style';
+ const filename = options.filename;
+ const addClasses = customSettings.addClassesToHTML === true;
const removeClasses = customSettings.removeOriginalClasses === true;
+ let html = '';
if (addClasses) {
- options.html = (self as any).postcssModules.addClassesToHtml(
- options.html,
+ html = (self as any).postcssModules.addClassesToHtml(
+ options.compileInfo?.modifiedHTML || options.html,
json,
removeClasses,
);
+ if (!isMultiFileProject) {
+ options.html = html;
+ }
}
options.compileInfo = {
...options.compileInfo,
- cssModules: json,
- ...(addClasses ? { modifiedHTML: options.html } : {}),
+ cssModules: {
+ ...options.compileInfo?.cssModules,
+ [filename]: json,
+ },
+ ...(addClasses ? { modifiedHTML: html || options.compileInfo?.modifiedHTML } : {}),
};
},
});
diff --git a/src/livecodes/languages/pug/lang-pug.ts b/src/livecodes/languages/pug/lang-pug.ts
index f3058c1ed6..68bcc0d2d4 100644
--- a/src/livecodes/languages/pug/lang-pug.ts
+++ b/src/livecodes/languages/pug/lang-pug.ts
@@ -23,4 +23,5 @@ export const pug: LanguageSpecs = {
},
extensions: ['pug', 'jade'],
editor: 'markup',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/react-native/lang-react-native-tsx.ts b/src/livecodes/languages/react-native/lang-react-native-tsx.ts
index 023090f62b..c6f7c0d4ab 100644
--- a/src/livecodes/languages/react-native/lang-react-native-tsx.ts
+++ b/src/livecodes/languages/react-native/lang-react-native-tsx.ts
@@ -22,4 +22,5 @@ export const reactNativeTsx: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/react-native/lang-react-native.ts b/src/livecodes/languages/react-native/lang-react-native.ts
index 786bbbbffd..493b102091 100644
--- a/src/livecodes/languages/react-native/lang-react-native.ts
+++ b/src/livecodes/languages/react-native/lang-react-native.ts
@@ -40,4 +40,5 @@ export const reactNative: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/react/lang-react-tsx.ts b/src/livecodes/languages/react/lang-react-tsx.ts
index b41f60dc13..baaad0cd50 100644
--- a/src/livecodes/languages/react/lang-react-tsx.ts
+++ b/src/livecodes/languages/react/lang-react-tsx.ts
@@ -21,4 +21,5 @@ export const reactTsx: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/react/lang-react.ts b/src/livecodes/languages/react/lang-react.ts
index 01e843923b..ad72589c63 100644
--- a/src/livecodes/languages/react/lang-react.ts
+++ b/src/livecodes/languages/react/lang-react.ts
@@ -46,4 +46,5 @@ export const react: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/reason/lang-reason.ts b/src/livecodes/languages/reason/lang-reason.ts
index ff9975dc84..78fecd3470 100644
--- a/src/livecodes/languages/reason/lang-reason.ts
+++ b/src/livecodes/languages/reason/lang-reason.ts
@@ -12,4 +12,5 @@ export const reason: LanguageSpecs = {
editor: 'script',
editorLanguage: 'javascript',
editorSupport: { monaco: { language: 'csharp' } },
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/rescript/lang-rescript.ts b/src/livecodes/languages/rescript/lang-rescript.ts
index bd200504a0..8996c3b17f 100644
--- a/src/livecodes/languages/rescript/lang-rescript.ts
+++ b/src/livecodes/languages/rescript/lang-rescript.ts
@@ -27,4 +27,5 @@ export const rescript: LanguageSpecs = {
editor: 'script',
editorLanguage: 'javascript',
editorSupport: { monaco: { language: 'csharp' } },
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/richtext/lang-richtext.ts b/src/livecodes/languages/richtext/lang-richtext.ts
index 4f282cf1ea..8d62e4fea6 100644
--- a/src/livecodes/languages/richtext/lang-richtext.ts
+++ b/src/livecodes/languages/richtext/lang-richtext.ts
@@ -14,4 +14,5 @@ export const richtext: LanguageSpecs = {
extensions: ['rte', 'rte.html', 'rich'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/riot/lang-riot.ts b/src/livecodes/languages/riot/lang-riot.ts
index 809fdb23c1..e1cbb48f16 100644
--- a/src/livecodes/languages/riot/lang-riot.ts
+++ b/src/livecodes/languages/riot/lang-riot.ts
@@ -26,4 +26,5 @@ export const riot: LanguageSpecs = {
extensions: ['riot', 'riotjs'],
editor: 'script',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/scss/lang-sass.ts b/src/livecodes/languages/scss/lang-sass.ts
index 0fc3bc8dd9..a01eca0f43 100644
--- a/src/livecodes/languages/scss/lang-sass.ts
+++ b/src/livecodes/languages/scss/lang-sass.ts
@@ -13,4 +13,5 @@ export const sass: LanguageSpecs = {
(await import(codeMirrorBaseUrl + 'codemirror-lang-scss.js')).sass({ indented: true }),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/scss/lang-scss.ts b/src/livecodes/languages/scss/lang-scss.ts
index 94aaf49fbb..a4f7c4b2f2 100644
--- a/src/livecodes/languages/scss/lang-scss.ts
+++ b/src/livecodes/languages/scss/lang-scss.ts
@@ -26,4 +26,5 @@ export const scss: LanguageSpecs = {
(await import(codeMirrorBaseUrl + 'codemirror-lang-scss.js')).sass(),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/solid/lang-solid-tsx.ts b/src/livecodes/languages/solid/lang-solid-tsx.ts
index a4f90bcd05..29d356d6af 100644
--- a/src/livecodes/languages/solid/lang-solid-tsx.ts
+++ b/src/livecodes/languages/solid/lang-solid-tsx.ts
@@ -11,7 +11,7 @@ export const solidTsx: LanguageSpecs = {
},
},
compiler: 'solid',
- extensions: ['solid.tsx'],
+ extensions: ['solid.tsx', 'solid-tsx'],
editor: 'script',
editorLanguage: 'typescript',
editorSupport: {
@@ -24,4 +24,5 @@ export const solidTsx: LanguageSpecs = {
jsxFragmentFactory: 'Fragment',
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/solid/lang-solid.ts b/src/livecodes/languages/solid/lang-solid.ts
index bc8cf21b91..6a8ffeef0c 100644
--- a/src/livecodes/languages/solid/lang-solid.ts
+++ b/src/livecodes/languages/solid/lang-solid.ts
@@ -30,4 +30,5 @@ export const solid: LanguageSpecs = {
jsxFragmentFactory: 'Fragment',
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/stencil/lang-stencil.ts b/src/livecodes/languages/stencil/lang-stencil.ts
index 1fe7844153..99076d6de4 100644
--- a/src/livecodes/languages/stencil/lang-stencil.ts
+++ b/src/livecodes/languages/stencil/lang-stencil.ts
@@ -43,4 +43,5 @@ export const stencil: LanguageSpecs = {
jsxFragmentFactory: 'Fragment',
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/stylis/lang-stylis.ts b/src/livecodes/languages/stylis/lang-stylis.ts
index 1e80f3da8b..cb34ab4a80 100644
--- a/src/livecodes/languages/stylis/lang-stylis.ts
+++ b/src/livecodes/languages/stylis/lang-stylis.ts
@@ -14,4 +14,5 @@ export const stylis: LanguageSpecs = {
extensions: ['stylis'],
editor: 'style',
editorLanguage: 'scss',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/stylus/lang-stylus.ts b/src/livecodes/languages/stylus/lang-stylus.ts
index 0b7d8b3e1b..99294ee48f 100644
--- a/src/livecodes/languages/stylus/lang-stylus.ts
+++ b/src/livecodes/languages/stylus/lang-stylus.ts
@@ -17,4 +17,5 @@ export const stylus: LanguageSpecs = {
codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-stylus.js')).stylus),
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/sucrase/lang-sucrase.ts b/src/livecodes/languages/sucrase/lang-sucrase.ts
index 2b7f11b9da..12ce4baf0f 100644
--- a/src/livecodes/languages/sucrase/lang-sucrase.ts
+++ b/src/livecodes/languages/sucrase/lang-sucrase.ts
@@ -30,4 +30,5 @@ export const sucrase: LanguageSpecs = {
jsx: 4, // monaco.languages.typescript.JsxEmit.ReactJSX,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/svelte/lang-svelte-compiler.ts b/src/livecodes/languages/svelte/lang-svelte-compiler.ts
index 8151f59fa9..257c43d285 100644
--- a/src/livecodes/languages/svelte/lang-svelte-compiler.ts
+++ b/src/livecodes/languages/svelte/lang-svelte-compiler.ts
@@ -1,9 +1,10 @@
+import { compileInCompiler } from '../../compiler';
import { compileAllBlocks } from '../../compiler/compile-blocks';
import { createImportMap, replaceSFCImports } from '../../compiler/import-map';
import { getCompileResult } from '../../compiler/utils';
-import type { CompilerFunction, Config, Language } from '../../models';
+import type { CompileOptions, CompilerFunction, Config, Language } from '../../models';
import { getErrorMessage } from '../../utils/utils';
-import { getLanguageByAlias, getLanguageCustomSettings } from '../utils';
+import { getFileExtension, getLanguageByAlias, getLanguageCustomSettings } from '../utils';
(self as any).createSvelteCompiler = (): CompilerFunction => {
const MAIN_FILE = '__LiveCodes_App__.svelte';
@@ -23,12 +24,14 @@ import { getLanguageByAlias, getLanguageCustomSettings } from '../utils';
if (!code) return getCompileResult('');
const isSfc = (mod: string) =>
- mod.toLowerCase().endsWith('.svelte') || mod.toLowerCase().startsWith('data:text/svelte');
+ (mod.toLowerCase().endsWith('.svelte') && !mod.startsWith('~/')) ||
+ mod.toLowerCase().startsWith('data:text/svelte');
const fullCode = await replaceSFCImports(code, {
config,
filename,
getLanguageByAlias,
+ getFileExtension,
isSfc,
compileSFC: async (
code: string,
@@ -73,20 +76,51 @@ import { getLanguageByAlias, getLanguageCustomSettings } from '../utils';
};
};
- return (code, { config, language }) => {
- const isMainFile = config.markup.language !== 'svelte-app' || language === 'svelte-app';
- return compileSvelteSFC(code, {
- config,
- language: language as Language,
- filename: isMainFile ? MAIN_FILE : SECONDARY_FILE,
- });
+ const compileSvelteModule = async (
+ code: string,
+ { config, options, filename }: { config: Config; options: CompileOptions; filename: string },
+ ) => {
+ try {
+ if (filename.endsWith('.ts')) {
+ code = (await compileInCompiler(code, 'typescript', config, options)).code;
+ }
+ const result = (window as any).svelte.compileModule(code, {
+ filename,
+ ...getLanguageCustomSettings('svelte', config),
+ });
+ return result.js;
+ } catch (err) {
+ errors.push(getErrorMessage(err));
+ return {
+ code: '',
+ info: { errors },
+ };
+ }
};
-};
-const getMountCode = (code: string) =>
- `
+ const getMountCode = (code: string) =>
+ `
import { mount } from "svelte";
${code}
mount(__LiveCodes_App__, { target: document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div')) });
`.trimStart();
+
+ return async (code, { config, language, options }) => {
+ const isMultiFileProject = Boolean(config.files.length);
+ const isMainFile = isMultiFileProject
+ ? false
+ : config.markup.language !== 'svelte-app' || language === 'svelte-app';
+ const filename = isMultiFileProject
+ ? options.filename
+ : isMainFile
+ ? MAIN_FILE
+ : SECONDARY_FILE;
+
+ if (filename.endsWith('.svelte.js') || filename.endsWith('.svelte.ts')) {
+ return compileSvelteModule(code, { config, options, filename });
+ }
+
+ return compileSvelteSFC(code, { config, language: language as Language, filename });
+ };
+};
diff --git a/src/livecodes/languages/svelte/lang-svelte.ts b/src/livecodes/languages/svelte/lang-svelte.ts
index c5e6b2cf91..20061fe6c2 100644
--- a/src/livecodes/languages/svelte/lang-svelte.ts
+++ b/src/livecodes/languages/svelte/lang-svelte.ts
@@ -46,7 +46,7 @@ export const svelte: LanguageSpecs = {
},
inlineScript: 'globalThis.process = { env: { NODE_ENV: "production" } };',
},
- extensions: ['svelte'],
+ extensions: ['svelte', 'svelte.js', 'svelte.ts'],
editor: 'script',
editorSupport: {
monaco: { languageSupport: monacoLanguagesBaseUrl + 'svelte.js' },
@@ -56,6 +56,7 @@ export const svelte: LanguageSpecs = {
},
codejar: { language: 'html' },
},
+ multiFileSupport: true,
};
export const svelteApp: LanguageSpecs = {
diff --git a/src/livecodes/languages/tailwindcss/processor-tailwindcss-compiler.ts b/src/livecodes/languages/tailwindcss/processor-tailwindcss-compiler.ts
index d9bf67bf17..3b2203e5c2 100644
--- a/src/livecodes/languages/tailwindcss/processor-tailwindcss-compiler.ts
+++ b/src/livecodes/languages/tailwindcss/processor-tailwindcss-compiler.ts
@@ -6,7 +6,8 @@ import { modulesService } from '../../services';
import { getLanguageCustomSettings } from '../../utils/utils';
import { tailwindcss3Url, tailwindcssBaseUrl, vendorsBaseUrl } from '../../vendors';
import { lightningcssFeatures } from '../lightningcss/processor-lightningcss-compiler';
-import { addCodeInStyleBlocks } from './utils';
+import { getLanguageEditorId } from '../utils';
+import { addCodeInStyleBlocks, hasTailwindImport } from './utils';
declare const self: any;
@@ -192,16 +193,32 @@ self.createTailwindcssCompiler = (): CompilerFunction => {
};
const tailwind4: CompilerFunction = async (code, { config, options }) => {
- const prepareCode = (css: string, html: string) => {
- let result = replaceStyleImports(css, [/tailwindcss/g]);
- if (!result.includes('@import')) {
+ const isMultiFile = config.files.length > 0;
+
+ if (isMultiFile && !hasTailwindImport(code)) return code;
+
+ const prepareCode = (css: string, html: string, isMultiFile = false) => {
+ let result = replaceStyleImports(css, { exceptions: [/tailwindcss/g] });
+ if (!result.includes('@import') && !isMultiFile) {
result = `@import "tailwindcss";${result}`;
}
return addCodeInStyleBlocks(result, html);
};
- const html = `${options.html}\n`;
- const css = prepareCode(code, html);
+ const html = isMultiFile
+ ? '' +
+ config.files
+ .map((f) => ({
+ type: getLanguageEditorId(f.language),
+ content: f.content,
+ }))
+ .filter((f) => f.type !== 'style')
+ .map((f) => (f.type === 'markup' ? f.content : ``))
+ .join('\n') +
+ ''
+ : `${options.html}\n`;
+ const css = prepareCode(code, html, isMultiFile);
+
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
index ec39c53713..5ade42527a 100644
--- a/src/livecodes/languages/tailwindcss/utils.ts
+++ b/src/livecodes/languages/tailwindcss/utils.ts
@@ -11,3 +11,8 @@ export const addCodeInStyleBlocks = (css: string, html: string) => {
}
return css;
};
+
+export const hasTailwindImport = (css: string) => {
+ const pattern = /@import\s+('tailwindcss')|("tailwindcss")\s*;/g;
+ return new RegExp(pattern).test(css);
+};
diff --git a/src/livecodes/languages/text/index.ts b/src/livecodes/languages/text/index.ts
new file mode 100644
index 0000000000..e2fbd1216e
--- /dev/null
+++ b/src/livecodes/languages/text/index.ts
@@ -0,0 +1 @@
+export { text } from './lang-text';
diff --git a/src/livecodes/languages/text/lang-text.ts b/src/livecodes/languages/text/lang-text.ts
new file mode 100644
index 0000000000..73ed05e8b9
--- /dev/null
+++ b/src/livecodes/languages/text/lang-text.ts
@@ -0,0 +1,14 @@
+import type { LanguageSpecs } from '../../models';
+
+export const text: LanguageSpecs = {
+ name: 'text',
+ title: 'Text',
+ info: false,
+ compiler: {
+ factory: () => async (code) => code,
+ },
+ extensions: ['txt', 'csv', 'tsv', 'plaintext'],
+ editor: '',
+ multiFileSupport: true,
+ editorSupport: { monaco: { language: 'plaintext' } },
+};
diff --git a/src/livecodes/languages/twig/lang-twig.ts b/src/livecodes/languages/twig/lang-twig.ts
index 7e220aef57..668ca87edd 100644
--- a/src/livecodes/languages/twig/lang-twig.ts
+++ b/src/livecodes/languages/twig/lang-twig.ts
@@ -21,4 +21,5 @@ export const twig: LanguageSpecs = {
extensions: ['twig'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/typescript/lang-typescript.ts b/src/livecodes/languages/typescript/lang-typescript.ts
index bb0053a6da..f3113d7818 100644
--- a/src/livecodes/languages/typescript/lang-typescript.ts
+++ b/src/livecodes/languages/typescript/lang-typescript.ts
@@ -37,14 +37,14 @@ export const typescript: LanguageSpecs = {
url: typescriptUrl,
factory:
() =>
- async (code, { config }) =>
+ async (code, { config, language }) =>
(window as any).ts.transpile(code, {
...typescriptOptions,
- ...(['jsx', 'tsx'].includes(config.script.language) && !hasCustomJsxRuntime(code, config)
+ ...(['jsx', 'tsx', 'typescript'].includes(language) && !hasCustomJsxRuntime(code, config)
? { jsx: 'react-jsx' }
: {}),
...getLanguageCustomSettings('typescript', config),
- ...getLanguageCustomSettings(config.script.language, config),
+ ...getLanguageCustomSettings(language, config),
}),
},
extensions: ['ts', 'mts', 'typescript'],
@@ -61,4 +61,5 @@ export const typescript: LanguageSpecs = {
strictNullChecks: true,
},
},
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/utils.ts b/src/livecodes/languages/utils.ts
index efdc294df9..0305cfe27b 100644
--- a/src/livecodes/languages/utils.ts
+++ b/src/livecodes/languages/utils.ts
@@ -5,14 +5,39 @@ import { highlightjsUrl } from '../vendors';
export const getLanguageByAlias = (alias: string = ''): Language | undefined => {
if (!alias) return;
const aliasLowerCase = alias?.toLowerCase();
- return window.deps.languages.find(
- (language) =>
- language.name === aliasLowerCase ||
- language.title.toLowerCase() === aliasLowerCase ||
- language.extensions.map((ext) => ext.toLowerCase()).includes(aliasLowerCase),
- )?.name;
+ return (
+ window.deps.languages.find(
+ (language) =>
+ language.name === aliasLowerCase ||
+ language.title.toLowerCase() === aliasLowerCase ||
+ language.extensions.map((ext) => ext.toLowerCase()).includes(aliasLowerCase),
+ )?.name || getLanguageByAlias(aliasLowerCase.split('.').slice(1).join('.')) // e.g. 'config.ts' => 'ts'
+ );
+};
+
+export const getFileExtension = /* @__PURE__ */ (filename: string) => {
+ const parts = filename.toLowerCase().split('.');
+ if (parts.length === 1) return ''; // e.g. myfile => ''
+ const extension = parts[parts.length - 1];
+ if (parts.length === 2) return extension; // e.g. App.tsx => 'tsx'
+ const lang = parts[parts.length - 2];
+ if (getLanguageByAlias(`${lang}.${extension}`)) return `${lang}.${extension}`; // e.g. App.react.tsx => 'react.tsx'
+ return parts.slice(1).join('.'); // e.g. .env.development.local
};
+export const getFileLanguage = (filename: string, config: Partial) => {
+ if (filename.startsWith('.env')) return 'dotenv';
+ const extension = getFileExtension(filename);
+ const fileLanguages: Config['fileLanguages'] = {
+ ...config.fileLanguages,
+ ...config.customSettings?.fileLanguages,
+ };
+ return getLanguageByAlias(fileLanguages[extension as Language]) || getLanguageByAlias(extension);
+};
+
+export const supportsMultiFile = (language: Language) =>
+ window.deps.languages.find((l) => l.name === language)?.multiFileSupport === true;
+
export const getLanguageTitle = (language: Language) => {
const languageSpecs = window.deps.languages.find((lang) => lang.name === language);
return languageSpecs?.longTitle || languageSpecs?.title || language.toUpperCase();
diff --git a/src/livecodes/languages/vento/lang-vento.ts b/src/livecodes/languages/vento/lang-vento.ts
index 5f016232e9..0060915faa 100644
--- a/src/livecodes/languages/vento/lang-vento.ts
+++ b/src/livecodes/languages/vento/lang-vento.ts
@@ -21,4 +21,5 @@ export const vento: LanguageSpecs = {
extensions: ['vto', 'vento'],
editor: 'markup',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/vue/lang-vue-compiler.ts b/src/livecodes/languages/vue/lang-vue-compiler.ts
index 212947a217..56f20b7cc3 100644
--- a/src/livecodes/languages/vue/lang-vue-compiler.ts
+++ b/src/livecodes/languages/vue/lang-vue-compiler.ts
@@ -1,10 +1,16 @@
import { compileAllBlocks, exportDefaultImports } from '../../compiler/compile-blocks';
import { compileInCompiler } from '../../compiler/compile-in-compiler';
-import { createImportMap, replaceSFCImports } from '../../compiler/import-map';
-import type { LanguageOrProcessor } from '../../compiler/models';
-import type { CompilerFunction, Config } from '../../models';
+import {
+ createImportMap,
+ getImports,
+ isBare,
+ replaceImports,
+ replaceSFCImports,
+} from '../../compiler/import-map';
+import type { CompilerMessageEvent, LanguageOrProcessor } from '../../compiler/models';
+import type { CompilerFunction, Config, EditorLibrary } from '../../models';
import { getErrorMessage, getRandomString, replaceAsync } from '../../utils/utils';
-import { getLanguageByAlias } from '../utils';
+import { getFileExtension, getLanguageByAlias, getLanguageEditorId } from '../utils';
// based on:
// https://github.com/vuejs/repl/blob/main/src/transform.ts
@@ -38,6 +44,7 @@ import { getLanguageByAlias } from '../utils';
}
if (!code.trim()) return;
+ const isMultiFile = config.files.length > 0;
const isSfc = (mod: string) =>
mod.toLowerCase().endsWith('.vue') || mod.toLowerCase().startsWith('data:text/vue');
const testTs = (filename: string) =>
@@ -49,6 +56,7 @@ import { getLanguageByAlias } from '../utils';
filename,
config,
getLanguageByAlias,
+ getFileExtension,
isSfc,
compileSFC: async (code, { filename, config }) => {
const compiled = (await compileVueSFC(code, { filename, config }))?.js || '';
@@ -118,7 +126,14 @@ import { getLanguageByAlias } from '../utils';
clientCode += code;
};
- const [compiledScript, bindings] = await doCompileScript(descriptor, id, false, isTS, isJSX);
+ const [compiledScript, bindings] = await doCompileScript(
+ descriptor,
+ id,
+ false,
+ isTS,
+ isJSX,
+ config,
+ );
const clientScript =
isTS || isJSX ? await compileTypescript(compiledScript, { config }) : compiledScript;
@@ -149,7 +164,7 @@ import { getLanguageByAlias } from '../utils';
}
const createAppCode =
- filename === MAIN_FILE
+ filename === MAIN_FILE && !isMultiFile
? `\nimport { createApp } from 'vue';` +
`\ncreateApp(${COMP_IDENTIFIER})` +
`\n .mount(document.querySelector("#livecodes-app") || document.body.appendChild(document.createElement('div')));\n`
@@ -181,8 +196,10 @@ import { getLanguageByAlias } from '../utils';
ssr: boolean,
isTS: boolean,
isJSX: boolean,
+ config: Config,
): Promise<[string, /* BindingMetadata | undefined */ any]> {
- if (descriptor.script || descriptor.scriptSetup) {
+ const script = descriptor.script || descriptor.scriptSetup;
+ if (script) {
const expressionPlugins = [];
if (isTS) {
expressionPlugins.push('typescript');
@@ -191,6 +208,58 @@ import { getLanguageByAlias } from '../utils';
expressionPlugins.push('jsx');
}
+ let types: Record = {};
+
+ const findFile = (filename: string) => {
+ filename = filename.replace('/~~~index~~~.d.ts', '');
+ const file = config.files?.find(
+ (f) =>
+ filename === f.filename ||
+ filename === f.filename + '.ts' ||
+ filename === f.filename + '.mts' ||
+ filename === f.filename + '.tsx' ||
+ filename === f.filename + '.d.ts',
+ );
+ if (file) return file;
+
+ const pkgPath = // e.g. '/node_modules/pkg'
+ filename.replace('node_modules/~~/', '/~~/node_modules/').split('/~~').pop() || filename;
+
+ if (types[pkgPath]) {
+ return { filename, content: types[pkgPath] };
+ }
+
+ const packagejson = types[pkgPath + '/package.json'];
+ if (packagejson) {
+ const typesEntry = JSON.parse(packagejson).types?.replace('./', '');
+ if (typesEntry) {
+ return {
+ filename: pkgPath + '/' + typesEntry,
+ content: types[pkgPath + '/' + typesEntry],
+ };
+ }
+ }
+
+ const definitelyTyped =
+ '/node_modules/@types/' + pkgPath.replace('/node_modules/', '/') + 'index.d.ts';
+ const definitelyTypedContent = types[definitelyTyped];
+ if (definitelyTypedContent) {
+ return {
+ filename: definitelyTyped,
+ content: definitelyTypedContent,
+ };
+ }
+ return undefined;
+ };
+
+ const scriptContent: string = script.content;
+ if (hasDefinePropsWithTypes(scriptContent)) {
+ types = (await getTypes(config)).reduce(
+ (acc, t) => ({ ...acc, [t.filename]: t.content }),
+ {},
+ );
+ }
+
const compiledScript = SFCCompiler.compileScript(descriptor, {
inlineTemplate: true,
// ...store.sfcOptions?.script,
@@ -205,7 +274,12 @@ import { getLanguageByAlias } from '../utils';
expressionPlugins,
},
},
+ fs: {
+ fileExists: (file: string) => findFile(file),
+ readFile: (file: string) => findFile(file)?.content || '',
+ },
});
+
let code = compiledScript.content;
if (compiledScript.bindings) {
code =
@@ -329,7 +403,7 @@ import { getLanguageByAlias } from '../utils';
content = await compileAllBlocks(content, config, { prepareFn, skipCompilers });
// CSS Modules
- let cssModules: Record | undefined;
+ let cssModules: Record> | undefined;
content = await replaceAsync(
content,
stylePattern,
@@ -362,13 +436,99 @@ import { getLanguageByAlias } from '../utils';
return compiled;
}
- return async (code, { config, language }) => {
+ const hasDefinePropsWithTypes = (code: string) => code.match(new RegExp(/defineProps\s* {
+ const content = !config.files.length
+ ? config.script.content + '\n' + config.markup.content
+ : config.files.reduce(
+ (acc, file) =>
+ ['script', 'markup'].includes(getLanguageEditorId(file.language) || '')
+ ? acc + file.content + '\n'
+ : acc,
+ '',
+ );
+ const id = getRandomString();
+ const type = 'ts-features';
+ const feature = 'getTypes';
+
+ return new Promise((resolve) => {
+ const handler = async (event: CompilerMessageEvent) => {
+ const message = event.data;
+ if (
+ message.type !== type ||
+ message.payload.feature !== feature ||
+ message.payload.id !== id
+ ) {
+ return;
+ }
+ (self as any).removeEventListener('message', handler);
+ resolve(message.payload.data);
+ };
+ (self as any).addEventListener('message', handler);
+ (self as any).postMessage({ type, payload: { id, feature, data: content } });
+ });
+ }
+
+ return async (code, { config, language, options }) => {
try {
- const isMainFile = config.markup.language !== 'vue-app' || language === 'vue-app';
- const filename = isMainFile ? MAIN_FILE : SECONDARY_FILE;
+ const isMultiFileProject = Boolean(config.files.length);
+ const isMainFile = isMultiFileProject
+ ? false
+ : config.markup.language !== 'vue-app' || language === 'vue-app';
+ const filename = isMultiFileProject
+ ? options.filename
+ : isMainFile
+ ? MAIN_FILE
+ : SECONDARY_FILE;
+
+ // Vue compiler does not allow importing types from non-relative paths
+ // in addition, the types file lookup fails for bare imports with no file extension!
+ // this workaround converts imports from `lib` to `./node_modules/~~/lib/~~~index~~~.d.ts`, then restores it after compilation
+ let libImports: Record = {};
+ const replaceBareImports = (code: string) => {
+ if (!hasDefinePropsWithTypes(code)) return code;
+ libImports = getImports(code)
+ .filter((mod) => isBare(mod))
+ .reduce(
+ (acc, mod) => ({
+ ...acc,
+ [mod]:
+ './node_modules/~~/' +
+ (mod.split('/').pop()?.includes('.') ? mod : mod + '/~~~index~~~.d.ts'),
+ }),
+ {},
+ );
+ if (Object.keys(libImports).length) {
+ code = replaceImports(code, config, { importMap: libImports });
+ }
+ return code;
+ };
+ code = replaceBareImports(code);
+
+ const dtsSeparator = '\n\n\n';
+ const dts = config.files
+ .filter((f) => f.filename.endsWith('.d.ts'))
+ .map((f) => f.content)
+ .join('\n\n');
+ if (dts) {
+ code += `${dtsSeparator}`;
+ }
+
const result = await compileVueSFC(code, { config, filename });
if (result) {
+ if (Object.keys(libImports).length) {
+ const restoredImports = Object.keys(libImports).reduce(
+ (acc, mod) => ({ ...acc, [(libImports as any)[mod]]: mod }),
+ {},
+ );
+ result.js = replaceImports(result.js, config, { importMap: restoredImports });
+ }
+ if (dts) {
+ result.js = result.js.split(dtsSeparator)[0];
+ }
+
const { css, js } = result;
const injectCSS = !css.trim()
diff --git a/src/livecodes/languages/vue/lang-vue.ts b/src/livecodes/languages/vue/lang-vue.ts
index fb15cfe26a..60e88c9d0f 100644
--- a/src/livecodes/languages/vue/lang-vue.ts
+++ b/src/livecodes/languages/vue/lang-vue.ts
@@ -46,6 +46,7 @@ export const vue: LanguageSpecs = {
jsxFragmentFactory: 'Fragment',
},
},
+ multiFileSupport: true,
};
export const vueApp: LanguageSpecs = {
diff --git a/src/livecodes/languages/vue2/lang-vue2.ts b/src/livecodes/languages/vue2/lang-vue2.ts
index a826ed98b0..8d85df2f48 100644
--- a/src/livecodes/languages/vue2/lang-vue2.ts
+++ b/src/livecodes/languages/vue2/lang-vue2.ts
@@ -27,4 +27,5 @@ export const vue2: LanguageSpecs = {
extensions: ['vue2'],
editor: 'script',
editorLanguage: 'html',
+ multiFileSupport: true,
};
diff --git a/src/livecodes/languages/yaml/index.ts b/src/livecodes/languages/yaml/index.ts
new file mode 100644
index 0000000000..1d552cfa25
--- /dev/null
+++ b/src/livecodes/languages/yaml/index.ts
@@ -0,0 +1 @@
+export { yaml } from './lang-yaml';
diff --git a/src/livecodes/languages/yaml/lang-yaml.ts b/src/livecodes/languages/yaml/lang-yaml.ts
new file mode 100644
index 0000000000..45c6a8af0b
--- /dev/null
+++ b/src/livecodes/languages/yaml/lang-yaml.ts
@@ -0,0 +1,22 @@
+import type { LanguageSpecs } from '../../models';
+import { jsYamlUrl } from '../../vendors';
+
+export const yaml: LanguageSpecs = {
+ name: 'yaml',
+ title: 'YAML',
+ info: false,
+ compiler: {
+ url: jsYamlUrl,
+ factory: () => async (code) => {
+ try {
+ return JSON.stringify((self as any).jsyaml.load(code, { json: true }), null, 2);
+ } catch {
+ return '{}';
+ }
+ },
+ compiledCodeLanguage: 'json',
+ },
+ extensions: ['yaml', 'yml'],
+ editor: '',
+ multiFileSupport: true,
+};
diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts
index 285e2dc716..e15986a633 100644
--- a/src/livecodes/models.ts
+++ b/src/livecodes/models.ts
@@ -1,3 +1,5 @@
+import type { Config, SourceFile } from '../sdk/models';
+
export type * from '../sdk/models';
export interface ModalOptions {
@@ -46,3 +48,7 @@ export interface INinjaAction {
) => boolean;
keepOpen?: boolean;
}
+
+export type ConfigWithCompiled = Omit & {
+ files: Array;
+};
diff --git a/src/livecodes/result/multi-file-result-page.ts b/src/livecodes/result/multi-file-result-page.ts
new file mode 100644
index 0000000000..91a31d3006
--- /dev/null
+++ b/src/livecodes/result/multi-file-result-page.ts
@@ -0,0 +1,658 @@
+import {
+ createImportMap,
+ getImports,
+ getStyleImports,
+ hasImports,
+ replaceImports,
+ replaceStyleImports,
+ resolvePath,
+} from '../compiler/import-map';
+import { getMainFile } from '../config/utils';
+import {
+ getFileLanguage,
+ getLanguageByAlias,
+ getLanguageCompiler,
+ getLanguageEditorId,
+ mapLanguage,
+} from '../languages/utils';
+import type { CompileInfo, Config, SourceFile } from '../models';
+import { getAppCDN, modulesService } from '../services/modules';
+import { testImports } from '../toolspane/test-imports';
+import {
+ cloneObject,
+ escapeCode,
+ escapeScript,
+ getAbsoluteUrl,
+ getRandomString,
+ isRelativeUrl,
+ objectMap,
+ removeCommentsAndStrings,
+ toCamelCase,
+ toDataUrl,
+} from '../utils/utils';
+import { browserJestUrl, esModuleShimsPath, spacingJsUrl } from '../vendors';
+import { getEnvVars, replaceEnvVars } from './utils';
+
+let lastInput = '';
+let lastOutput = '';
+
+export const createMultiFileResultPage = async ({
+ compiledFiles,
+ compiledTests,
+ config,
+ forExport,
+ template,
+ baseUrl,
+ runTests,
+ compileInfo,
+}: {
+ compiledFiles: Array;
+ compiledTests: string;
+ config: Config;
+ forExport: boolean;
+ template: string;
+ baseUrl: string;
+ singleFileResult: boolean;
+ runTests: boolean;
+ compileInfo: CompileInfo;
+}): Promise => {
+ const input = JSON.stringify({
+ compiledFiles,
+ compiledTests,
+ config,
+ forExport,
+ template,
+ baseUrl,
+ runTests,
+ compileInfo,
+ });
+ if (input === lastInput && lastOutput) return lastOutput;
+ lastInput = input;
+
+ const absoluteBaseUrl = getAbsoluteUrl(baseUrl);
+ const testsFilename = `tests.${getRandomString()}.js`;
+ // avoid mutation
+ compiledFiles = cloneObject(
+ [
+ ...compiledFiles,
+ runTests
+ ? {
+ filename: testsFilename,
+ content: config.tests?.content || '',
+ compiled: compiledTests,
+ language: 'js',
+ }
+ : null,
+ ].filter((x) => x != null),
+ );
+
+ const publicFiles = compiledFiles
+ .filter(
+ (f) =>
+ f.filename.startsWith('public/') &&
+ !compiledFiles.find((ff) => ff.filename === f.filename.replace('public/', '')),
+ )
+ .map((f) => ({ ...f, filename: f.filename.replace('public/', '') }));
+ compiledFiles.push(...publicFiles);
+
+ const envVars = getEnvVars(compiledFiles, forExport);
+ compiledFiles.forEach((f) => {
+ const fileType = getLanguageEditorId(f.language);
+ if (fileType === 'markup') {
+ f.compiled = replaceEnvVars(f.compiled, envVars);
+ } else if (fileType === 'script') {
+ if (removeCommentsAndStrings(f.compiled).match(/\bimport\b/g)) {
+ f.compiled = `import.meta.env=${JSON.stringify(envVars)};${f.compiled}`;
+ }
+ }
+ });
+
+ const mainFile = getMainFile(config);
+ const mainFileHTML = compiledFiles.find((f) => f.filename === mainFile)?.compiled || '';
+
+ const domParser = new DOMParser();
+ const dom = domParser.parseFromString(mainFileHTML, 'text/html');
+
+ // env vars
+ const envVarsScript = dom.createElement('script');
+ envVarsScript.innerHTML = `window.process = window.process || {}; window.process.env = ${JSON.stringify(envVars)};`;
+ dom.head.appendChild(envVarsScript);
+
+ // if export => clean, else => add utils
+ if (forExport) {
+ const utilsScript = dom.createElement('script');
+ utilsScript.innerHTML = 'window.livecodes = window.livecodes || {};';
+ dom.head.appendChild(utilsScript);
+ } else {
+ const templateDomParser = new DOMParser();
+ const templateDom = templateDomParser.parseFromString(template, 'text/html');
+ const script = templateDom.querySelector('script')!;
+ dom.head.appendChild(script.cloneNode(true));
+
+ const utilsScript = dom.createElement('script');
+ utilsScript.src = absoluteBaseUrl + '{{hash:result-utils.js}}';
+ utilsScript.dataset.env = 'development';
+ dom.head.appendChild(utilsScript);
+ }
+
+ // user-defined import map in
+
+
+`,
+ },
+ {
+ filename: 'styles.css',
+ language: 'css',
+ content: '',
+ },
+ {
+ filename: 'script.js',
+ language: 'javascript',
+ content: '',
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/blank.ts b/src/livecodes/templates/multifile/blank.ts
new file mode 100644
index 0000000000..9a01d86f7d
--- /dev/null
+++ b/src/livecodes/templates/multifile/blank.ts
@@ -0,0 +1,16 @@
+import type { Template } from '../../models';
+
+export const blank: Template = {
+ name: 'multifile-blank',
+ title: window.deps.translateString('templates.multifile.blank', 'Blank Template'),
+ thumbnail: 'assets/templates/blank.svg',
+ mainFile: 'index.html',
+ activeEditor: 'index.html',
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: '',
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/index.ts b/src/livecodes/templates/multifile/index.ts
new file mode 100644
index 0000000000..842328fcf6
--- /dev/null
+++ b/src/livecodes/templates/multifile/index.ts
@@ -0,0 +1,25 @@
+import { basic } from './basic';
+import { blank } from './blank';
+import { javascript } from './javascript';
+import { jest } from './jest';
+import { lit } from './lit';
+import { preact } from './preact';
+import { react } from './react';
+import { solid } from './solid';
+import { svelte } from './svelte';
+import { typescript } from './typescript';
+import { vue } from './vue';
+
+export const multifileTemplates = [
+ blank,
+ basic,
+ javascript,
+ typescript,
+ react,
+ vue,
+ preact,
+ svelte,
+ solid,
+ lit,
+ jest,
+];
diff --git a/src/livecodes/templates/multifile/javascript.ts b/src/livecodes/templates/multifile/javascript.ts
new file mode 100644
index 0000000000..6ba51f2bba
--- /dev/null
+++ b/src/livecodes/templates/multifile/javascript.ts
@@ -0,0 +1,174 @@
+import type { Template } from '../../models';
+
+export const javascript: Template = {
+ name: 'multifile-javascript',
+ title: window.deps.translateString('templates.multifile.javascript', 'JavaScript Template'),
+ thumbnail: 'assets/templates/javascript.svg',
+ aliases: ['multifile-js'],
+ mainFile: 'index.html',
+ activeEditor: 'src/main.js',
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ JavaScript
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/main.js',
+ language: 'javascript',
+ content: `import "./style.css";
+import javascriptLogo from "./javascript.svg";
+import { setupCounter } from "./counter.js";
+
+document.querySelector("#app").innerHTML = \`
+
+\`;
+
+setupCounter(document.querySelector("#counter"));
+`,
+ },
+ {
+ filename: 'src/style.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #f7df1eaa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/counter.js',
+ language: 'javascript',
+ content: `export function setupCounter(element) {
+ let counter = 0;
+ const setCounter = (count) => {
+ counter = count;
+ element.innerHTML = \`count is \${counter}\`;
+ };
+ element.addEventListener("click", () => setCounter(counter + 1));
+ setCounter(0);
+}
+`,
+ },
+ {
+ filename: 'src/javascript.svg',
+ language: 'svg',
+ content: ``,
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/jest.ts b/src/livecodes/templates/multifile/jest.ts
new file mode 100644
index 0000000000..110cc1e7b5
--- /dev/null
+++ b/src/livecodes/templates/multifile/jest.ts
@@ -0,0 +1,144 @@
+import type { Template } from '../../models';
+
+export const jest: Template = {
+ name: 'multifile-jest',
+ title: window.deps.translateString('templates.multifile.jest', 'Jest Template'),
+ thumbnail: 'assets/templates/jest.svg',
+ mainFile: 'index.html',
+ activeEditor: 'Counter.js',
+ autotest: true,
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+
+
+
+
+
+ Hello, World!
+
+ You clicked 0 times.
+
+ Run tests in the "Tests" panel below.
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'styles.css',
+ language: 'css',
+ content: `.container,
+.container button {
+ text-align: center;
+ font: 1em sans-serif;
+}
+.logo {
+ width: 150px;
+}
+.info {
+ color: #404040;
+ font-size: 0.9em;
+ margin: 2em;
+}
+`,
+ },
+ {
+ filename: 'script.js',
+ language: 'javascript',
+ content: `import { Counter } from './Counter.js';
+
+const title = document.querySelector("#title");
+const count = document.querySelector("#counter");
+const button = document.querySelector("#counter-button");
+
+title.innerText = "Jest";
+const counter = new Counter();
+button.addEventListener(
+ "click",
+ () => {
+ counter.increment();
+ count.innerText = counter.getValue();
+ },
+ false
+);
+`,
+ },
+ {
+ filename: 'Counter.js',
+ language: 'javascript',
+ content: `export class Counter {
+ count;
+
+ constructor() {
+ this.count = 0;
+ }
+
+ increment() {
+ this.count += 1;
+ }
+
+ getValue() {
+ return this.count;
+ }
+}
+`,
+ },
+ ],
+ tests: {
+ language: 'tsx',
+ content: `import { fireEvent, screen } from "@testing-library/dom";
+import "@testing-library/jest-dom";
+import { assert } from "chai";
+import { Counter } from "./Counter.js";
+
+describe("Counter Class", () => {
+ test("Should initialize count with zero", () => {
+ const counter = new Counter();
+ expect(counter.getValue()).toBe(0);
+ });
+
+ test("Should increment", () => {
+ const counter = new Counter();
+ counter.increment();
+ counter.increment();
+ counter.increment();
+ assert.equal(counter.getValue(), 3);
+ });})
+
+describe("Page", () => {
+ test("Should display title", async () => {
+ expect(screen.getByText("Hello", { exact: false })).toHaveTextContent(
+ "Hello, Jest!"
+ );
+ });
+
+ test("Should display logo", async () => {
+ expect(document.querySelector('.logo').src).toContain('jest.svg');
+ });
+
+ test("Should increment counter on button click", async () => {
+ fireEvent.click(screen.getByText("Click me"));
+ fireEvent.click(screen.getByText("Click me"));
+ fireEvent.click(screen.getByText("Click me"));
+ expect(screen.getByText("You clicked", { exact: false })).toHaveTextContent(
+ "You clicked 3 times."
+ );
+ });
+});
+`,
+ },
+ tools: {
+ enabled: 'all',
+ active: 'tests',
+ status: 'open',
+ },
+};
diff --git a/src/livecodes/templates/multifile/lit.ts b/src/livecodes/templates/multifile/lit.ts
new file mode 100644
index 0000000000..8718adffb6
--- /dev/null
+++ b/src/livecodes/templates/multifile/lit.ts
@@ -0,0 +1,200 @@
+import type { Template } from '../../models';
+
+export const lit: Template = {
+ name: 'multifile-lit',
+ title: window.deps.translateString('templates.multifile.lit', 'Lit Template'),
+ thumbnail: 'assets/templates/lit.svg',
+ mainFile: 'index.html',
+ activeEditor: 'src/my-element.js',
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ Lit
+
+
+
+
+
+ Lit
+
+
+
+`,
+ },
+ {
+ filename: 'src/index.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/my-element.js',
+ language: 'javascript',
+ content: `import { LitElement, css, html } from "lit";
+import litLogo from "./assets/lit.svg";
+
+/**
+ * An example element.
+ *
+ * @slot - This element has a slot
+ * @csspart button - The button
+ */
+export class MyElement extends LitElement {
+ static get properties() {
+ return {
+ /**
+ * Copy for the read the docs hint.
+ */
+ docsHint: { type: String },
+
+ /**
+ * The number of times the button has been clicked.
+ */
+ count: { type: Number },
+ };
+ }
+
+ constructor() {
+ super();
+ this.docsHint = "Click on the Lit logo to learn more";
+ this.count = 0;
+ }
+
+ render() {
+ return html\`
+
+
+
+
+
+
+
+
+
+ \${this.docsHint}
+ \`;
+ }
+
+ _onClick() {
+ this.count++;
+ }
+
+ static get styles() {
+ return css\`
+ :host {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+ }
+
+ .logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+ }
+ .logo:hover {
+ filter: drop-shadow(0 0 2em #325cffaa);
+ }
+
+ .card {
+ padding: 2em;
+ }
+
+ .read-the-docs {
+ color: #888;
+ }
+
+ a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+ }
+ a:hover {
+ color: #535bf2;
+ }
+
+ ::slotted(h1) {
+ font-size: 3.2em;
+ line-height: 1.1;
+ }
+
+ button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+ }
+ button:hover {
+ border-color: #646cff;
+ }
+ button:focus,
+ button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+ }
+
+ @media (prefers-color-scheme: light) {
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+ }
+ \`;
+ }
+}
+
+window.customElements.define("my-element", MyElement);
+`,
+ },
+ {
+ filename: 'src/assets/lit.svg',
+ language: 'svg',
+ content: `
+`,
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/preact.ts b/src/livecodes/templates/multifile/preact.ts
new file mode 100644
index 0000000000..c47e612ed6
--- /dev/null
+++ b/src/livecodes/templates/multifile/preact.ts
@@ -0,0 +1,176 @@
+import type { Template } from '../../models';
+
+export const preact: Template = {
+ name: 'multifile-preact',
+ title: window.deps.translateString('templates.multifile.preact', 'Preact Template'),
+ thumbnail: 'assets/templates/preact.svg',
+ mainFile: 'index.html',
+ activeEditor: 'src/app.jsx',
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ Preact
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/main.jsx',
+ language: 'tsx',
+ content: `import { render } from "preact";
+import "./index.css";
+import { App } from "./app.jsx";
+
+render( , document.getElementById("app"));
+`,
+ },
+ {
+ filename: 'src/index.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/app.jsx',
+ language: 'jsx',
+ content: `import { useState } from "preact/hooks";
+import preactLogo from "./assets/preact.svg";
+import "./app.css";
+
+export function App() {
+ const [count, setCount] = useState(0);
+
+ return (
+ <>
+
+
+
+
+
+ Preact
+
+
+
+
+ Click on the Preact logo to learn more
+
+ >
+ );
+}
+`,
+ },
+ {
+ filename: 'src/app.css',
+ language: 'css',
+ content: `#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #673ab8aa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+`,
+ },
+ {
+ filename: 'src/assets/preact.svg',
+ language: 'svg',
+ content: `
+`,
+ },
+ ],
+ customSettings: { typescript: { jsxImportSource: 'preact' } },
+};
diff --git a/src/livecodes/templates/multifile/react.ts b/src/livecodes/templates/multifile/react.ts
new file mode 100644
index 0000000000..ddc17767d7
--- /dev/null
+++ b/src/livecodes/templates/multifile/react.ts
@@ -0,0 +1,197 @@
+import type { Template } from '../../models';
+
+export const react: Template = {
+ name: 'multifile-react',
+ title: window.deps.translateString('templates.multifile.react', 'React Template'),
+ thumbnail: 'assets/templates/react.svg',
+ mainFile: 'index.html',
+ activeEditor: 'src/App.tsx',
+ fileLanguages: { jsx: 'react', tsx: 'react.tsx' },
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ React + TS
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/main.tsx',
+ language: 'react.tsx',
+ content: `import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import App from "./App.tsx";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
+`,
+ },
+ {
+ filename: 'src/index.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/App.tsx',
+ language: 'react.tsx',
+ content: `import { useState } from "react";
+import reactLogo from "./assets/react.svg";
+import "./App.css";
+
+function App() {
+ const [count, setCount] = useState(0);
+
+ return (
+ <>
+
+
+
+
+
+ React
+
+
+
+ Click on the React logo to learn more
+ >
+ );
+}
+
+export default App;
+`,
+ },
+ {
+ filename: 'src/App.css',
+ language: 'css',
+ content: `#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+`,
+ },
+ {
+ filename: 'src/assets/react.svg',
+ language: 'svg',
+ content: ``,
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/solid.ts b/src/livecodes/templates/multifile/solid.ts
new file mode 100644
index 0000000000..f0d2853603
--- /dev/null
+++ b/src/livecodes/templates/multifile/solid.ts
@@ -0,0 +1,182 @@
+import type { Template } from '../../models';
+
+export const solid: Template = {
+ name: 'multifile-solid',
+ title: window.deps.translateString('templates.multifile.solid', 'Solid Template'),
+ thumbnail: 'assets/templates/solid.svg',
+ mainFile: 'index.html',
+ activeEditor: 'src/App.tsx',
+ customSettings: { fileLanguages: { jsx: 'solid', tsx: 'solid.tsx' } },
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ Solid + TS
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/index.tsx',
+ language: 'solid.tsx',
+ content: `import { render } from 'solid-js/web'
+import './index.css'
+import App from './App.tsx'
+
+const root = document.getElementById('root')
+
+render(() => , root!)
+`,
+ },
+ {
+ filename: 'src/index.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/App.tsx',
+ language: 'solid.tsx',
+ content: `import { createSignal } from "solid-js";
+import solidLogo from "./assets/solid.svg";
+import "./App.css";
+
+function App() {
+ const [count, setCount] = createSignal(0);
+
+ return (
+ <>
+
+
+
+
+
+ Solid
+
+
+
+
+ Click on the Solid logo to learn more
+
+ >
+ );
+}
+
+export default App;
+`,
+ },
+ {
+ filename: 'src/App.css',
+ language: 'css',
+ content: `#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+`,
+ },
+ {
+ filename: 'src/assets/solid.svg',
+ language: 'svg',
+ content: `
+`,
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/svelte.ts b/src/livecodes/templates/multifile/svelte.ts
new file mode 100644
index 0000000000..116e7e1d23
--- /dev/null
+++ b/src/livecodes/templates/multifile/svelte.ts
@@ -0,0 +1,188 @@
+import type { Template } from '../../models';
+
+export const svelte: Template = {
+ name: 'multifile-svelte',
+ title: window.deps.translateString('templates.multifile.svelte', 'Svelte Template'),
+ thumbnail: 'assets/templates/svelte.svg',
+ mainFile: 'index.html',
+ activeEditor: 'src/App.svelte',
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ Svelte + TS
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/main.ts',
+ language: 'typescript',
+ content: `import { mount } from "svelte";
+import "./app.css";
+import App from "./App.svelte";
+
+const app = mount(App, {
+ target: document.getElementById("app"),
+});
+
+export default app;
+`,
+ },
+ {
+ filename: 'src/app.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+.card {
+ padding: 2em;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/App.svelte',
+ language: 'svelte',
+ content: `
+
+
+
+
+
+
+
+ Svelte
+
+
+
+
+
+
+ Click on the Svelte logo to learn more
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/lib/Counter.svelte',
+ language: 'svelte',
+ content: `
+
+
+`,
+ },
+ {
+ filename: 'src/assets/svelte.svg',
+ language: 'svg',
+ content: `
+`,
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/typescript.ts b/src/livecodes/templates/multifile/typescript.ts
new file mode 100644
index 0000000000..ccddd886ba
--- /dev/null
+++ b/src/livecodes/templates/multifile/typescript.ts
@@ -0,0 +1,175 @@
+import type { Template } from '../../models';
+
+export const typescript: Template = {
+ name: 'multifile-typescript',
+ title: window.deps.translateString('templates.multifile.typescript', 'TypeScript Template'),
+ thumbnail: 'assets/templates/typescript.svg',
+ aliases: ['multifile-ts'],
+ mainFile: 'index.html',
+ activeEditor: 'src/main.ts',
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ TypeScript
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/main.ts',
+ language: 'typescript',
+ content: `import "./style.css";
+import typescriptLogo from "./typescript.svg";
+import { setupCounter } from "./counter.ts";
+
+document.querySelector("#app")!.innerHTML = \`
+
+\`;
+
+setupCounter(document.querySelector("#counter")!);
+`,
+ },
+ {
+ filename: 'src/style.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #3178c6aa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/counter.ts',
+ language: 'typescript',
+ content: `export function setupCounter(element: HTMLButtonElement) {
+ let counter = 0;
+ const setCounter = (count: number) => {
+ counter = count;
+ element.innerHTML = \`count is \${counter}\`;
+ };
+ element.addEventListener("click", () => setCounter(counter + 1));
+ setCounter(0);
+}
+`,
+ },
+ {
+ filename: 'src/typescript.svg',
+ language: 'svg',
+ content: `
+`,
+ },
+ ],
+};
diff --git a/src/livecodes/templates/multifile/vue.ts b/src/livecodes/templates/multifile/vue.ts
new file mode 100644
index 0000000000..d99d6734da
--- /dev/null
+++ b/src/livecodes/templates/multifile/vue.ts
@@ -0,0 +1,184 @@
+import type { Template } from '../../models';
+
+export const vue: Template = {
+ name: 'multifile-vue',
+ title: window.deps.translateString('templates.multifile.vue', 'Vue Template'),
+ thumbnail: 'assets/templates/vue.svg',
+ mainFile: 'index.html',
+ activeEditor: 'src/components/HelloWorld.vue',
+ files: [
+ {
+ filename: 'index.html',
+ language: 'html',
+ content: `
+
+
+
+
+ Vue
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/main.ts',
+ language: 'typescript',
+ content: `import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+
+createApp(App).mount('#app')
+`,
+ },
+ {
+ filename: 'src/style.css',
+ language: 'css',
+ content: `:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+.card {
+ padding: 2em;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+`,
+ },
+ {
+ filename: 'src/App.vue',
+ language: 'vue',
+ content: `
+
+
+
+
+
+
+
+
+
+
+
+`,
+ },
+ {
+ filename: 'src/components/HelloWorld.vue',
+ language: 'vue',
+ content: `
+
+
+ {{ msg }}
+
+
+
+
+
+ Click on the Vue logo to learn more
+
+
+
+`,
+ },
+ {
+ filename: 'src/assets/vue.svg',
+ language: 'svg',
+ content: ``,
+ },
+ ],
+};
diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts
index 4bc1caaf2a..007c0c7b90 100644
--- a/src/livecodes/templates/starter/index.ts
+++ b/src/livecodes/templates/starter/index.ts
@@ -1,5 +1,6 @@
// this is bundled to build/livecodes/templates.js
+import { multifileTemplates } from '../multifile/index';
import { angularStarter } from './angular-starter';
import { assemblyscriptStarter } from './assemblyscript-starter';
import { astroStarter } from './astro-starter';
@@ -140,4 +141,5 @@ export const starterTemplates = [
minizincStarter,
blocklyStarter,
diagramsStarter,
+ ...multifileTemplates,
];
diff --git a/src/livecodes/templates/starter/preact-starter.ts b/src/livecodes/templates/starter/preact-starter.ts
index b2c2d81cfb..80796c5828 100644
--- a/src/livecodes/templates/starter/preact-starter.ts
+++ b/src/livecodes/templates/starter/preact-starter.ts
@@ -25,8 +25,7 @@ export const preactStarter: Template = {
script: {
language: 'jsx',
content: `
-/** @jsx h */
-import { h, render } from 'preact';
+import { render } from 'preact';
import { useSignal } from "@preact/signals";
function App(props) {
@@ -41,7 +40,8 @@ function App(props) {
);
}
-render( , document.body);
+render( , document.getElementById("app"));
`.trimStart(),
},
+ customSettings: { typescript: { jsxImportSource: 'preact' } },
};
diff --git a/src/livecodes/types/type-loader.ts b/src/livecodes/types/type-loader.ts
index 4512190145..6ae1969597 100644
--- a/src/livecodes/types/type-loader.ts
+++ b/src/livecodes/types/type-loader.ts
@@ -121,13 +121,21 @@ export const createTypeLoader = (baseUrl: string) => {
];
};
+ let isRunning = false;
+
const load = async (
code: string,
configTypes: Types,
loadAll = false,
forceLoad = false,
callback?: (type: EditorLibrary) => void,
- ) => {
+ ): Promise => {
+ if (isRunning) {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ return load(code, configTypes, loadAll, forceLoad, callback);
+ }
+ isRunning = true;
+
const imports = getImports(code);
const codeTypes: Types = imports.reduce((accTypes, lib) => {
@@ -152,7 +160,9 @@ export const createTypeLoader = (baseUrl: string) => {
};
}, {} as Types);
- const typesToGet = Object.keys(codeTypes).filter((key) => codeTypes[key] === '');
+ const typesToGet = Object.keys(codeTypes).filter(
+ (key) => codeTypes[key] === '' && !loadedTypes[key],
+ );
// mark as loaded to avoid re-fetching
loadedTypes = {
@@ -173,6 +183,7 @@ export const createTypeLoader = (baseUrl: string) => {
const newLibs = await loadTypes({ ...codeTypes, ...fetchedTypes, ...autoloadTypes }, callback);
+ isRunning = false;
return loadAll ? libs : newLibs;
};
diff --git a/src/livecodes/utils/utils.ts b/src/livecodes/utils/utils.ts
index 47df8efc0c..3bfe805991 100644
--- a/src/livecodes/utils/utils.ts
+++ b/src/livecodes/utils/utils.ts
@@ -34,6 +34,7 @@ export const escapeCode = /* @__PURE__ */ (code: string, slash = true) =>
code
.replace(/\\/g, slash ? '\\\\' : '\\')
.replace(/`/g, '\\`')
+ .replace(/\$/g, '\\$')
.replace(/<\/script>/g, '<\\/script>');
export const pipe = /* @__PURE__ */ (...fns: Function[]) =>
@@ -348,9 +349,6 @@ export const runOrContinue =
}
};
-export const getFileExtension = /* @__PURE__ */ (file: string) =>
- file.split('.')[file.split('.').length - 1];
-
export const isInIframe = /* @__PURE__ */ () => {
// TODO allow in storybook
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') return false;
@@ -685,6 +683,20 @@ export const addProp = /* @__PURE__ */ (
addProp(obj[first] as Record, rest.join('.'), value);
};
+export const handleSlash = /* @__PURE__ */ (x: string) => {
+ if (!x) return '';
+ x = x.replaceAll('\\', '/');
+ return x.startsWith('/')
+ ? x.slice(1)
+ : x.startsWith('./')
+ ? x.slice(2)
+ : x.startsWith('../')
+ ? x.slice(3)
+ : x.startsWith('~/')
+ ? x.slice(2)
+ : x;
+};
+
export const onLoad = /* @__PURE__ */ (fn: (...args: any[]) => any) => {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
fn();
@@ -693,6 +705,12 @@ export const onLoad = /* @__PURE__ */ (fn: (...args: any[]) => any) => {
}
};
+export const removeFormatting = (e: any) => {
+ e.preventDefault();
+ const text = e.clipboardData.getData('text/plain');
+ document.execCommand('insertHTML', false, text);
+};
+
export const predefinedValues = {
APP_VERSION: process.env.VERSION || '',
SDK_VERSION: process.env.SDK_VERSION || '',
diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts
index 46e05a0262..2a4d4ca903 100644
--- a/src/livecodes/vendors.ts
+++ b/src/livecodes/vendors.ts
@@ -3,7 +3,7 @@ import { modulesService } from './services/modules';
const { getUrl, getModuleUrl } = modulesService;
export const vendorsBaseUrl = // 'http://127.0.0.1:8081/';
- /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.6/dist/');
+ /* @__PURE__ */ getUrl('@live-codes/browser-compilers@0.22.7/dist/');
export const acornUrl = /* @__PURE__ */ getUrl('acorn@8.12.1/dist/acorn.js');
@@ -90,7 +90,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.4/build/');
+export const codeMirrorBaseUrl = /* @__PURE__ */ getUrl('@live-codes/codemirror@0.3.5/build/');
export const codemirrorMinimapUrl = /* @__PURE__ */ getUrl(
'@replit/codemirror-minimap@0.5.2/dist/index.js',
@@ -126,6 +126,10 @@ export const doppioJvmBaseUrl = 'https://unpkg.com/@seth0x41/doppio@1.0.0/';
export const dotUrl = /* @__PURE__ */ getUrl('dot@1.1.3/doT.js');
+export const draggableUrl = /* @__PURE__ */ getUrl(
+ '@shopify/draggable@1.2.1/build/umd/index.min.js',
+);
+
export const ejsUrl = /* @__PURE__ */ getUrl('ejs@4.0.1/ejs.js');
export const elkjsBaseUrl = /* @__PURE__ */ getUrl('elkjs@0.8.2/lib/');
@@ -272,6 +276,10 @@ export const jsclUrl = /* @__PURE__ */ getUrl(
'gh:jscl-project/jscl-project.github.io@058adc599f0d012718ef3ad28e704a92c4dd741e/jscl.js',
);
+export const json5Url = /* @__PURE__ */ getUrl('json5@2.2.3/dist/index.min.js');
+
+export const jsYamlUrl = /* @__PURE__ */ getUrl('js-yaml@4.1.1/dist/js-yaml.min.js');
+
export const jsZipUrl = /* @__PURE__ */ getUrl('jszip@3.10.1/dist/jszip.js');
export const juliaWasmBaseUrl = /* @__PURE__ */ getUrl('@chriskoch/julia-wasm@1.0.4');
@@ -320,10 +328,6 @@ 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 mustacheUrl = /* @__PURE__ */ getUrl('mustache@4.2.0/mustache.js');
export const ninjaKeysUrl = /* @__PURE__ */ getUrl('@hatemhosny/ninja-keys@1.14.0/bundle/index.js');
diff --git a/src/sdk/__tests__/getPlaygroundUrl.test.ts b/src/sdk/__tests__/getPlaygroundUrl.test.ts
index fa41bd8c2d..959a47b607 100644
--- a/src/sdk/__tests__/getPlaygroundUrl.test.ts
+++ b/src/sdk/__tests__/getPlaygroundUrl.test.ts
@@ -1,6 +1,5 @@
import { compressToEncodedURIComponent } from 'lz-string';
-import { getPlaygroundUrl } from '../index';
-import type { Config, EmbedOptions, UrlQueryParams } from '../models';
+import { getPlaygroundUrl, type Config, type EmbedOptions } from '../index';
test('empty options object', () => {
const url = new URL(getPlaygroundUrl());
@@ -16,7 +15,7 @@ test('passing some params, they should be stored in hash params', () => {
param1: 1,
param2: 2,
};
- const url = new URL(getPlaygroundUrl({ params: params as UrlQueryParams }));
+ const url = new URL(getPlaygroundUrl({ params: params as EmbedOptions['params'] }));
const searchParams = url.searchParams;
const receivedParams = searchParams.get('params');
expect(receivedParams).toBeNull();
diff --git a/src/sdk/index.ts b/src/sdk/index.ts
index e8a5c4b2ac..360b19350b 100644
--- a/src/sdk/index.ts
+++ b/src/sdk/index.ts
@@ -3,7 +3,7 @@ import { compressToEncodedURIComponent } from 'lz-string';
import type {
API,
Code,
- Config,
+ SDKConfig as Config,
CustomEvents,
EmbedOptions,
Language,
diff --git a/src/sdk/models.ts b/src/sdk/models.ts
index db669faeb6..bfa845ba62 100644
--- a/src/sdk/models.ts
+++ b/src/sdk/models.ts
@@ -58,10 +58,19 @@ export interface API {
*
* createPlayground("#container").then(async (playground) => {
* const config = await playground.getConfig();
+ *
+ * // check if a multi-file project
+ * if ("files" in config) {
+ * config.files.forEach((file) => {
+ * const { filename, content, language } = file;
+ * })
+ * } else { // single-file (3-editor) project
+ * const { content, language } = config.script; // script editor
+ * }
* });
* ```
*/
- getConfig: (contentOnly?: boolean) => Promise;
+ getConfig: (contentOnly?: boolean) => Promise;
/**
* Loads a new project using the passed configuration object.
@@ -81,7 +90,7 @@ export interface API {
* });
* ```
*/
- setConfig: (config: Partial) => Promise;
+ setConfig: (config: Partial) => Promise;
/**
* Gets the playground code (including source code, source language and compiled code) for each editor (markup, style, script), in addition to result page HTML.
@@ -94,8 +103,14 @@ export interface API {
* createPlayground("#container").then(async (playground) => {
* const code = await playground.getCode();
*
- * // source code, language and compiled code for the script editor
- * const { content, language, compiled } = code.script;
+ * // check if a multi-file project
+ * if ("files" in code) {
+ * code.files.forEach((file) => {
+ * const { filename, content, language, compiled } = file;
+ * })
+ * } else { // single-file (3-editor) project
+ * const { content, language, compiled } = code.script; // script editor
+ * }
*
* // result page HTML
* const result = code.result;
@@ -139,7 +154,7 @@ export interface API {
*
* @deprecated Use [`watch`](https://livecodes.io/docs/sdk/js-ts#watch) method instead.
*/
- onChange: (fn: (data: { code: Code; config: Config }) => void) => { remove: () => void };
+ onChange: (fn: (data: { code: Code; config: ExportedConfig }) => void) => { remove: () => void };
/**
* Allows to watch for various playground events.
@@ -229,7 +244,7 @@ export type WatchLoad = (event: 'load', fn: () => void) => { remove: () => void
*/
export type WatchReady = (
event: 'ready',
- fn: (data: { config: Config }) => void,
+ fn: (data: { config: ExportedConfig }) => void,
) => { remove: () => void };
/**
@@ -247,7 +262,7 @@ export type WatchReady = (
*/
export type WatchCode = (
event: 'code',
- fn: (data: { code: Code; config: Config }) => void,
+ fn: (data: { code: Code; config: ExportedConfig }) => void,
) => { remove: () => void };
export type WatchConsole = (
@@ -275,6 +290,8 @@ export type Prettify = {
[K in keyof T]: T[K] extends object ? Prettify : T[K];
} & {};
+export type NonEmptyArray = [T, ...T[]];
+
export type WatchFn = UnionToIntersection;
export type APICommands = 'setBroadcastToken' | 'showVersion';
@@ -343,7 +360,7 @@ export interface EmbedOptions {
* If supplied and is not an object or a valid URL, an error is thrown.
* @default {}
*/
- config?: Partial | string;
+ config?: Partial | string;
/**
* If `true`, the playground is loaded in [headless mode](https://livecodes.io/docs/sdk/headless).
@@ -405,6 +422,59 @@ export interface EmbedOptions {
*/
export interface Config extends ContentConfig, AppConfig, UserConfig {}
+export interface SingleFileConfig
+ extends Omit,
+ AppConfig,
+ UserConfig {
+ files: never;
+ mainFile: never;
+ fileLanguages: never;
+ lockFiles: never;
+}
+
+export interface MultiFileConfig
+ extends Omit<
+ ContentConfig,
+ | 'files'
+ | 'markup'
+ | 'style'
+ | 'script'
+ | 'stylesheets'
+ | 'scripts'
+ | 'cssPreset'
+ | 'htmlAttrs'
+ | 'head'
+ >,
+ AppConfig,
+ UserConfig {
+ files: NonEmptyArray<{ filename: string } & Partial>;
+ markup: never;
+ style: never;
+ script: never;
+ stylesheets: never;
+ scripts: never;
+ cssPreset: never;
+ htmlAttrs: never;
+ head: never;
+}
+
+export type SDKConfig = Prettify | Prettify;
+export type ExportedConfig =
+ | Prettify>
+ | Prettify<
+ Omit<
+ MultiFileConfig,
+ | 'markup'
+ | 'style'
+ | 'script'
+ | 'stylesheets'
+ | 'scripts'
+ | 'cssPreset'
+ | 'htmlAttrs'
+ | 'head'
+ >
+ >;
+
/**
* The properties that define the content of the current [project](https://livecodes.io/docs/features/projects).
*/
@@ -450,9 +520,9 @@ export interface ContentConfig {
* Selects the active editor to show.
*
* Defaults to the last used editor for user, otherwise `"markup"`
- * @type {`"markup"` | `"style"` | `"script"` | `undefined`}
+ * @type {`"markup"` | `"style"` | `"script"` | `string` | `undefined`}
*/
- activeEditor: EditorId | undefined;
+ activeEditor: EditorId | (string & {}) | undefined;
/**
* List of enabled languages.
@@ -485,6 +555,32 @@ export interface ContentConfig {
*/
script: Prettify;
+ /**
+ * List of source files.
+ */
+ files: SourceFile[];
+
+ /**
+ * The name of the main markup file.
+ * @default "index.html"
+ */
+ mainFile?: string;
+
+ /**
+ * An object with file extensions and languages to use for them.
+ * This overrides the default mapping of file extensions to languages.
+ * It is ignored for files that have the `language` property explicitly set (see {@link Config.files}).
+ * @example
+ * { jsx: "solid", tsx: "solid.tsx" }
+ */
+ fileLanguages?: Partial>;
+
+ /**
+ * When `true`, the user won't be able to add/rename/re-order/delete files. The file content can still be edited.
+ * @default false
+ */
+ lockFiles?: boolean;
+
/**
* List of URLs for [external stylesheets](https://livecodes.io/docs/features/external-resources) to add to the [result page](https://livecodes.io/docs/features/result).
*/
@@ -583,6 +679,16 @@ export interface ContentConfig {
readonly version: string;
}
+export type SourceFile = Prettify<
+ {
+ /**
+ * Name of the file with extension, including path (e.g. `index.html` or `components/Counter.jsx`).
+ */
+ filename: string;
+ } & Required> &
+ Partial>
+>;
+
/**
* These are properties that define how the app behaves.
*/
@@ -890,6 +996,7 @@ export interface AppData {
export type Language =
| 'html'
| 'htm'
+ | 'svg'
| 'markdown'
| 'md'
| 'mdown'
@@ -941,6 +1048,8 @@ export type Language =
| 'js'
| 'mjs'
| 'json'
+ | 'json5'
+ | 'jsonc'
| 'babel'
| 'es'
| 'sucrase'
@@ -967,11 +1076,14 @@ export type Language =
| 'svelte'
| 'svelte-app'
| 'app.svelte'
+ | 'svelte.js'
+ | 'svelte.ts'
| 'stencil'
| 'stencil.tsx'
| 'solid'
| 'solid.jsx'
| 'solid.tsx'
+ | 'solid-tsx'
| 'riot'
| 'riotjs'
| 'malina'
@@ -1100,7 +1212,47 @@ export type Language =
| 'blockly'
| 'blockly.xml'
| 'xml'
- | 'pintora';
+ | 'pintora'
+ | 'text'
+ | 'txt'
+ | 'csv'
+ | 'tsv'
+ | 'plaintext'
+ | 'yaml'
+ | 'yml'
+ | 'binary'
+ | 'png'
+ | 'jpg'
+ | 'jpeg'
+ | 'gif'
+ | 'webp'
+ | 'bmp'
+ | 'tif'
+ | 'tiff'
+ | 'ico'
+ | 'ttf'
+ | 'otf'
+ | 'woff'
+ | 'woff2'
+ | 'mp4'
+ | 'mpeg'
+ | 'webm'
+ | 'ogv'
+ | 'ogg'
+ | 'mov'
+ | 'mp3'
+ | 'm4a'
+ | 'wav'
+ | 'oga'
+ | 'mid'
+ | 'midi'
+ | 'dotenv'
+ | 'env'
+ | 'env.local'
+ | 'env.development'
+ | 'env.production'
+ | 'env.development.local'
+ | 'env.production.local';
export interface Editor {
/**
@@ -1123,6 +1275,13 @@ export interface Editor {
*/
contentUrl?: string;
+ /**
+ * If `true`, the code editor is hidden, however its code is still evaluated.
+ *
+ * This can be useful in embedded playgrounds (e.g. for hiding irrelevant code).
+ */
+ hidden?: boolean;
+
/**
* Hidden content that gets evaluated without being visible in the code editor.
*
@@ -1137,14 +1296,6 @@ export interface Editor {
*/
hiddenContentUrl?: string;
- /**
- * Lines that get folded when the editor loads.
- *
- * This can be used for less relevant content.
- * @example [{ from: 5, to: 8 }, { from: 15, to: 20 }]
- */
- foldedLines?: Array<{ from: number; to: number }>;
-
/**
* If set, this is used as the title of the editor in the UI,
* overriding the default title set to the language name
@@ -1153,6 +1304,9 @@ export interface Editor {
title?: string;
/**
+ * @deprecated
+ * Use `hidden` instead.
+ *
* If `true`, the title of the code editor is hidden, however its code is still evaluated.
*
* This can be useful in embedded playgrounds (e.g. for hiding unnecessary code).
@@ -1170,6 +1324,14 @@ export interface Editor {
*/
selector?: string;
+ /**
+ * Lines that get folded when the editor loads.
+ *
+ * This can be used for less relevant content.
+ * @example [{ from: 5, to: 8 }, { from: 15, to: 20 }]
+ */
+ foldedLines?: Array<{ from: number; to: number }>;
+
/**
* The initial position of the cursor in the code editor.
* @example {lineNumber: 5, column: 10}
@@ -1182,14 +1344,16 @@ export interface EditorPosition {
column?: number;
}
-export type EditorId = 'markup' | 'style' | 'script';
+export type EditorId = 'markup' | 'style' | 'script' | (string & {});
export interface Editors {
+ [key: string]: CodeEditor;
markup: CodeEditor;
style: CodeEditor;
script: CodeEditor;
}
export interface EditorLanguages {
+ [key: string]: Language;
markup: Language;
style: Language;
script: Language;
@@ -1239,6 +1403,7 @@ export interface LanguageSpecs {
editorSupport?: LanguageEditorSupport;
preset?: CssPresetId;
largeDownload?: boolean;
+ multiFileSupport?: boolean;
}
export interface ProcessorSpecs {
@@ -1278,6 +1443,9 @@ export type ParserName =
| 'babel'
| 'babel-ts'
| 'babel-flow'
+ | 'json'
+ | 'json5'
+ | 'jsonc'
| 'glimmer'
| 'html'
| 'markdown'
@@ -1320,6 +1488,7 @@ export interface EditorLibrary {
}
export interface CompileOptions {
+ filename: string;
html?: string;
blockly?: BlocklyContent;
forceCompile?: boolean;
@@ -1327,7 +1496,7 @@ export interface CompileOptions {
}
export interface CompileInfo {
- cssModules?: Record;
+ cssModules?: Record>;
modifiedHTML?: string;
importedContent?: string;
imports?: Record;
@@ -1407,7 +1576,10 @@ export interface Compilers {
[language: string]: Compiler;
}
-export type Template = Pick &
+export type Template = (
+ | Pick
+ | Pick
+) &
Partial & {
name: TemplateName;
aliases?: TemplateAlias[];
@@ -1485,7 +1657,18 @@ export type TemplateName =
| 'prolog'
| 'minizinc'
| 'blockly'
- | 'diagrams';
+ | 'diagrams'
+ | 'multifile-blank'
+ | 'multifile-basic'
+ | 'multifile-javascript'
+ | 'multifile-typescript'
+ | 'multifile-react'
+ | 'multifile-vue'
+ | 'multifile-preact'
+ | 'multifile-svelte'
+ | 'multifile-solid'
+ | 'multifile-lit'
+ | 'multifile-jest';
export type TemplateAlias =
| 'js'
@@ -1518,7 +1701,9 @@ export type TemplateAlias =
| 'postgres'
| 'pg'
| 'pgsql'
- | 'mzn';
+ | 'mzn'
+ | 'multifile-js'
+ | 'multifile-ts';
export interface Tool {
name: 'console' | 'compiled' | 'tests';
@@ -1592,6 +1777,7 @@ export interface CodeEditor {
getLanguage: () => Language;
setLanguage: (language: Language, value?: string) => void;
getEditorId: () => string;
+ setEditorId: (filename: string, language?: Language) => void;
focus: () => void;
getPosition: () => EditorPosition;
setPosition: (position: EditorPosition) => void;
@@ -1939,6 +2125,7 @@ export type CustomSettings = Partial<
convertCommonjs: boolean;
defaultCDN: CDN;
types: Types;
+ fileLanguages: Config['fileLanguages'];
}
>;
@@ -1973,39 +2160,51 @@ export type EditorCache = Editor & {
modified?: string;
};
-export type Cache = ContentConfig & {
+export type Cache = Omit & {
markup: EditorCache;
style: EditorCache;
script: EditorCache;
tests?: EditorCache;
+ files: Array;
+ mainFile?: string;
result?: string;
styleOnlyUpdate?: boolean;
};
/**
- * An object that contains the language, content and compiled code for each of the 3 [code editors](https://livecodes.io/docs/features/projects)
+ * An object that contains the language, content and compiled code for each of the [code editors](https://livecodes.io/docs/features/projects)/files
* and the [result page](https://livecodes.io/docs/features/result) HTML.
*
* See [docs](https://livecodes.io/docs/api/interfaces/Code) for details.
*/
-export interface Code {
- markup: {
- language: Language;
- content: string;
- compiled: string;
- };
- style: {
- language: Language;
- content: string;
- compiled: string;
- };
- script: {
- language: Language;
- content: string;
- compiled: string;
- };
- result: string;
-}
+export type Code = { result: string } & (
+ | {
+ markup: {
+ language: Language;
+ content: string;
+ compiled: string;
+ };
+ style: {
+ language: Language;
+ content: string;
+ compiled: string;
+ };
+ script: {
+ language: Language;
+ content: string;
+ compiled: string;
+ };
+ }
+ | {
+ files: Array<{
+ filename: string;
+ language: Language;
+ content: string;
+ compiled: string;
+ }>;
+ mainFile: string;
+ }
+);
export type Theme = 'light' | 'dark';
diff --git a/storybook/package-lock.json b/storybook/package-lock.json
index 6ed903d459..214b3a82d1 100644
--- a/storybook/package-lock.json
+++ b/storybook/package-lock.json
@@ -26,9 +26,10 @@
"@types/dedent": "0.7.0",
"@types/flat": "5.0.2",
"@types/react": "18.3.18",
+ "@types/webpack-env": "1.18.8",
"babel-loader": "8.3.0",
"cross-env": "7.0.3",
- "typescript": "5.4.5"
+ "typescript": "5.9.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -4785,10 +4786,14 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "18.11.18",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
- "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
- "dev": true
+ "version": "20.19.33",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
+ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
},
"node_modules/@types/node-fetch": {
"version": "2.6.2",
@@ -4894,10 +4899,11 @@
}
},
"node_modules/@types/webpack-env": {
- "version": "1.18.0",
- "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.0.tgz",
- "integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==",
- "dev": true
+ "version": "1.18.8",
+ "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz",
+ "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/webpack-sources": {
"version": "3.2.0",
@@ -17009,10 +17015,11 @@
}
},
"node_modules/typescript": {
- "version": "5.4.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
- "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17049,6 +17056,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unfetch": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
@@ -22075,10 +22089,13 @@
"dev": true
},
"@types/node": {
- "version": "18.11.18",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
- "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
- "dev": true
+ "version": "20.19.33",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
+ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~6.21.0"
+ }
},
"@types/node-fetch": {
"version": "2.6.2",
@@ -22184,9 +22201,9 @@
}
},
"@types/webpack-env": {
- "version": "1.18.0",
- "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.0.tgz",
- "integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==",
+ "version": "1.18.8",
+ "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz",
+ "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==",
"dev": true
},
"@types/webpack-sources": {
@@ -31760,9 +31777,9 @@
}
},
"typescript": {
- "version": "5.4.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
- "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true
},
"uglify-js": {
@@ -31784,6 +31801,12 @@
"which-boxed-primitive": "^1.0.2"
}
},
+ "undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true
+ },
"unfetch": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
diff --git a/storybook/package.json b/storybook/package.json
index 865e539bb5..a5eb9523dd 100644
--- a/storybook/package.json
+++ b/storybook/package.json
@@ -23,9 +23,10 @@
"@types/dedent": "0.7.0",
"@types/flat": "5.0.2",
"@types/react": "18.3.18",
+ "@types/webpack-env": "1.18.8",
"babel-loader": "8.3.0",
"cross-env": "7.0.3",
- "typescript": "5.4.5"
+ "typescript": "5.9.3"
},
"dependencies": {
"dedent": "0.7.0",
diff --git a/tsconfig.json b/tsconfig.json
index ebb5d652a0..eb853c04e0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -43,7 +43,7 @@
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"jsx": "react-jsx",
- "lib": ["es2022", "dom", "dom.iterable"],
+ "lib": ["esnext", "dom", "dom.iterable"],
"types": ["node", "jest"],
"typeRoots": ["node_modules/@types"]
},
diff --git a/vendor-licenses.md b/vendor-licenses.md
index ac70c65302..b7d2b948d3 100644
--- a/vendor-licenses.md
+++ b/vendor-licenses.md
@@ -62,6 +62,10 @@ dart-sass: [MIT License](https://github.com/sass/dart-sass/blob/e3bf3eb3a3a87088
DoppioJVM: [MIT License](https://github.com/plasma-umass/doppio/blob/9a8bcf705c74f91301a542abfe8b7175ca757259/LICENSE)
+dotenv: [BSD 2-Clause License](https://github.com/motdotla/dotenv/blob/9d93f227bd04e1c364da31128a3606f98b321e61/LICENSE)
+
+dotenv-expand: [BSD 2-Clause License](https://github.com/motdotla/dotenv-expand/blob/eac922076a34dbde5c478d05725b0e366bf1751b/LICENSE)
+
dts-bundle: [MIT License](https://github.com/TypeStrong/dts-bundle/blob/2ca1591e890dc4276efc4bb0893367e6ff32a039/LICENSE-MIT)
EasyQRCodeJS: [MIT License](https://github.com/ushelp/EasyQRCodeJS/blob/573373b110d706132a41cc8abb5b297016b80094/LICENSE)
@@ -116,6 +120,10 @@ JSCL: [MIT License](https://cdn.jsdelivr.net/gh/jscl-project/jscl-project.github
JSCPP: [MIT License](https://github.com/felixhao28/JSCPP/blob/befbd6b48666007151c259c5dd291ab028ad4c04/LICENSE)
+JSON5: [MIT License](https://github.com/json5/json5/blob/b935d4a280eafa8835e6182551b63809e61243b0/LICENSE.md)
+
+js-yaml: [MIT License](https://github.com/nodeca/js-yaml/blob/74a01992953302040be3277c08ba214a06f00b87/LICENSE)
+
JSZip: [MIT License](https://github.com/Stuk/jszip/blob/3db5fdc85586ef6c26d15b503c45ce8e42905d77/LICENSE.markdown)
konnors-ninja-keys: [MIT License](https://github.com/KonnorRogers/konnors-ninja-keys/blob/6dde465ab2c64329c4284a1f95d9d7501acdb86a/LICENSE)
@@ -242,6 +250,8 @@ ruby.wasm: [MIT License](https://github.com/ruby/ruby.wasm/blob/097b7ca8d2ed2a98
Sass.js: [MIT License](https://github.com/medialize/sass.js/blob/71d9bed2cad10969efda9905aa1bddacc480f372/LICENSE)
+@shopify/draggable: [MIT License](https://github.com/Shopify/draggable/blob/8a1eed57f3ab2dff9371e8ce60fb39ac85871e8d/LICENSE.md)
+
SnackBar: [MIT License](https://github.com/egoist/snackbar/blob/4bc2fb7afd32d53a39661418fa5189dbb6e4aa86/LICENSE)
Solid: [MIT License](https://github.com/solidjs/solid/blob/0d83a1947aab4bea4223460d6756a38374ba391e/LICENSE)