Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions src/livecodes/compiler/compile.worker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import type TS from 'typescript';
import { getCompilerOptions } from '../editor/ts-compiler-options';
import { languages, processors } from '../languages';
import type { CompileOptions, Compilers, Config, EditorLibrary, Language } from '../models';
import { doOnce, objectFilter } from '../utils/utils';
import type {
CompileOptions,
CompileResult,
Compilers,
Config,
EditorLibrary,
Language,
} from '../models';
import { doOnce, getErrorMessage, objectFilter } from '../utils/utils';
import { codeMirrorBaseUrl, comlinkBaseUrl, vendorsBaseUrl } from '../vendors';
import { getAllCompilers } from './get-all-compilers';
import type { CompilerMessage, CompilerMessageEvent, LanguageOrProcessor } from './models';
Expand Down Expand Up @@ -98,13 +105,16 @@ const compile = async (
throw new Error('Failed to load compiler for: ' + language);
}

let value;
let value: string | CompileResult = '';
try {
value = await compiler(content, { config, language, baseUrl, options });
} catch (err) {
} catch (err: unknown) {
// eslint-disable-next-line no-console
console.error('Failed compiling: ' + language, err);
value = content;
value = {
code: '',
info: { errors: [getErrorMessage(err)] },
};
}
return value || '';
};
Expand Down
30 changes: 16 additions & 14 deletions src/livecodes/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,7 @@ const getResultPage = async ({
},
};

const compileResults = await Promise.all([
const [styleCompileResult, testsCompileResult] = await Promise.all([
compiler.compile(styleContent, styleLanguage, config, {
html: `${compiledMarkup}<script type="script-for-styles">${compiledScript}</script>
<script type="script-for-styles">${compileInfo.importedContent}</script>`,
Expand All @@ -1008,8 +1008,7 @@ const getResultPage = async ({
: compiler.compile(testsContent, testsLanguage, config, {})
: Promise.resolve(getCompileResult(getCache().tests?.compiled || '')),
]);

const [compiledStyle, compiledTests] = compileResults.map((result) => {
const [compiledStyle, compiledTests] = [styleCompileResult, testsCompileResult].map((result) => {
const { code, info } = getCompileResult(result);
compileInfo = {
...compileInfo,
Expand All @@ -1027,10 +1026,12 @@ const getResultPage = async ({
markup: {
...contentConfig.markup,
compiled: compiledMarkup,
modified: compiledMarkup,
},
style: {
...contentConfig.style,
compiled: compiledStyle,
modified: compiledStyle,
},
script: {
...contentConfig.script,
Expand All @@ -1045,6 +1046,7 @@ const getResultPage = async ({
compiled: compiledTests,
},
};
compiledCode.script.modified = compiledCode.script.compiled;

if (scriptType != null && scriptType !== 'module') {
singleFile = true;
Expand All @@ -1063,6 +1065,14 @@ const getResultPage = async ({

const styleOnlyUpdate = sourceEditor === 'style' && !compileInfo.cssModules;

const logError = (language: Language, errors: string[] = []) => {
errors.forEach((err) => toolsPane?.console?.error(`[${getLanguageTitle(language)}] ${err}`));
};
logError(markupLanguage, markupCompileResult.info?.errors);
logError(styleLanguage, styleCompileResult.info?.errors);
logError(scriptLanguage, scriptCompileResult.info?.errors);
logError(testsLanguage, getCompileResult(testsCompileResult).info?.errors);

if (singleFile) {
setCache({
...getCache(),
Expand Down Expand Up @@ -1124,17 +1134,9 @@ const flushResult = () => {
wat: ';; loading',
};

updateCache(
'markup',
compiledLanguages.markup,
loadingComments[compiledLanguages.markup] || 'html',
);
updateCache('style', compiledLanguages.style, loadingComments[compiledLanguages.style] || 'css');
updateCache(
'script',
compiledLanguages.script,
loadingComments[compiledLanguages.script] || 'javascript',
);
updateCache('markup', compiledLanguages.markup, loadingComments[compiledLanguages.markup] ?? '');
updateCache('style', compiledLanguages.style, loadingComments[compiledLanguages.style] ?? '');
updateCache('script', compiledLanguages.script, loadingComments[compiledLanguages.script] ?? '');
setCache({
...getCache(),
tests: {
Expand Down
6 changes: 5 additions & 1 deletion src/livecodes/languages/gleam/lang-gleam-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/* eslint-disable max-classes-per-file */
import type { CompilerFunction } from '../../models';
import { getErrorMessage } from '../../utils/utils';
import { gleamBaseUrl } from '../../vendors';
import { getLanguageCustomSettings } from '../utils';
import { modules, type Modules } from './gleam-modules';
Expand Down Expand Up @@ -193,7 +194,10 @@ import { modules, type Modules } from './gleam-modules';
} catch (error) {
// eslint-disable-next-line no-console
console.warn(error);
return '';
return {
code: '',
info: { errors: [getErrorMessage(error)] },
};
} finally {
project.delete();
}
Expand Down
27 changes: 20 additions & 7 deletions src/livecodes/languages/svelte/lang-svelte-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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 { getErrorMessage } from '../../utils/utils';
import { getLanguageByAlias, getLanguageCustomSettings } from '../utils';

(self as any).createSvelteCompiler = (): CompilerFunction => {
const MAIN_FILE = '__LiveCodes_App__.svelte';
const SECONDARY_FILE = '__LiveCodes_Component__.svelte';
let importedContent = '';
let imports: Record<string, string> = {};
let errors: string[] = [];

const compileSvelteSFC = async (
code: string,
Expand Down Expand Up @@ -42,21 +44,32 @@ import { getLanguageByAlias, getLanguageCustomSettings } from '../utils';
});
const customSettings = getLanguageCustomSettings('svelte', config);

const { js } = (window as any).svelte.compile(processedCode, {
css: 'injected',
filename,
...customSettings,
});

if (filename === MAIN_FILE || filename === SECONDARY_FILE) {
imports = createImportMap(importedContent, config);
errors = [];
}

Comment on lines 47 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Reset errors at the root entry, not here (prevents losing nested SFC errors) and also reset imports for SECONDARY_FILE.

Errors collected while compiling nested SFCs via replaceSFCImports are wiped here. Move the reset to the root-file preamble (for BOTH MAIN_FILE and SECONDARY_FILE) and keep this block for import map creation only.

Apply within this hunk:

       imports = createImportMap(importedContent, config);
-      errors = [];

Additionally (outside this hunk), ensure root entry resets state and handles empty code:

// right after detecting root file
if (filename === MAIN_FILE || filename === SECONDARY_FILE) {
  importedContent = '';
  imports = {};
  errors = [];
}
// ensure empty-root clears errors too
if (!code) {
  if (filename === MAIN_FILE || filename === SECONDARY_FILE) errors = [];
  return getCompileResult('');
}
🤖 Prompt for AI Agents
In src/livecodes/languages/svelte/lang-svelte-compiler.ts around lines 47-51, do
not reset errors inside this block—only build the import map for
MAIN_FILE/SECONDARY_FILE here (keep imports = createImportMap(importedContent,
config); remove errors = [] from this hunk). Instead, at the root-file preamble
(right after detecting filename === MAIN_FILE || filename === SECONDARY_FILE)
initialize/reset shared state by setting importedContent = '', imports = {}, and
errors = []; also add the empty-root guard: if (!code) { if (filename ===
MAIN_FILE || filename === SECONDARY_FILE) errors = []; return
getCompileResult(''); } so nested SFC compile errors aren’t wiped and root files
still reset state and handle empty code.

let js: { code: string };
try {
const result = (window as any).svelte.compile(processedCode, {
css: 'injected',
filename,
...customSettings,
});
Comment on lines +54 to +58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Stabilize component name for mount().

Pass name to svelte.compile so getMountCode’s LiveCodes_App symbol is guaranteed across Svelte versions.

       const result = (window as any).svelte.compile(processedCode, {
         css: 'injected',
         filename,
+        name: filename.replace(/\.svelte$/,'').replace(/[^A-Za-z0-9_$]/g, '_'),
         ...customSettings,
       });

🌐 Web query:

Does the current Svelte compiler support compile options { css: 'injected', name } and a `mount` API via `import { mount } from 'svelte'`?

💡 Result:

Yes.

  • The compiler supports compile options including css: 'injected' (and 'external' etc.) and related name/cssHash options. [1]
  • Svelte exposes an imperative mount/hydrate/unmount API (import { mount } from 'svelte'), introduced for the Svelte 5 component API. [2][3]

Sources:
[1] svelte/compiler docs (CompileOptions).
[2] Imperative component API (mount/hydrate/unmount).
[3] Svelte 5 migration guide (notes on mount/hydrate).


Stabilize component name for mount().

Svelte supports the compile option name and the imperative mount API — pass a deterministic name so getMountCode’s LiveCodes_App symbol is stable across Svelte versions.

File: src/livecodes/languages/svelte/lang-svelte-compiler.ts — Lines: 54-58

       const result = (window as any).svelte.compile(processedCode, {
         css: 'injected',
         filename,
+        name: filename.replace(/\.svelte$/,'').replace(/[^A-Za-z0-9_$]/g, '_'),
         ...customSettings,
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = (window as any).svelte.compile(processedCode, {
css: 'injected',
filename,
...customSettings,
});
const result = (window as any).svelte.compile(processedCode, {
css: 'injected',
filename,
name: filename.replace(/\.svelte$/,'').replace(/[^A-Za-z0-9_$]/g, '_'),
...customSettings,
});
🤖 Prompt for AI Agents
In src/livecodes/languages/svelte/lang-svelte-compiler.ts around lines 54-58,
the compile call needs a deterministic component name so getMountCode's
__LiveCodes_App__ symbol remains stable; add a name property with a fixed string
(e.g. "__LiveCodes_App__") to the compile options and ensure it overrides any
customSettings.name by placing it after the spread (or explicitly set
customSettings.name = "__LiveCodes_App__" before calling). Keep other options
(css, filename, customSettings) intact.

js = result.js;
} catch (err) {
errors.push(getErrorMessage(err));
return {
code: '',
info: { errors },
};
Comment on lines +62 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Keep info shape consistent on error path.

Return importedContent and imports alongside errors.

-      return {
-        code: '',
-        info: { errors },
-      };
+      return {
+        code: '',
+        info: { importedContent, imports, errors },
+      };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return {
code: '',
info: { errors },
};
return {
code: '',
info: { importedContent, imports, errors },
};
🤖 Prompt for AI Agents
In src/livecodes/languages/svelte/lang-svelte-compiler.ts around lines 62 to 65,
the error return currently only sets info: { errors } which breaks the expected
info shape; update the error path to include the existing importedContent and
imports properties alongside errors (e.g., info: { errors, importedContent,
imports }) so callers always receive a consistent info object.

}

const compiledCode = filename === MAIN_FILE ? getMountCode(js.code) : js.code;
return {
code:
language === 'svelte-app' ? `<script type="module">${compiledCode}</script>` : compiledCode,
info: { importedContent, imports },
info: { importedContent, imports, errors },
};
};

Expand Down
53 changes: 33 additions & 20 deletions src/livecodes/languages/vue/lang-vue-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { getRandomString, replaceAsync } from '../../utils/utils';
import { getErrorMessage, getRandomString, replaceAsync } from '../../utils/utils';
import { getLanguageByAlias } from '../utils';

// based on:
Expand Down Expand Up @@ -363,35 +363,48 @@ import { getLanguageByAlias } from '../utils';
}

return async (code, { config, language }) => {
const isMainFile = config.markup.language !== 'vue-app' || language === 'vue-app';
const filename = isMainFile ? MAIN_FILE : SECONDARY_FILE;
const result = await compileVueSFC(code, { config, filename });
try {
const isMainFile = config.markup.language !== 'vue-app' || language === 'vue-app';
const filename = isMainFile ? MAIN_FILE : SECONDARY_FILE;
const result = await compileVueSFC(code, { config, filename });

if (result) {
const { css, js } = result;
if (result) {
const { css, js } = result;

const injectCSS = !css.trim()
? ''
: `
const injectCSS = !css.trim()
? ''
: `
document.head.insertBefore(
Object.assign(document.createElement('style'), { textContent: ${JSON.stringify(css)} }),
document.head.getElementsByTagName('style')[0]
);
`;
const compiledCode = js + injectCSS;
const compiledCode = js + injectCSS;

return {
code:
language === 'vue-app'
? `<script type="module">${compiledCode}</script>`
: compiledCode,
info: { importedContent, imports: createImportMap(importedContent, config), errors },
};
}
Comment on lines +384 to +391
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

errors contains non-strings; violates CompileInfo contract and breaks UI rendering.

errors gathers objects from Vue compiler (parse/style/template). Map to strings before returning.

Apply this diff:

-        return {
-          code:
-            language === 'vue-app'
-              ? `<script type="module">${compiledCode}</script>`
-              : compiledCode,
-          info: { importedContent, imports: createImportMap(importedContent, config), errors },
-        };
+        const errorMessages = errors.map(getErrorMessage).filter(Boolean);
+        return {
+          code:
+            language === 'vue-app'
+              ? `<script type="module">${compiledCode}</script>`
+              : compiledCode,
+          info: {
+            importedContent,
+            imports: createImportMap(importedContent, config),
+            errors: errorMessages,
+          },
+        };

Additionally, change the declaration to enforce string accumulation (outside this hunk):

// before
let errors: any[] = [];
// after
let errors: string[] = [];

And when pushing compiler errors elsewhere in this file, convert to messages:

// parse errors
errors.push(...err.map(getErrorMessage));
// style errors
errors.push(...styleResult.errors.map(getErrorMessage));
// template errors
errors.push(...templateResult.errors.map(getErrorMessage));
🤖 Prompt for AI Agents
In src/livecodes/languages/vue/lang-vue-compiler.ts around lines 384 to 391, the
CompileInfo.errors array currently contains non-string objects from the Vue
compiler which breaks UI rendering; change the errors declaration to let errors:
string[] = []; then ensure every place pushing compiler errors converts them to
strings (e.g., push mapped messages using a getErrorMessage helper) — for
example replace pushes of raw error objects with
errors.push(...err.map(getErrorMessage)),
errors.push(...styleResult.errors.map(getErrorMessage)), and
errors.push(...templateResult.errors.map(getErrorMessage)); finally return the
CompileInfo with the errors array of strings so the UI contract is respected.


if (errors.length) {
// eslint-disable-next-line no-console
console.error(...errors);
}

const empty = `export default () => {}`;
return {
code:
language === 'vue-app' ? `<script type="module">${compiledCode}</script>` : compiledCode,
info: { importedContent, imports: createImportMap(importedContent, config) },
code: language === 'vue-app' ? `<script type="module">${empty}</script>` : empty,
info: { errors },
};
} catch (err) {
return {
code: '',
info: { errors: [...errors, getErrorMessage(err)] },
};
Comment on lines +398 to 407
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Normalize errors in fallback/catch paths.

Ensure strings only; include prior errors.

Apply this diff:

-      const empty = `export default () => {}`;
-      return {
-        code: language === 'vue-app' ? `<script type="module">${empty}</script>` : empty,
-        info: { errors },
-      };
+      const empty = `export default () => {}`;
+      return {
+        code: language === 'vue-app' ? `<script type="module">${empty}</script>` : empty,
+        info: { errors: errors.map(getErrorMessage).filter(Boolean) },
+      };
     } catch (err) {
-      return {
-        code: '',
-        info: { errors: [...errors, getErrorMessage(err)] },
-      };
+      return {
+        code: '',
+        info: {
+          errors: [...errors.map(getErrorMessage), getErrorMessage(err)].filter(Boolean),
+        },
+      };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const empty = `export default () => {}`;
return {
code:
language === 'vue-app' ? `<script type="module">${compiledCode}</script>` : compiledCode,
info: { importedContent, imports: createImportMap(importedContent, config) },
code: language === 'vue-app' ? `<script type="module">${empty}</script>` : empty,
info: { errors },
};
} catch (err) {
return {
code: '',
info: { errors: [...errors, getErrorMessage(err)] },
};
const empty = `export default () => {}`;
return {
code: language === 'vue-app' ? `<script type="module">${empty}</script>` : empty,
info: { errors: errors.map(getErrorMessage).filter(Boolean) },
};
} catch (err) {
return {
code: '',
info: {
errors: [...errors.map(getErrorMessage), getErrorMessage(err)].filter(Boolean),
},
};
🤖 Prompt for AI Agents
In src/livecodes/languages/vue/lang-vue-compiler.ts around lines 398 to 407, the
catch/fallback returns an info.errors array that can contain non-string values
and may not include prior errors consistently; normalize all errors to strings
and include previous errors. Update both the successful-fallback and catch
branches so info.errors is an array of strings by mapping existing errors
through String or getErrorMessage (e.g., const normalized = errors.map(e =>
typeof e === 'string' ? e : getErrorMessage(e))), then return info: { errors:
[...normalized, getErrorMessage(err)] } in the catch, and in the other fallback
return info: { errors: normalized } (for vue-app keep the same code value).
Ensure no non-string values are left in info.errors.

}

if (errors.length) {
// eslint-disable-next-line no-console
console.error(...errors);
}

return '';
};
};
9 changes: 9 additions & 0 deletions src/livecodes/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,15 @@ export const compareObjects = /* @__PURE__ */ (
return diff;
};

export const getErrorMessage = /* @__PURE__ */ (err: unknown): string => {
if (err == null) return '';
if (err instanceof Error) return err.message;
if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') {
return err.message;
}
return String(err);
};

export const predefinedValues = {
APP_VERSION: process.env.VERSION || '',
SDK_VERSION: process.env.SDK_VERSION || '',
Expand Down
1 change: 1 addition & 0 deletions src/sdk/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,7 @@ export interface CompileInfo {
modifiedHTML?: string;
importedContent?: string;
imports?: Record<string, string>;
errors?: string[];
}

export interface CompileResult {
Expand Down