diff --git a/.changeset/itchy-pumas-wonder.md b/.changeset/itchy-pumas-wonder.md new file mode 100644 index 000000000000..70a3775c1224 --- /dev/null +++ b/.changeset/itchy-pumas-wonder.md @@ -0,0 +1,6 @@ +--- +"@typescript/twoslash": patch +"@typescript/sandbox": patch +--- + +Support twoslash directives for `TsConfigOnlyOption` and its list diff --git a/.changeset/purple-goats-cough.md b/.changeset/purple-goats-cough.md new file mode 100644 index 000000000000..09ed4db200d5 --- /dev/null +++ b/.changeset/purple-goats-cough.md @@ -0,0 +1,5 @@ +--- +"@typescript/ata": patch +--- + +Internal typing improvements diff --git a/packages/ata/src/index.ts b/packages/ata/src/index.ts index a9755734646c..35ca0ee6b671 100644 --- a/packages/ata/src/index.ts +++ b/packages/ata/src/index.ts @@ -162,8 +162,8 @@ export const getReferencesForModule = (ts: typeof import("typescript"), code: st const meta = ts.preProcessFile(code) // Ensure we don't try download TypeScript lib references - // @ts-ignore - private but likely to never change - const libMap: Map = ts.libMap || new Map() + // private but likely to never change + const libMap = ts.libMap || new Map() // TODO: strip /// ? diff --git a/packages/ata/tsconfig.json b/packages/ata/tsconfig.json index e228c47e9e33..03adbb9e4984 100644 --- a/packages/ata/tsconfig.json +++ b/packages/ata/tsconfig.json @@ -1,4 +1,5 @@ { + "include": ["../ts-internals"], "compilerOptions": { "module": "ESNext", "moduleResolution": "node", diff --git a/packages/playground/src/createConfigDropdown.ts b/packages/playground/src/createConfigDropdown.ts index 030edff787e2..4b915735abb4 100644 --- a/packages/playground/src/createConfigDropdown.ts +++ b/packages/playground/src/createConfigDropdown.ts @@ -1,3 +1,5 @@ +import { CommandLineOption, CommandLineOptionOfBooleanType } from "typescript" + type Sandbox = import("@typescript/sandbox").Sandbox type Monaco = typeof import("monaco-editor") @@ -12,14 +14,6 @@ type OptionsSummary = { // This is where all the localized descriptions come from declare const optionsSummary: OptionsSummary[] -type CompilerOptStub = { - name: string - type: string - isCommandLineOnly: boolean - category: any - description: any -} - const notCompilerOptions = ["Project_Files_0", "Watch_Options_999", "Command_line_Options_6171"] const notRelevantToPlayground = [ "listFiles", @@ -56,15 +50,15 @@ export const createConfigDropdown = (sandbox: Sandbox, monaco: Monaco) => { container.id = "boolean-options-container" configContainer.appendChild(container) - // @ts-ignore - const allOptions: CompilerOptStub[] = sandbox.ts.optionDeclarations + const allOptions = sandbox.ts.optionDeclarations const boolOptions = allOptions.filter( - k => + (k): k is CommandLineOptionOfBooleanType & Required> => !notRelevantToPlayground.includes(k.name) && k.type === "boolean" && !k.isCommandLineOnly && - k.category && + !!k.category && + !!k.description && !notCompilerOptions.includes(k.category.key) ) diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json index 885a5495a406..a3d961f9360e 100644 --- a/packages/playground/tsconfig.json +++ b/packages/playground/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src"], + "include": ["src", "../ts-internals"], "compilerOptions": { "outDir": "../typescriptlang-org/static/js/playground", "jsx": "react", @@ -13,6 +13,6 @@ "strict": true, "moduleResolution": "node", "esModuleInterop": true, - "skipLibCheck": true, + "skipLibCheck": true } } diff --git a/packages/sandbox/src/compilerOptions.ts b/packages/sandbox/src/compilerOptions.ts index 7d319c0009d1..54147812d806 100644 --- a/packages/sandbox/src/compilerOptions.ts +++ b/packages/sandbox/src/compilerOptions.ts @@ -58,6 +58,7 @@ export function getDefaultSandboxCompilerOptions( target: monaco.languages.typescript.ScriptTarget.ES2017, jsx: monaco.languages.typescript.JsxEmit.React, module: monaco.languages.typescript.ModuleKind.ESNext, + baseUrl: "file:///", } if (major >= 5) { @@ -94,7 +95,6 @@ export const getCompilerOptionsFromParams = ( if (toSet !== undefined) returnedOptions[key] = toSet } else { // If that doesn't work, double check that the flag exists and allow it through - // @ts-ignore const flagExists = ts.optionDeclarations.find(opt => opt.name === key) if (flagExists) { let realValue: number | boolean = true diff --git a/packages/sandbox/src/twoslashSupport.ts b/packages/sandbox/src/twoslashSupport.ts index 0a65beec4277..84225af75a17 100644 --- a/packages/sandbox/src/twoslashSupport.ts +++ b/packages/sandbox/src/twoslashSupport.ts @@ -4,7 +4,8 @@ const booleanConfigRegexp = /^\/\/\s?@(\w+)$/ const valuedConfigRegexp = /^\/\/\s?@(\w+):\s?(.+)$/ type TS = typeof import("typescript") -type CompilerOptions = import("typescript").CompilerOptions +type CompilerOptions = import("monaco-editor").languages.typescript.CompilerOptions +type CommandLineOption = import("typescript").CommandLineOption /** * This is a port of the twoslash bit which grabs compiler options @@ -12,12 +13,11 @@ type CompilerOptions = import("typescript").CompilerOptions */ export const extractTwoSlashCompilerOptions = (ts: TS) => { - let optMap = new Map() + const optMap = new Map() if (!("optionDeclarations" in ts)) { console.error("Could not get compiler options from ts.optionDeclarations - skipping twoslash support.") } else { - // @ts-ignore - optionDeclarations is not public API for (const opt of ts.optionDeclarations) { optMap.set(opt.name.toLowerCase(), opt) } @@ -25,7 +25,7 @@ export const extractTwoSlashCompilerOptions = (ts: TS) => { return (code: string) => { const codeLines = code.split("\n") - const options = {} as any + const options: CompilerOptions = {} codeLines.forEach(_line => { let match @@ -45,7 +45,7 @@ export const extractTwoSlashCompilerOptions = (ts: TS) => { } } -function setOption(name: string, value: string, opts: CompilerOptions, optMap: Map) { +function setOption(name: string, value: string, opts: CompilerOptions, optMap: Map) { const opt = optMap.get(name.toLowerCase()) if (!opt) return @@ -57,27 +57,50 @@ function setOption(name: string, value: string, opts: CompilerOptions, optMap: M break case "list": - const elementType = opt.element!.type + case "listOrElement": + const elementType = opt.element.type const strings = value.split(",") - if (typeof elementType === "string") { - opts[opt.name] = strings.map(v => parsePrimitive(v, elementType)) - } else { - opts[opt.name] = strings.map(v => getOptionValueFromMap(opt.name, v, elementType as Map)!).filter(Boolean) + switch (elementType) { + case "string": + case "number": + opts[opt.name] = strings.map(v => parsePrimitive(v, elementType)) + break + case "object": + opts[opt.name] = strings.map(v => parseObject(v, opt.name)) + break + case "boolean": + console.log(`List of ${elementType} is not yet supported.`) + break + default: + opts[opt.name] = strings + .map(v => getOptionValueFromMap(opt.name, v, elementType)) + .filter(v => v !== undefined) } break - default: // It's a map! - const optMap = opt.type as Map + case "object": + opts[opt.name] = parseObject(value, opt.name) + break + default: // It's a map! + const optMap = opt.type opts[opt.name] = getOptionValueFromMap(opt.name, value, optMap) } - if (opts[opt.name] === undefined) { - const keys = Array.from(opt.type.keys() as any) + if (opts[opt.name] === undefined && opt.type instanceof Map) { + const keys = Array.from(opt.type.keys()) console.log(`Invalid value ${value} for ${opt.name}. Allowed values: ${keys.join(",")}`) } } -export function parsePrimitive(value: string, type: string): any { +export function parsePrimitive( + value: string, + type: T +): { + number: number + string: string + boolean: boolean + [type: string]: number | string | boolean | undefined +}[T] { switch (type) { case "number": return +value @@ -89,11 +112,18 @@ export function parsePrimitive(value: string, type: string): any { console.log(`Unknown primitive type ${type} with - ${value}`) } +function parseObject(value: string, name: string) { + try { + return JSON.parse(value) + } catch { + console.log(`Invalid JSON value ${value} for ${name}.`) + } +} -function getOptionValueFromMap(name: string, key: string, optMap: Map) { +function getOptionValueFromMap(name: string, key: string, optMap: Map) { const result = optMap.get(key.toLowerCase()) if (result === undefined) { - const keys = Array.from(optMap.keys() as any) + const keys = Array.from(optMap.keys()) console.error( `Invalid inline compiler value`, @@ -161,7 +191,6 @@ export const twoslashCompletions = (ts: TS, monaco: typeof import("monaco-editor "noErrorValidation", "filename", ] - // @ts-ignore - ts.optionDeclarations is private const optsNames = ts.optionDeclarations.map(o => o.name) knowns.concat(optsNames).forEach(name => { if (name.startsWith(word.slice(1))) { diff --git a/packages/sandbox/tsconfig.json b/packages/sandbox/tsconfig.json index 2612cde3e8a0..a8b6ebefa484 100644 --- a/packages/sandbox/tsconfig.json +++ b/packages/sandbox/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src", "src/vendor/lzstring.min.js"], + "include": ["src", "src/vendor/lzstring.min.js", "../ts-internals"], "compilerOptions": { "outDir": "../typescriptlang-org/static/js/sandbox", diff --git a/packages/ts-internals/index.d.ts b/packages/ts-internals/index.d.ts new file mode 100644 index 000000000000..0edc6f4b7737 --- /dev/null +++ b/packages/ts-internals/index.d.ts @@ -0,0 +1,127 @@ +import "typescript" + +// These are all copy-pasted from https://github.com/microsoft/TypeScript/tree/be8678315541e814da14316848a9468e8f90ab11 +declare module "typescript" { + /** @internal */ + const optionDeclarations: CommandLineOption[] + /** @internal */ + const optionsForWatch: CommandLineOption[] + /** @internal */ + export const commonOptionsWithBuild: CommandLineOption[] + /** @internal */ + export const buildOpts: CommandLineOption[] + /** @internal */ + const typeAcquisitionDeclarations: CommandLineOption[] + /** @internal */ + const defaultInitCompilerOptions: CompilerOptions + /** + * A map of lib names to lib files. This map is used both for parsing the "lib" command line + * option as well as for resolving lib reference directives. + * + * @internal + */ + export const libMap: Map + + /** @internal */ + export interface OptionsNameMap { + optionsNameMap: Map + shortOptionNames: Map + } + + // prettier-ignore + /** @internal */ + export interface CommandLineOptionBase { + name: string; + type: "string" | "number" | "boolean" | "object" | "list" | "listOrElement" | Map; // a value of a primitive type, or an object literal mapping named values to actual values + isFilePath?: boolean; // True if option value is a path or fileName + shortName?: string; // A short mnemonic for convenience - for instance, 'h' can be used in place of 'help' + description?: DiagnosticMessage; // The message describing what the command line switch does. + defaultValueDescription?: string | number | boolean | DiagnosticMessage | undefined; // The message describing what the dafault value is. string type is prepared for fixed chosen like "false" which do not need I18n. + paramType?: DiagnosticMessage; // The name to be used for a non-boolean option's parameter + isTSConfigOnly?: boolean; // True if option can only be specified via tsconfig.json file + isCommandLineOnly?: boolean; + showInSimplifiedHelpView?: boolean; + category?: DiagnosticMessage; + strictFlag?: true; // true if the option is one of the flag under strict + allowJsFlag?: true; + affectsSourceFile?: true; // true if we should recreate SourceFiles after this option changes + affectsModuleResolution?: true; // currently same effect as `affectsSourceFile` + affectsBindDiagnostics?: true; // true if this affects binding (currently same effect as `affectsSourceFile`) + affectsSemanticDiagnostics?: true; // true if option affects semantic diagnostics + affectsEmit?: true; // true if the options affects emit + affectsProgramStructure?: true; // true if program should be reconstructed from root files if option changes and does not affect module resolution as affectsModuleResolution indirectly means program needs to reconstructed + affectsDeclarationPath?: true; // true if the options affects declaration file path computed + affectsBuildInfo?: true; // true if this options should be emitted in buildInfo + transpileOptionValue?: boolean | undefined; // If set this means that the option should be set to this value when transpiling + extraValidation?: (value: CompilerOptionsValue) => [DiagnosticMessage, ...string[]] | undefined; // Additional validation to be performed for the value to be valid + disallowNullOrUndefined?: true; // If set option does not allow setting null + allowConfigDirTemplateSubstitution?: true; // If set option allows substitution of `${configDir}` in the value +} + + /** @internal */ + export interface CommandLineOptionOfStringType extends CommandLineOptionBase { + type: "string" + defaultValueDescription?: string | undefined | DiagnosticMessage + } + + /** @internal */ + export interface CommandLineOptionOfNumberType extends CommandLineOptionBase { + type: "number" + defaultValueDescription: number | undefined | DiagnosticMessage + } + + /** @internal */ + export interface CommandLineOptionOfBooleanType extends CommandLineOptionBase { + type: "boolean" + defaultValueDescription: boolean | undefined | DiagnosticMessage + } + + /** @internal */ + export interface CommandLineOptionOfCustomType extends CommandLineOptionBase { + type: Map // an object literal mapping named values to actual values + defaultValueDescription: number | string | undefined | DiagnosticMessage + deprecatedKeys?: Set + } + + /** @internal */ + export interface AlternateModeDiagnostics { + diagnostic: DiagnosticMessage + getOptionsNameMap: () => OptionsNameMap + } + + /** @internal */ + export interface DidYouMeanOptionsDiagnostics { + alternateMode?: AlternateModeDiagnostics + optionDeclarations: CommandLineOption[] + unknownOptionDiagnostic: DiagnosticMessage + unknownDidYouMeanDiagnostic: DiagnosticMessage + } + + /** @internal */ + export interface TsConfigOnlyOption extends CommandLineOptionBase { + type: "object" + elementOptions?: Map + extraKeyDiagnostics?: DidYouMeanOptionsDiagnostics + } + + /** @internal */ + export interface CommandLineOptionOfListType extends CommandLineOptionBase { + type: "list" | "listOrElement" + element: + | CommandLineOptionOfCustomType + | CommandLineOptionOfStringType + | CommandLineOptionOfNumberType + | CommandLineOptionOfBooleanType + | TsConfigOnlyOption + listPreserveFalsyValues?: boolean + } + + /** @internal */ + export type CommandLineOption = + | CommandLineOptionOfCustomType + | CommandLineOptionOfStringType + | CommandLineOptionOfNumberType + | CommandLineOptionOfBooleanType + | TsConfigOnlyOption + | CommandLineOptionOfListType +} diff --git a/packages/ts-internals/package.json b/packages/ts-internals/package.json new file mode 100644 index 000000000000..68fd790a6e57 --- /dev/null +++ b/packages/ts-internals/package.json @@ -0,0 +1,12 @@ +{ + "name": "ts-internals", + "private": true, + "license": "MIT", + "version": "1.0.0", + "scripts": { + "build": "tsc --build ." + }, + "devDependencies": { + "typescript": "*" + } +} diff --git a/packages/ts-internals/tsconfig.json b/packages/ts-internals/tsconfig.json new file mode 100644 index 000000000000..37a4aee8ea57 --- /dev/null +++ b/packages/ts-internals/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "target": "ES5", + "strict": true, + "skipLibCheck": false, + "noEmit": true + } +} diff --git a/packages/ts-twoslasher/src/index.ts b/packages/ts-twoslasher/src/index.ts index 0864bf3de185..7c9f5ac7d0f9 100755 --- a/packages/ts-twoslasher/src/index.ts +++ b/packages/ts-twoslasher/src/index.ts @@ -10,24 +10,20 @@ type TS = typeof import("typescript") type CompilerOptions = import("typescript").CompilerOptions type CustomTransformers = import("typescript").CustomTransformers -import { parsePrimitive, cleanMarkdownEscaped, typesToExtension, getIdentifierTextSpans, getClosestWord } from "./utils" +import { + parsePrimitive, + parseObject, + cleanMarkdownEscaped, + typesToExtension, + getIdentifierTextSpans, + getClosestWord, +} from "./utils" import { validateInput, validateCodeForErrors } from "./validation" import { createSystem, createVirtualTypeScriptEnvironment, createFSBackedSystem } from "@typescript/vfs" const log = shouldDebug ? console.log : (_message?: any, ..._optionalParams: any[]) => "" -// Hacking in some internal stuff -declare module "typescript" { - type Option = { - name: string - type: "list" | "boolean" | "number" | "string" | Map - element?: Option - } - - const optionDeclarations: Array