mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): add tsconfig support to editor and use it in ts compiler (#66259)
This commit is contained in:
committed by
GitHub
parent
c9071dd6a9
commit
9356588e80
@@ -232,7 +232,8 @@ const modeMap = {
|
|||||||
ts: 'typescript',
|
ts: 'typescript',
|
||||||
tsx: 'typescript',
|
tsx: 'typescript',
|
||||||
py: 'python',
|
py: 'python',
|
||||||
python: 'python'
|
python: 'python',
|
||||||
|
json: 'json'
|
||||||
};
|
};
|
||||||
|
|
||||||
let monacoThemesDefined = false;
|
let monacoThemesDefined = false;
|
||||||
@@ -413,6 +414,11 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
allowUmdGlobalAccess: true
|
allowUmdGlobalAccess: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// support JSONC:
|
||||||
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
|
allowComments: 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
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type VisibleEditors = {
|
|||||||
indexts?: boolean;
|
indexts?: boolean;
|
||||||
indextsx?: boolean;
|
indextsx?: boolean;
|
||||||
mainpy?: boolean;
|
mainpy?: boolean;
|
||||||
|
tsconfigjson?: boolean;
|
||||||
};
|
};
|
||||||
type MultifileEditorProps = Pick<
|
type MultifileEditorProps = Pick<
|
||||||
EditorProps,
|
EditorProps,
|
||||||
@@ -72,7 +73,8 @@ const MultifileEditor = (props: MultifileEditorProps) => {
|
|||||||
indexts,
|
indexts,
|
||||||
indexjsx,
|
indexjsx,
|
||||||
indextsx,
|
indextsx,
|
||||||
mainpy
|
mainpy,
|
||||||
|
tsconfigjson
|
||||||
},
|
},
|
||||||
usesMultifileEditor,
|
usesMultifileEditor,
|
||||||
showProjectPreview,
|
showProjectPreview,
|
||||||
@@ -102,6 +104,7 @@ const MultifileEditor = (props: MultifileEditorProps) => {
|
|||||||
if (scriptjs) editorKeys.push('scriptjs');
|
if (scriptjs) editorKeys.push('scriptjs');
|
||||||
if (mainpy) editorKeys.push('mainpy');
|
if (mainpy) editorKeys.push('mainpy');
|
||||||
if (indexts) editorKeys.push('indexts');
|
if (indexts) editorKeys.push('indexts');
|
||||||
|
if (tsconfigjson) editorKeys.push('tsconfigjson');
|
||||||
|
|
||||||
const editorAndSplitterKeys = editorKeys.reduce((acc: string[] | [], key) => {
|
const editorAndSplitterKeys = editorKeys.reduce((acc: string[] | [], key) => {
|
||||||
if (acc.length === 0) {
|
if (acc.length === 0) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChallengeFile } from "../../src/redux/prop-types";
|
import { ChallengeFile } from '../../src/redux/prop-types';
|
||||||
|
|
||||||
export const challengeFiles: ChallengeFile[] = [
|
export const challengeFiles: ChallengeFile[] = [
|
||||||
{
|
{
|
||||||
@@ -13,7 +13,7 @@ export const challengeFiles: ChallengeFile[] = [
|
|||||||
tail: '',
|
tail: '',
|
||||||
editableRegionBoundaries: [],
|
editableRegionBoundaries: [],
|
||||||
usesMultifileEditor: true,
|
usesMultifileEditor: true,
|
||||||
path: 'index.ts',
|
path: 'index.ts'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contents: 'some css',
|
contents: 'some css',
|
||||||
@@ -27,7 +27,7 @@ export const challengeFiles: ChallengeFile[] = [
|
|||||||
tail: '',
|
tail: '',
|
||||||
editableRegionBoundaries: [],
|
editableRegionBoundaries: [],
|
||||||
usesMultifileEditor: true,
|
usesMultifileEditor: true,
|
||||||
path: 'styles.css',
|
path: 'styles.css'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contents: 'some html',
|
contents: 'some html',
|
||||||
@@ -41,7 +41,7 @@ export const challengeFiles: ChallengeFile[] = [
|
|||||||
tail: '',
|
tail: '',
|
||||||
editableRegionBoundaries: [],
|
editableRegionBoundaries: [],
|
||||||
usesMultifileEditor: true,
|
usesMultifileEditor: true,
|
||||||
path: 'index.html',
|
path: 'index.html'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contents: 'some js',
|
contents: 'some js',
|
||||||
@@ -55,7 +55,7 @@ export const challengeFiles: ChallengeFile[] = [
|
|||||||
tail: '',
|
tail: '',
|
||||||
editableRegionBoundaries: [],
|
editableRegionBoundaries: [],
|
||||||
usesMultifileEditor: true,
|
usesMultifileEditor: true,
|
||||||
path: 'script.js',
|
path: 'script.js'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contents: 'some jsx',
|
contents: 'some jsx',
|
||||||
@@ -69,7 +69,7 @@ export const challengeFiles: ChallengeFile[] = [
|
|||||||
tail: '',
|
tail: '',
|
||||||
editableRegionBoundaries: [],
|
editableRegionBoundaries: [],
|
||||||
usesMultifileEditor: true,
|
usesMultifileEditor: true,
|
||||||
path: 'index.jsx',
|
path: 'index.jsx'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contents: 'some tsx',
|
contents: 'some tsx',
|
||||||
@@ -83,6 +83,20 @@ export const challengeFiles: ChallengeFile[] = [
|
|||||||
tail: '',
|
tail: '',
|
||||||
editableRegionBoundaries: [],
|
editableRegionBoundaries: [],
|
||||||
usesMultifileEditor: true,
|
usesMultifileEditor: true,
|
||||||
path: 'index.tsx',
|
path: 'index.tsx'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contents: '{\n "compilerOptions": {}\n}',
|
||||||
|
error: null,
|
||||||
|
ext: 'json',
|
||||||
|
head: '',
|
||||||
|
history: ['tsconfig.json'],
|
||||||
|
fileKey: 'tsconfigjson',
|
||||||
|
name: 'tsconfig',
|
||||||
|
seed: '{\n "compilerOptions": {}\n}',
|
||||||
|
tail: '',
|
||||||
|
editableRegionBoundaries: [],
|
||||||
|
usesMultifileEditor: true,
|
||||||
|
path: 'tsconfig.json'
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe('sort-files', () => {
|
|||||||
expect(sorted.length).toEqual(expected.length);
|
expect(sorted.length).toEqual(expected.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort the objects into jsx, tsx, html, css, js, ts order', () => {
|
it('should sort the objects into jsx, tsx, html, css, js, ts, tsconfig order', () => {
|
||||||
const sorted = sortChallengeFiles(challengeFiles);
|
const sorted = sortChallengeFiles(challengeFiles);
|
||||||
const sortedKeys = sorted.map(({ fileKey }) => fileKey);
|
const sortedKeys = sorted.map(({ fileKey }) => fileKey);
|
||||||
const expected = [
|
const expected = [
|
||||||
@@ -24,7 +24,8 @@ describe('sort-files', () => {
|
|||||||
'indexhtml',
|
'indexhtml',
|
||||||
'stylescss',
|
'stylescss',
|
||||||
'scriptjs',
|
'scriptjs',
|
||||||
'indexts'
|
'indexts',
|
||||||
|
'tsconfigjson'
|
||||||
];
|
];
|
||||||
expect(sortedKeys).toStrictEqual(expected);
|
expect(sortedKeys).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export function sortChallengeFiles<File extends { fileKey: string }>(
|
|||||||
if (b.fileKey === 'scriptjs') return 1;
|
if (b.fileKey === 'scriptjs') return 1;
|
||||||
if (a.fileKey === 'indexts') return -1;
|
if (a.fileKey === 'indexts') return -1;
|
||||||
if (b.fileKey === 'indexts') return 1;
|
if (b.fileKey === 'indexts') return 1;
|
||||||
|
if (a.fileKey === 'tsconfigjson') return -1;
|
||||||
|
if (b.fileKey === 'tsconfigjson') return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { getTSConfig } from './build';
|
||||||
|
import { ChallengeFile } from '@freecodecamp/shared/utils/polyvinyl';
|
||||||
|
|
||||||
|
describe('getTSConfig', () => {
|
||||||
|
it("should return the tsconfig file's contents if it exists", () => {
|
||||||
|
const compileOptions = 'any string is valid here';
|
||||||
|
const challengeFiles = [
|
||||||
|
{ name: 'index', ext: 'ts' },
|
||||||
|
{ name: 'tsconfig', ext: 'json', contents: compileOptions }
|
||||||
|
] as ChallengeFile[];
|
||||||
|
|
||||||
|
expect(getTSConfig(challengeFiles)).toEqual(compileOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if there is no tsconfig file', () => {
|
||||||
|
const challengeFiles = [
|
||||||
|
{ name: 'index', ext: 'ts' },
|
||||||
|
{ name: 'app', ext: 'ts' }
|
||||||
|
] as ChallengeFile[];
|
||||||
|
|
||||||
|
expect(getTSConfig(challengeFiles)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if there are multiple tsconfig.json files', () => {
|
||||||
|
const challengeFiles = [
|
||||||
|
{ name: 'index', ext: 'ts' },
|
||||||
|
{ name: 'tsconfig', ext: 'json' },
|
||||||
|
{ name: 'tsconfig', ext: 'json' }
|
||||||
|
] as ChallengeFile[];
|
||||||
|
|
||||||
|
expect(() => getTSConfig(challengeFiles)).toThrow(
|
||||||
|
'TypeScript challenge must include only one tsconfig.json file'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getPythonTransformers,
|
getPythonTransformers,
|
||||||
getMultifileJSXTransformers
|
getMultifileJSXTransformers
|
||||||
} from './transformers.js';
|
} from './transformers.js';
|
||||||
|
import { setupTSCompiler } from './typescript-worker-handler.js';
|
||||||
|
|
||||||
interface Source {
|
interface Source {
|
||||||
index: string;
|
index: string;
|
||||||
@@ -165,6 +166,39 @@ type BuildResult = {
|
|||||||
error?: unknown;
|
error?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function hasTS(challengeFiles: ChallengeFile[]) {
|
||||||
|
return challengeFiles.some(
|
||||||
|
challengeFile => challengeFile.ext === 'ts' || challengeFile.ext === 'tsx'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTSConfig = (f: { name: string; ext: string }) =>
|
||||||
|
f.name === 'tsconfig' && f.ext === 'json';
|
||||||
|
|
||||||
|
export function getTSConfig(challengeFiles: ChallengeFile[]) {
|
||||||
|
const tsConfigFiles = challengeFiles.filter(isTSConfig);
|
||||||
|
|
||||||
|
if (tsConfigFiles.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
'TypeScript challenge must include only one tsconfig.json file'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tsConfigFiles.length === 1 ? tsConfigFiles[0].contents : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configureTSCompiler(challengeFiles: ChallengeFile[]) {
|
||||||
|
if (hasTS(challengeFiles)) {
|
||||||
|
const tsConfig = getTSConfig(challengeFiles);
|
||||||
|
|
||||||
|
if (tsConfig) {
|
||||||
|
await setupTSCompiler(tsConfig);
|
||||||
|
} else {
|
||||||
|
await setupTSCompiler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: All the buildXChallenge files have a similar structure, so make that
|
// TODO: All the buildXChallenge files have a similar structure, so make that
|
||||||
// abstraction (function, class, whatever) and then create the various functions
|
// abstraction (function, class, whatever) and then create the various functions
|
||||||
// out of it.
|
// out of it.
|
||||||
@@ -182,12 +216,10 @@ async function buildDOMChallenge(
|
|||||||
const hasJsx = challengeFiles.some(
|
const hasJsx = challengeFiles.some(
|
||||||
challengeFile => challengeFile.ext === 'jsx' || challengeFile.ext === 'tsx'
|
challengeFile => challengeFile.ext === 'jsx' || challengeFile.ext === 'tsx'
|
||||||
);
|
);
|
||||||
const isMultifile = challengeFiles.length > 1;
|
|
||||||
|
|
||||||
const requiresReact16 = required.some(({ src }) =>
|
|
||||||
src?.includes('https://cdnjs.cloudflare.com/ajax/libs/react/16.')
|
|
||||||
);
|
|
||||||
|
|
||||||
|
await configureTSCompiler(challengeFiles);
|
||||||
|
const sourceFiles = challengeFiles.filter(file => !isTSConfig(file));
|
||||||
|
const isMultifile = sourceFiles.length > 1;
|
||||||
// I'm reasonably sure this is fine, but we need to migrate transformers to
|
// I'm reasonably sure this is fine, but we need to migrate transformers to
|
||||||
// TypeScript to be sure.
|
// TypeScript to be sure.
|
||||||
const transformers: ApplyFunctionProps[] = (isMultifile && hasJsx
|
const transformers: ApplyFunctionProps[] = (isMultifile && hasJsx
|
||||||
@@ -195,7 +227,7 @@ async function buildDOMChallenge(
|
|||||||
: getTransformers(options)) as unknown as ApplyFunctionProps[];
|
: getTransformers(options)) as unknown as ApplyFunctionProps[];
|
||||||
|
|
||||||
const pipeLine = composeFunctions(...transformers);
|
const pipeLine = composeFunctions(...transformers);
|
||||||
const finalFiles = await Promise.all(challengeFiles.map(pipeLine));
|
const finalFiles = await Promise.all(sourceFiles.map(pipeLine));
|
||||||
const error = finalFiles.find(({ error }) => error)?.error;
|
const error = finalFiles.find(({ error }) => error)?.error;
|
||||||
const contents = (await embedFilesInHtml(finalFiles)) as string;
|
const contents = (await embedFilesInHtml(finalFiles)) as string;
|
||||||
|
|
||||||
@@ -209,6 +241,10 @@ async function buildDOMChallenge(
|
|||||||
contents
|
contents
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requiresReact16 = required.some(({ src }) =>
|
||||||
|
src?.includes('https://cdnjs.cloudflare.com/ajax/libs/react/16.')
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challengeType,
|
challengeType,
|
||||||
build: concatHtml(toBuild),
|
build: concatHtml(toBuild),
|
||||||
@@ -230,7 +266,9 @@ async function buildJSChallenge(
|
|||||||
...(getTransformers(options) as unknown as ApplyFunctionProps[])
|
...(getTransformers(options) as unknown as ApplyFunctionProps[])
|
||||||
);
|
);
|
||||||
|
|
||||||
const finalFiles = await Promise.all(challengeFiles?.map(pipeLine));
|
await configureTSCompiler(challengeFiles);
|
||||||
|
const sourceFiles = challengeFiles.filter(file => !isTSConfig(file));
|
||||||
|
const finalFiles = await Promise.all(sourceFiles?.map(pipeLine));
|
||||||
const error = finalFiles.find(({ error }) => error)?.error;
|
const error = finalFiles.find(({ error }) => error)?.error;
|
||||||
|
|
||||||
const toBuild = error ? [] : finalFiles;
|
const toBuild = error ? [] : finalFiles;
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ import {
|
|||||||
import { version } from '@freecodecamp/browser-scripts/package.json';
|
import { version } from '@freecodecamp/browser-scripts/package.json';
|
||||||
|
|
||||||
import { WorkerExecutor } from './worker-executor';
|
import { WorkerExecutor } from './worker-executor';
|
||||||
import {
|
import { compileTypeScriptCode } from './typescript-worker-handler';
|
||||||
compileTypeScriptCode,
|
|
||||||
setupTSCompiler
|
|
||||||
} from './typescript-worker-handler';
|
|
||||||
|
|
||||||
const protectTimeout = 100;
|
const protectTimeout = 100;
|
||||||
const testProtectTimeout = 1500;
|
const testProtectTimeout = 1500;
|
||||||
@@ -148,7 +145,6 @@ const getJSXModuleTranspiler = loopProtectOptions => async challengeFile => {
|
|||||||
|
|
||||||
const getTSTranspiler = loopProtectOptions => async challengeFile => {
|
const getTSTranspiler = loopProtectOptions => async challengeFile => {
|
||||||
await loadBabel();
|
await loadBabel();
|
||||||
await setupTSCompiler();
|
|
||||||
const babelOptions = getBabelOptions(presetsJS, loopProtectOptions);
|
const babelOptions = getBabelOptions(presetsJS, loopProtectOptions);
|
||||||
return flow(
|
return flow(
|
||||||
partial(transformHeadTailAndContents, compileTypeScriptCode),
|
partial(transformHeadTailAndContents, compileTypeScriptCode),
|
||||||
@@ -159,7 +155,6 @@ const getTSTranspiler = loopProtectOptions => async challengeFile => {
|
|||||||
const getTSXModuleTranspiler = loopProtectOptions => async challengeFile => {
|
const getTSXModuleTranspiler = loopProtectOptions => async challengeFile => {
|
||||||
await loadBabel();
|
await loadBabel();
|
||||||
await loadPresetReact();
|
await loadPresetReact();
|
||||||
await setupTSCompiler();
|
|
||||||
const baseOptions = getBabelOptions(presetsJSX, loopProtectOptions);
|
const baseOptions = getBabelOptions(presetsJSX, loopProtectOptions);
|
||||||
const babelOptions = {
|
const babelOptions = {
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
@@ -379,7 +374,18 @@ function challengeFilesToObject(challengeFiles) {
|
|||||||
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');
|
||||||
const indexTsx = challengeFiles.find(file => file.fileKey === 'indextsx');
|
const indexTsx = challengeFiles.find(file => file.fileKey === 'indextsx');
|
||||||
return { indexHtml, indexJsx, stylesCss, scriptJs, indexTs, indexTsx };
|
const tsconfigJson = challengeFiles.find(
|
||||||
|
file => file.fileKey === 'tsconfigjson'
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
indexHtml,
|
||||||
|
indexJsx,
|
||||||
|
stylesCss,
|
||||||
|
scriptJs,
|
||||||
|
indexTs,
|
||||||
|
indexTsx,
|
||||||
|
tsconfigJson
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseAndTransform = async function (transform, contents) {
|
const parseAndTransform = async function (transform, contents) {
|
||||||
|
|||||||
@@ -31,12 +31,10 @@ export function compileTypeScriptCode(code: string): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupTSCompiler(
|
export function setupTSCompiler(tsconfig?: string): Promise<boolean> {
|
||||||
compilerOptions?: Record<string, unknown>
|
|
||||||
): Promise<boolean> {
|
|
||||||
return awaitResponse({
|
return awaitResponse({
|
||||||
messenger: getTypeScriptWorker(),
|
messenger: getTypeScriptWorker(),
|
||||||
message: { type: 'setup', ...(compilerOptions && { compilerOptions }) },
|
message: { type: 'setup', ...(tsconfig && { tsconfig }) },
|
||||||
onMessage: (data, onSuccess) => {
|
onMessage: (data, onSuccess) => {
|
||||||
if (data.type === 'ready') {
|
if (data.type === 'ready') {
|
||||||
onSuccess(true);
|
onSuccess(true);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const exts = ['js', 'html', 'css', 'jsx', 'ts', 'tsx', 'py'] as const;
|
const exts = ['js', 'html', 'css', 'jsx', 'ts', 'tsx', 'py', 'json'] as const;
|
||||||
export type Ext = (typeof exts)[number];
|
export type Ext = (typeof exts)[number];
|
||||||
|
|
||||||
export interface IncompleteChallengeFile {
|
export interface IncompleteChallengeFile {
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ body {
|
|||||||
var x = 'y';
|
var x = 'y';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# --solutions--
|
# --solutions--
|
||||||
|
|
||||||
|
|||||||
@@ -374,6 +374,19 @@ exports[`challenge parser > should parse a simple md file 1`] = `
|
|||||||
"name": "script",
|
"name": "script",
|
||||||
"tail": "",
|
"tail": "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"contents": "{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020"
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
"editableRegionBoundaries": [],
|
||||||
|
"ext": "json",
|
||||||
|
"head": "",
|
||||||
|
"id": "",
|
||||||
|
"name": "tsconfig",
|
||||||
|
"tail": "",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"description": "<section id="description">
|
"description": "<section id="description">
|
||||||
<p>Paragraph 1</p>
|
<p>Paragraph 1</p>
|
||||||
|
|||||||
@@ -35,6 +35,19 @@ exports[`add-seed plugin > should have an output to match the snapshot 1`] = `
|
|||||||
"name": "script",
|
"name": "script",
|
||||||
"tail": "",
|
"tail": "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"contents": "{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020"
|
||||||
|
}
|
||||||
|
}",
|
||||||
|
"editableRegionBoundaries": [],
|
||||||
|
"ext": "json",
|
||||||
|
"head": "",
|
||||||
|
"id": "",
|
||||||
|
"name": "tsconfig",
|
||||||
|
"tail": "",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -272,6 +272,19 @@ const Button = () => {
|
|||||||
};`);
|
};`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles json', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
plugin(simpleAST, file);
|
||||||
|
const {
|
||||||
|
data: { challengeFiles }
|
||||||
|
} = file;
|
||||||
|
const tsconfigjsonc = challengeFiles.find(x => x.ext === 'json');
|
||||||
|
|
||||||
|
expect(tsconfigjsonc.contents).toBe(
|
||||||
|
`{\n "compilerOptions": {\n "target": "ES2020"\n }\n}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw an error if a seed has no contents', () => {
|
it('should throw an error if a seed has no contents', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
expect(() => plugin(withEmptyContentsAST, file)).toThrow(
|
expect(() => plugin(withEmptyContentsAST, file)).toThrow(
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ const keyToSection = {
|
|||||||
head: 'before-user-code',
|
head: 'before-user-code',
|
||||||
tail: 'after-user-code'
|
tail: 'after-user-code'
|
||||||
};
|
};
|
||||||
const supportedLanguages = ['js', 'css', 'html', 'jsx', 'py', 'ts', 'tsx'];
|
const supportedLanguages = [
|
||||||
|
'js',
|
||||||
|
'css',
|
||||||
|
'html',
|
||||||
|
'jsx',
|
||||||
|
'py',
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'json'
|
||||||
|
];
|
||||||
const longToShortLanguages = {
|
const longToShortLanguages = {
|
||||||
javascript: 'js',
|
javascript: 'js',
|
||||||
typescript: 'ts',
|
typescript: 'ts',
|
||||||
@@ -30,7 +39,8 @@ function getFilenames(lang) {
|
|||||||
const langToFilename = {
|
const langToFilename = {
|
||||||
js: 'script',
|
js: 'script',
|
||||||
css: 'styles',
|
css: 'styles',
|
||||||
py: 'main'
|
py: 'main',
|
||||||
|
json: 'tsconfig'
|
||||||
};
|
};
|
||||||
return langToFilename[lang] ?? 'index';
|
return langToFilename[lang] ?? 'index';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,25 @@ export class Compiler {
|
|||||||
this.tsvfs = tsvfs;
|
this.tsvfs = tsvfs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setup(opts?: { useNodeModules?: boolean; compilerOptions?: unknown }) {
|
async setup(opts?: { useNodeModules?: boolean; tsconfig?: string }) {
|
||||||
const ts = this.ts;
|
const ts = this.ts;
|
||||||
const tsvfs = this.tsvfs;
|
const tsvfs = this.tsvfs;
|
||||||
|
|
||||||
const parsedOptions = ts.convertCompilerOptionsFromJson(
|
// This just parses the JSON, it doesn't do any validation.
|
||||||
opts?.compilerOptions ?? {},
|
const parsedOptions = opts?.tsconfig
|
||||||
'/'
|
? (ts.parseConfigFileTextToJson('', opts.tsconfig).config as {
|
||||||
|
compilerOptions?: unknown;
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// For now we're only interested in the compilerOptions, so that's all we're
|
||||||
|
// extracting and validating. For everything else, we could
|
||||||
|
// parseJsonConfigFileContent and create a host using createSystem and
|
||||||
|
// fsMap, but that needs compilerOptions... This is a bit of a chicken and
|
||||||
|
// egg problem, which we don't need to solve yet.
|
||||||
|
const validatedOptions = ts.convertCompilerOptionsFromJson(
|
||||||
|
parsedOptions?.compilerOptions ?? {},
|
||||||
|
'./'
|
||||||
);
|
);
|
||||||
|
|
||||||
const compilerOptions: CompilerOptions = {
|
const compilerOptions: CompilerOptions = {
|
||||||
@@ -34,7 +46,7 @@ export class Compiler {
|
|||||||
// 3.8.0-rc."
|
// 3.8.0-rc."
|
||||||
jsx: ts.JsxEmit.Preserve, // Babel will handle JSX,
|
jsx: ts.JsxEmit.Preserve, // Babel will handle JSX,
|
||||||
allowUmdGlobalAccess: true, // Necessary because React is loaded via a UMD script.
|
allowUmdGlobalAccess: true, // Necessary because React is loaded via a UMD script.
|
||||||
...parsedOptions.options
|
...validatedOptions.options
|
||||||
};
|
};
|
||||||
|
|
||||||
const fsMap = opts?.useNodeModules
|
const fsMap = opts?.useNodeModules
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { CompilerOptions } from 'typescript';
|
|
||||||
import { Compiler } from './modules/typescript-compiler';
|
import { Compiler } from './modules/typescript-compiler';
|
||||||
|
|
||||||
// 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
|
||||||
@@ -27,7 +26,7 @@ interface TSCompiledMessage {
|
|||||||
interface SetupEvent extends MessageEvent {
|
interface SetupEvent extends MessageEvent {
|
||||||
data: {
|
data: {
|
||||||
type: 'setup';
|
type: 'setup';
|
||||||
compilerOptions?: CompilerOptions;
|
tsconfig?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ function handleCancelRequest({ value }: { value: number }) {
|
|||||||
|
|
||||||
async function handleSetupRequest(data: SetupEvent['data'], port: MessagePort) {
|
async function handleSetupRequest(data: SetupEvent['data'], port: MessagePort) {
|
||||||
await compiler.setup({
|
await compiler.setup({
|
||||||
compilerOptions: data.compilerOptions
|
tsconfig: data.tsconfig
|
||||||
});
|
});
|
||||||
// We freeze this to prevent learners from getting the worker into a weird
|
// We freeze this to prevent learners from getting the worker into a weird
|
||||||
// state.
|
// state.
|
||||||
|
|||||||
Reference in New Issue
Block a user