mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): tsx compilation (#62236)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -54,6 +54,7 @@ const config = {
|
|||||||
'sql',
|
'sql',
|
||||||
'svg',
|
'svg',
|
||||||
'typescript',
|
'typescript',
|
||||||
|
'tsx',
|
||||||
'xml'
|
'xml'
|
||||||
],
|
],
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as ReactDOMServer from 'react-dom/server';
|
import * as ReactDOMServer from 'react-dom/server';
|
||||||
import Loadable from '@loadable/component';
|
import Loadable from '@loadable/component';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-duplicates
|
// eslint-disable-next-line import/no-duplicates
|
||||||
import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
|
import type * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import type {
|
import type {
|
||||||
@@ -68,10 +69,18 @@ import { getScrollbarWidth } from '../../../utils/scrollbar-width';
|
|||||||
import { isProjectBased } from '../../../utils/curriculum-layout';
|
import { isProjectBased } from '../../../utils/curriculum-layout';
|
||||||
import envConfig from '../../../../config/env.json';
|
import envConfig from '../../../../config/env.json';
|
||||||
import LowerJaw from './lower-jaw';
|
import LowerJaw from './lower-jaw';
|
||||||
|
// Direct from npm, license in react-types-licence
|
||||||
|
import reactTypes from './react-types.json';
|
||||||
|
|
||||||
import './editor.css';
|
import './editor.css';
|
||||||
|
|
||||||
const MonacoEditor = Loadable(() => import('react-monaco-editor'));
|
const MonacoEditor = Loadable(() => import('react-monaco-editor'));
|
||||||
|
|
||||||
|
const monacoModelFileMap = {
|
||||||
|
tsxFile: 'index.tsx',
|
||||||
|
reactTypes: 'react.d.ts'
|
||||||
|
};
|
||||||
|
|
||||||
export interface EditorProps {
|
export interface EditorProps {
|
||||||
attempts: number;
|
attempts: number;
|
||||||
canFocus: boolean;
|
canFocus: boolean;
|
||||||
@@ -353,15 +362,44 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
const { usesMultifileEditor = false } = props;
|
const { usesMultifileEditor = false } = props;
|
||||||
|
|
||||||
monacoRef.current = monaco;
|
monacoRef.current = monaco;
|
||||||
|
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||||
|
...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||||
|
jsx: monaco.languages.typescript.JsxEmit.Preserve,
|
||||||
|
allowUmdGlobalAccess: true
|
||||||
|
});
|
||||||
|
|
||||||
defineMonacoThemes(monaco, { usesMultifileEditor });
|
defineMonacoThemes(monaco, { usesMultifileEditor });
|
||||||
// If a model is not provided, then the editor 'owns' the model it creates
|
// If a model is not provided, then the editor 'owns' the model it creates
|
||||||
// and will dispose of that model if it is replaced. Since we intend to
|
// and will dispose of that model if it is replaced. Since we intend to
|
||||||
// swap and reuse models, we have to create our own models to prevent
|
// swap and reuse models, we have to create our own models to prevent
|
||||||
// disposal.
|
// disposal.
|
||||||
|
|
||||||
|
const setupTSModels = (monaco: typeof monacoEditor) => {
|
||||||
|
const reactFile = monaco.Uri.file(monacoModelFileMap.reactTypes);
|
||||||
|
monaco.editor.createModel(
|
||||||
|
reactTypes['react-18'],
|
||||||
|
'typescript',
|
||||||
|
reactFile
|
||||||
|
);
|
||||||
|
|
||||||
|
const file = monaco.Uri.file(monacoModelFileMap.tsxFile);
|
||||||
|
return monaco.editor.createModel('', 'typescript', file);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: make sure these aren't getting created over and over
|
||||||
|
function createModel(contents: string, language: string) {
|
||||||
|
if (language !== 'typescript') {
|
||||||
|
return monaco.editor.createModel(contents, language);
|
||||||
|
} else {
|
||||||
|
const model = setupTSModels(monaco);
|
||||||
|
model.setValue(contents);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const model =
|
const model =
|
||||||
dataRef.current.model ||
|
dataRef.current.model ||
|
||||||
monaco.editor.createModel(
|
createModel(
|
||||||
challengeFile?.contents ?? '',
|
challengeFile?.contents ?? '',
|
||||||
modeMap[challengeFile?.ext ?? 'html']
|
modeMap[challengeFile?.ext ?? 'html']
|
||||||
);
|
);
|
||||||
@@ -1328,6 +1366,15 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
editorDidMount={editorDidMount}
|
editorDidMount={editorDidMount}
|
||||||
editorWillMount={editorWillMount}
|
editorWillMount={editorWillMount}
|
||||||
|
editorWillUnmount={(editor, monaco) => {
|
||||||
|
const reactFile = monaco.Uri.file(monacoModelFileMap.reactTypes);
|
||||||
|
const file = monaco.Uri.file(monacoModelFileMap.tsxFile);
|
||||||
|
// Any model we've created has to be manually disposed of to prevent
|
||||||
|
// memory leaks.
|
||||||
|
editor.getModel()?.dispose();
|
||||||
|
monaco.editor.getModel(reactFile)?.dispose();
|
||||||
|
monaco.editor.getModel(file)?.dispose();
|
||||||
|
}}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={{ ...options, folding: !hasEditableRegion() }}
|
options={{ ...options, folding: !hasEditableRegion() }}
|
||||||
theme={editorTheme}
|
theme={editorTheme}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE
|
||||||
File diff suppressed because one or more lines are too long
@@ -96,13 +96,20 @@ const NBSPReg = new RegExp(String.fromCharCode(160), 'g');
|
|||||||
|
|
||||||
const testJS = matchesProperty('ext', 'js');
|
const testJS = matchesProperty('ext', 'js');
|
||||||
const testJSX = matchesProperty('ext', 'jsx');
|
const testJSX = matchesProperty('ext', 'jsx');
|
||||||
|
const testTSX = matchesProperty('ext', 'tsx');
|
||||||
const testTypeScript = matchesProperty('ext', 'ts');
|
const testTypeScript = matchesProperty('ext', 'ts');
|
||||||
const testHTML = matchesProperty('ext', 'html');
|
const testHTML = matchesProperty('ext', 'html');
|
||||||
const testHTML$JS$JSX$TS = overSome(testHTML, testJS, testJSX, testTypeScript);
|
const testHTML$JS$JSX$TS$TSX = overSome(
|
||||||
|
testHTML,
|
||||||
|
testJS,
|
||||||
|
testJSX,
|
||||||
|
testTypeScript,
|
||||||
|
testTSX
|
||||||
|
);
|
||||||
|
|
||||||
const replaceNBSP = cond([
|
const replaceNBSP = cond([
|
||||||
[
|
[
|
||||||
testHTML$JS$JSX$TS,
|
testHTML$JS$JSX$TS$TSX,
|
||||||
partial(transformContents, contents => contents.replace(NBSPReg, ' '))
|
partial(transformContents, contents => contents.replace(NBSPReg, ' '))
|
||||||
],
|
],
|
||||||
[stubTrue, identity]
|
[stubTrue, identity]
|
||||||
@@ -150,6 +157,22 @@ const getTSTranspiler = loopProtectOptions => async challengeFile => {
|
|||||||
)(challengeFile);
|
)(challengeFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTSXModuleTranspiler = loopProtectOptions => async challengeFile => {
|
||||||
|
await loadBabel();
|
||||||
|
await loadPresetReact();
|
||||||
|
await checkTSServiceIsReady();
|
||||||
|
const baseOptions = getBabelOptions(presetsJSX, loopProtectOptions);
|
||||||
|
const babelOptions = {
|
||||||
|
...baseOptions,
|
||||||
|
plugins: [...baseOptions.plugins, MODULE_TRANSFORM_PLUGIN],
|
||||||
|
moduleId: 'index' // TODO: this should be dynamic
|
||||||
|
};
|
||||||
|
return flow(
|
||||||
|
partial(transformHeadTailAndContents, compileTypeScriptCode),
|
||||||
|
partial(transformHeadTailAndContents, babelTransformCode(babelOptions))
|
||||||
|
)(challengeFile);
|
||||||
|
};
|
||||||
|
|
||||||
const createTranspiler = loopProtectOptions => {
|
const createTranspiler = loopProtectOptions => {
|
||||||
return cond([
|
return cond([
|
||||||
[testJS, getJSTranspiler(loopProtectOptions)],
|
[testJS, getJSTranspiler(loopProtectOptions)],
|
||||||
@@ -163,6 +186,7 @@ const createTranspiler = loopProtectOptions => {
|
|||||||
const createModuleTransformer = loopProtectOptions => {
|
const createModuleTransformer = loopProtectOptions => {
|
||||||
return cond([
|
return cond([
|
||||||
[testJSX, getJSXModuleTranspiler(loopProtectOptions)],
|
[testJSX, getJSXModuleTranspiler(loopProtectOptions)],
|
||||||
|
[testTSX, getTSXModuleTranspiler(loopProtectOptions)],
|
||||||
[testHTML, getHtmlTranspiler({ useModules: true })],
|
[testHTML, getHtmlTranspiler({ useModules: true })],
|
||||||
[stubTrue, identity]
|
[stubTrue, identity]
|
||||||
]);
|
]);
|
||||||
@@ -242,7 +266,7 @@ async function transformScript(documentElement, { useModules }) {
|
|||||||
// This does the final transformations of the files needed to embed them into
|
// This does the final transformations of the files needed to embed them into
|
||||||
// HTML.
|
// HTML.
|
||||||
export const embedFilesInHtml = async function (challengeFiles) {
|
export const embedFilesInHtml = async function (challengeFiles) {
|
||||||
const { indexHtml, stylesCss, scriptJs, indexJsx, indexTs } =
|
const { indexHtml, stylesCss, scriptJs, indexJsx, indexTs, indexTsx } =
|
||||||
challengeFilesToObject(challengeFiles);
|
challengeFilesToObject(challengeFiles);
|
||||||
|
|
||||||
const embedStylesAndScript = contentDocument => {
|
const embedStylesAndScript = contentDocument => {
|
||||||
@@ -266,6 +290,14 @@ export const embedFilesInHtml = async function (challengeFiles) {
|
|||||||
`script[data-plugins="${MODULE_TRANSFORM_PLUGIN}"][type="text/babel"][src="./index.jsx"]`
|
`script[data-plugins="${MODULE_TRANSFORM_PLUGIN}"][type="text/babel"][src="./index.jsx"]`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tsxScript =
|
||||||
|
documentElement.querySelector(
|
||||||
|
`script[data-plugins="${MODULE_TRANSFORM_PLUGIN}"][type="text/babel"][src="index.tsx"]`
|
||||||
|
) ??
|
||||||
|
documentElement.querySelector(
|
||||||
|
`script[data-plugins="${MODULE_TRANSFORM_PLUGIN}"][type="text/babel"][src="./index.tsx"]`
|
||||||
|
);
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
const style = contentDocument.createElement('style');
|
const style = contentDocument.createElement('style');
|
||||||
style.classList.add('fcc-injected-styles');
|
style.classList.add('fcc-injected-styles');
|
||||||
@@ -293,6 +325,13 @@ export const embedFilesInHtml = async function (challengeFiles) {
|
|||||||
jsxScript.setAttribute('data-src', 'index.jsx');
|
jsxScript.setAttribute('data-src', 'index.jsx');
|
||||||
jsxScript.setAttribute('data-type', 'text/babel');
|
jsxScript.setAttribute('data-type', 'text/babel');
|
||||||
}
|
}
|
||||||
|
if (tsxScript) {
|
||||||
|
tsxScript.innerHTML = indexTsx?.contents;
|
||||||
|
tsxScript.removeAttribute('src');
|
||||||
|
tsxScript.removeAttribute('type');
|
||||||
|
tsxScript.setAttribute('data-src', 'index.tsx');
|
||||||
|
tsxScript.setAttribute('data-type', 'text/babel');
|
||||||
|
}
|
||||||
return documentElement.innerHTML;
|
return documentElement.innerHTML;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -308,8 +347,10 @@ export const embedFilesInHtml = async function (challengeFiles) {
|
|||||||
return `<script>${scriptJs.contents}</script>`;
|
return `<script>${scriptJs.contents}</script>`;
|
||||||
} else if (indexTs) {
|
} else if (indexTs) {
|
||||||
return `<script>${indexTs.contents}</script>`;
|
return `<script>${indexTs.contents}</script>`;
|
||||||
|
} else if (indexTsx) {
|
||||||
|
return `<script>${indexTsx.contents}</script>`;
|
||||||
} else {
|
} else {
|
||||||
throw Error('No html, ts or js(x) file found');
|
throw Error('No html, ts(x) or js(x) file found');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -319,7 +360,8 @@ function challengeFilesToObject(challengeFiles) {
|
|||||||
const stylesCss = challengeFiles.find(file => file.fileKey === 'stylescss');
|
const stylesCss = challengeFiles.find(file => file.fileKey === 'stylescss');
|
||||||
const scriptJs = challengeFiles.find(file => file.fileKey === 'scriptjs');
|
const scriptJs = challengeFiles.find(file => file.fileKey === 'scriptjs');
|
||||||
const indexTs = challengeFiles.find(file => file.fileKey === 'indexts');
|
const indexTs = challengeFiles.find(file => file.fileKey === 'indexts');
|
||||||
return { indexHtml, indexJsx, stylesCss, scriptJs, indexTs };
|
const indexTsx = challengeFiles.find(file => file.fileKey === 'indextsx');
|
||||||
|
return { indexHtml, indexJsx, stylesCss, scriptJs, indexTs, indexTsx };
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseAndTransform = async function (transform, contents) {
|
const parseAndTransform = async function (transform, contents) {
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export async function buildDOMChallenge(
|
|||||||
// TODO: make this required in the schema.
|
// TODO: make this required in the schema.
|
||||||
if (!challengeFiles) throw Error('No challenge files provided');
|
if (!challengeFiles) throw Error('No challenge files provided');
|
||||||
const hasJsx = challengeFiles.some(
|
const hasJsx = challengeFiles.some(
|
||||||
challengeFile => challengeFile.ext === 'jsx'
|
challengeFile => challengeFile.ext === 'jsx' || challengeFile.ext === 'tsx'
|
||||||
);
|
);
|
||||||
const isMultifile = challengeFiles.length > 1;
|
const isMultifile = challengeFiles.length > 1;
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export function enhancePrismAccessibility(
|
|||||||
json: 'JSON',
|
json: 'JSON',
|
||||||
pug: 'pug',
|
pug: 'pug',
|
||||||
ts: 'TypeScript',
|
ts: 'TypeScript',
|
||||||
typescript: 'TypeScript'
|
typescript: 'TypeScript',
|
||||||
|
tsx: 'TSX'
|
||||||
};
|
};
|
||||||
const parent = prismEnv?.element?.parentElement;
|
const parent = prismEnv?.element?.parentElement;
|
||||||
if (
|
if (
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
import { type VirtualTypeScriptEnvironment } from '@typescript/vfs';
|
import { type VirtualTypeScriptEnvironment } from '@typescript/vfs';
|
||||||
import type { CompilerOptions, CompilerHost } from 'typescript';
|
import type { CompilerOptions, CompilerHost } from 'typescript';
|
||||||
|
import reactTypes from './react-types.json';
|
||||||
|
|
||||||
// Most of the ts types are only a guideline. This is because we're not bundling
|
// Most of the ts types are only a guideline. This is because we're not bundling
|
||||||
// TS in this worker. The specific TS version is going to be determined by the
|
// TS in this worker. The specific TS version is going to be determined by the
|
||||||
@@ -40,7 +41,7 @@ interface CancelEvent extends MessageEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pin at the latest TS version available as cdnjs doesn't support version range.
|
// Pin at the latest TS version available as cdnjs doesn't support version range.
|
||||||
const TS_VERSION = '5.7.3';
|
const TS_VERSION = '5.9.2';
|
||||||
|
|
||||||
let tsEnv: VirtualTypeScriptEnvironment | null = null;
|
let tsEnv: VirtualTypeScriptEnvironment | null = null;
|
||||||
let compilerHost: CompilerHost | null = null;
|
let compilerHost: CompilerHost | null = null;
|
||||||
@@ -54,7 +55,12 @@ importScripts(
|
|||||||
function importTS(version: string) {
|
function importTS(version: string) {
|
||||||
if (cachedVersion == version) return;
|
if (cachedVersion == version) return;
|
||||||
importScripts(
|
importScripts(
|
||||||
`https://cdnjs.cloudflare.com/ajax/libs/typescript/${version}/typescript.min.js`
|
/* typescript.min.js fails with
|
||||||
|
|
||||||
|
typescript.min.js:320 Uncaught TypeError: Class constructors cannot be invoked without 'new'
|
||||||
|
|
||||||
|
so we're using the non-minified version for now. */
|
||||||
|
`https://cdnjs.cloudflare.com/ajax/libs/typescript/${version}/typescript.js`
|
||||||
);
|
);
|
||||||
cachedVersion = version;
|
cachedVersion = version;
|
||||||
}
|
}
|
||||||
@@ -63,10 +69,13 @@ async function setupTypeScript() {
|
|||||||
importTS(TS_VERSION);
|
importTS(TS_VERSION);
|
||||||
const compilerOptions: CompilerOptions = {
|
const compilerOptions: CompilerOptions = {
|
||||||
target: ts.ScriptTarget.ES2015,
|
target: ts.ScriptTarget.ES2015,
|
||||||
skipLibCheck: true // TODO: look into why this is needed. Are we doing something wrong? Could it be that it's not "synced" with this TS version?
|
module: ts.ModuleKind.Preserve, // Babel is handling module transformation, so TS should leave them alone.
|
||||||
|
skipLibCheck: true, // TODO: look into why this is needed. Are we doing something wrong? Could it be that it's not "synced" with this TS version?
|
||||||
// from the docs: "Note: it's possible for this list to get out of
|
// from the docs: "Note: it's possible for this list to get out of
|
||||||
// sync with TypeScript over time. It was last synced with TypeScript
|
// sync with TypeScript over time. It was last synced with TypeScript
|
||||||
// 3.8.0-rc."
|
// 3.8.0-rc."
|
||||||
|
jsx: ts.JsxEmit.Preserve, // Babel will handle JSX,
|
||||||
|
allowUmdGlobalAccess: true // Necessary because React is loaded via a UMD script.
|
||||||
};
|
};
|
||||||
const fsMap = await createDefaultMapFromCDN(
|
const fsMap = await createDefaultMapFromCDN(
|
||||||
compilerOptions,
|
compilerOptions,
|
||||||
@@ -75,6 +84,12 @@ async function setupTypeScript() {
|
|||||||
ts
|
ts
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// This can be any path, but doing this means import React from 'react' works, if we ever need it.
|
||||||
|
const reactTypesPath = `/node_modules/@types/react/index.d.ts`;
|
||||||
|
|
||||||
|
// It may be necessary to get all the types (global.d.ts etc)
|
||||||
|
fsMap.set(reactTypesPath, reactTypes['react-18'] || '');
|
||||||
|
|
||||||
const system = tsvfs.createSystem(fsMap);
|
const system = tsvfs.createSystem(fsMap);
|
||||||
// TODO: if passed an invalid compiler options object (e.g. { module:
|
// TODO: if passed an invalid compiler options object (e.g. { module:
|
||||||
// ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.NodeNext
|
// ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.NodeNext
|
||||||
@@ -82,10 +97,11 @@ async function setupTypeScript() {
|
|||||||
// show them the diagnostics from this function.
|
// show them the diagnostics from this function.
|
||||||
const env = tsvfs.createVirtualTypeScriptEnvironment(
|
const env = tsvfs.createVirtualTypeScriptEnvironment(
|
||||||
system,
|
system,
|
||||||
[],
|
[reactTypesPath],
|
||||||
ts,
|
ts,
|
||||||
compilerOptions
|
compilerOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
compilerHost = createVirtualCompilerHost(
|
compilerHost = createVirtualCompilerHost(
|
||||||
system,
|
system,
|
||||||
compilerOptions,
|
compilerOptions,
|
||||||
@@ -133,11 +149,12 @@ function handleCompileRequest(data: TSCompileEvent['data'], port: MessagePort) {
|
|||||||
|
|
||||||
// TODO: If creating the file fresh each time is too slow, we can try checking
|
// TODO: If creating the file fresh each time is too slow, we can try checking
|
||||||
// if the file exists and updating it if it does.
|
// if the file exists and updating it if it does.
|
||||||
tsEnv?.createFile('index.ts', code);
|
// TODO: make sure the .tsx extension doesn't cause issues with vanilla TS.
|
||||||
|
tsEnv?.createFile('/index.tsx', code);
|
||||||
|
|
||||||
const program = tsEnv!.languageService.getProgram()!;
|
const program = tsEnv!.languageService.getProgram()!;
|
||||||
|
|
||||||
const emitOutput = tsEnv!.languageService.getEmitOutput('index.ts');
|
const emitOutput = tsEnv!.languageService.getEmitOutput('index.tsx');
|
||||||
const compiled = emitOutput.outputFiles[0].text;
|
const compiled = emitOutput.outputFiles[0].text;
|
||||||
|
|
||||||
const message: TSCompiledMessage = {
|
const message: TSCompiledMessage = {
|
||||||
|
|||||||
Reference in New Issue
Block a user