Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support importing json files as modules with commonjs or node module resolution #13665

Closed
wants to merge 11 commits into from
Closed
  •  
  •  
  •  
11 changes: 11 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ namespace ts {
return InternalSymbolName.ExportStar;
case SyntaxKind.ExportAssignment:
return (<ExportAssignment>node).isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default;
case SyntaxKind.SourceFile:
// json file should behave as
// module.exports = ...
return InternalSymbolName.ExportEquals;
case SyntaxKind.BinaryExpression:
if (getSpecialPropertyAssignmentKind(node as BinaryExpression) === SpecialPropertyAssignmentKind.ModuleExports) {
// module.exports = ...
Expand Down Expand Up @@ -2184,6 +2188,13 @@ namespace ts {
if (isExternalModule(file)) {
bindSourceFileAsExternalModule();
}
else if (isJsonSourceFile(file)) {
bindSourceFileAsExternalModule();
// Create symbol equivalent for the module.exports = {}
const originalFileSymbol = file.symbol;
declareSymbol(file.symbol.exports, file.symbol, file, SymbolFlags.Property, SymbolFlags.Property | SymbolFlags.AliasExcludes | SymbolFlags.Class | SymbolFlags.Function);
file.symbol = originalFileSymbol;
}
}

function bindSourceFileAsExternalModule() {
Expand Down
11 changes: 10 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4552,6 +4552,11 @@ namespace ts {
return links.type = anyType;
}
// Handle export default expressions
if (declaration.kind === SyntaxKind.SourceFile) {
// JSON file
const jsonSourceFile = <JsonSourceFile>declaration;
return links.type = jsonSourceFile.jsonObject ? checkObjectLiteral(jsonSourceFile.jsonObject) : emptyObjectType;
}
if (declaration.kind === SyntaxKind.ExportAssignment) {
return links.type = checkExpression((<ExportAssignment>declaration).expression);
}
Expand Down Expand Up @@ -13606,7 +13611,8 @@ namespace ts {
const contextualType = getApparentTypeOfContextualType(node);
const contextualTypeHasPattern = contextualType && contextualType.pattern &&
(contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression);
const isJSObjectLiteral = !contextualType && isInJavaScriptFile(node);
const isJSObjectLiteral = !contextualType && isInJavaScriptFile(node) &&
!(isSourceFile(node.parent) && isJsonSourceFile(node.parent) && node.parent.jsonObject === node);
let typeFlags: TypeFlags = 0;
let patternWithComputedProperties = false;
let hasComputedStringProperty = false;
Expand Down Expand Up @@ -22644,6 +22650,9 @@ namespace ts {
flowAnalysisDisabled = false;

forEach(node.statements, checkSourceElement);
if (isJsonSourceFile(node) && node.jsonObject) {
checkObjectLiteral(node.jsonObject);
}

checkDeferredNodes();

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1673,7 +1673,7 @@ namespace ts {
return undefined;
}
let extendedConfigPath = toPath(extendedConfig, basePath, getCanonicalFileName);
if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) {
if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, Extension.Json)) {
extendedConfigPath = `${extendedConfigPath}.json` as Path;
if (!host.fileExists(extendedConfigPath)) {
errors.push(createDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig));
Expand Down
8 changes: 4 additions & 4 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2221,7 +2221,7 @@ namespace ts {
return ScriptKind.TS;
case Extension.Tsx:
return ScriptKind.TSX;
case ".json":
case Extension.Json:
return ScriptKind.JSON;
default:
return ScriptKind.Unknown;
Expand Down Expand Up @@ -2316,9 +2316,9 @@ namespace ts {
}
}

const extensionsToRemove = [Extension.Dts, Extension.Ts, Extension.Js, Extension.Tsx, Extension.Jsx];
export const allSupportedExtensionForExtraction: ReadonlyArray<Extension> = [...supportedTypescriptExtensionsForExtractExtension, , ...supportedJavascriptExtensions, Extension.Json];
export function removeFileExtension(path: string): string {
for (const ext of extensionsToRemove) {
for (const ext of allSupportedExtensionForExtraction) {
const extensionless = tryRemoveExtension(path, ext);
if (extensionless !== undefined) {
return extensionless;
Expand Down Expand Up @@ -2614,7 +2614,7 @@ namespace ts {
}

export function tryGetExtensionFromPath(path: string): Extension | undefined {
return find<Extension>(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e));
return find(allSupportedExtensionForExtraction, e => fileExtensionIs(path, e));
}

export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ namespace ts {
return Extension.Jsx;
}
}
if (isJsonSourceFile(sourceFile)) {
return Extension.Json;
}
return Extension.Js;
}

Expand Down Expand Up @@ -2236,6 +2239,9 @@ namespace ts {
const index = findIndex(statements, statement => !isPrologueDirective(statement));
emitList(node, statements, ListFormat.MultiLine, index === -1 ? statements.length : index);
popNameGenerationScope();
if (isJsonSourceFile(node) && node.jsonObject) {
emitObjectLiteralExpression(node.jsonObject);
}
}

// Transformation nodes
Expand Down
1 change: 1 addition & 0 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2258,6 +2258,7 @@ namespace ts {
const updated = <SourceFile>createSynthesizedNode(SyntaxKind.SourceFile);
updated.flags |= node.flags;
updated.statements = createNodeArray(statements);
if (isJsonSourceFile(node) && node.jsonObject !== undefined) (<JsonSourceFile>updated).jsonObject = node.jsonObject;
updated.endOfFileToken = node.endOfFileToken;
updated.fileName = node.fileName;
updated.path = node.path;
Expand Down
16 changes: 11 additions & 5 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ namespace ts {
enum Extensions {
TypeScript, /** '.ts', '.tsx', or '.d.ts' */
JavaScript, /** '.js' or '.jsx' */
DtsOnly /** Only '.d.ts' */
Json, /** '.json' */
DtsOnly, /** Only '.d.ts' */
}

interface PathAndPackageId {
Expand Down Expand Up @@ -110,6 +111,7 @@ namespace ts {
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path);
}
return path;

}
}

Expand Down Expand Up @@ -722,7 +724,7 @@ namespace ts {
const failedLookupLocations: string[] = [];
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled };

const result = jsOnly ? tryResolve(Extensions.JavaScript) : (tryResolve(Extensions.TypeScript) || tryResolve(Extensions.JavaScript));
const result = (jsOnly ? tryResolve(Extensions.JavaScript) : (tryResolve(Extensions.TypeScript) || tryResolve(Extensions.JavaScript))) || tryResolve(Extensions.Json);
if (result && result.value) {
const { resolved, isExternalLibraryImport } = result.value;
return createResolvedModuleWithFailedLookupLocations(resolved, isExternalLibraryImport, failedLookupLocations);
Expand Down Expand Up @@ -825,7 +827,7 @@ namespace ts {

// If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one;
// e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts"
if (hasJavaScriptFileExtension(candidate)) {
if (hasJavaScriptFileExtension(candidate) || fileExtensionIs(candidate, Extension.Json)) {
const extensionless = removeFileExtension(candidate);
if (state.traceEnabled) {
const extension = candidate.substring(extensionless.length);
Expand All @@ -852,6 +854,8 @@ namespace ts {
return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts);
case Extensions.JavaScript:
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
case Extensions.Json:
return tryExtension(Extension.Json);
}

function tryExtension(ext: Extension): PathAndExtension | undefined {
Expand Down Expand Up @@ -925,7 +929,7 @@ namespace ts {
}

function loadModuleFromPackageJson(jsonContent: PackageJson, extensions: Extensions, candidate: string, failedLookupLocations: Push<string>, state: ModuleResolutionState): PathAndExtension | undefined {
const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, jsonContent, candidate, state);
const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript && extensions !== Extensions.Json, jsonContent, candidate, state);
if (!file) {
return undefined;
}
Expand Down Expand Up @@ -964,6 +968,8 @@ namespace ts {
switch (extensions) {
case Extensions.JavaScript:
return extension === Extension.Js || extension === Extension.Jsx;
case Extensions.Json:
return extension === Extension.Json;
case Extensions.TypeScript:
return extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Dts;
case Extensions.DtsOnly:
Expand Down Expand Up @@ -1023,7 +1029,7 @@ namespace ts {
if (packageResult) {
return packageResult;
}
if (extensions !== Extensions.JavaScript) {
if (extensions !== Extensions.JavaScript && extensions !== Extensions.Json) {
const nodeModulesAtTypes = combinePaths(nodeModulesFolder, "@types");
let nodeModulesAtTypesExists = nodeModulesFolderExists;
if (nodeModulesFolderExists && !directoryProbablyExists(nodeModulesAtTypes, state.host)) {
Expand Down
16 changes: 14 additions & 2 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ namespace ts {
return visitNodes(cbNode, cbNodes, (<Block>node).statements);
case SyntaxKind.SourceFile:
return visitNodes(cbNode, cbNodes, (<SourceFile>node).statements) ||
visitNode(cbNode, (<JsonSourceFile>node).jsonObject) ||
visitNode(cbNode, (<SourceFile>node).endOfFileToken);
case SyntaxKind.VariableStatement:
return visitNodes(cbNode, cbNodes, node.decorators) ||
Expand Down Expand Up @@ -616,6 +617,13 @@ namespace ts {

export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean, scriptKind?: ScriptKind): SourceFile {
scriptKind = ensureScriptKind(fileName, scriptKind);
if (scriptKind === ScriptKind.JSON) {
const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes);
result.statements = createNodeArray<Statement>(/*elements*/ [], result.pos);
result.typeReferenceDirectives = emptyArray;
result.amdDependencies = emptyArray;
return result;
}

initializeState(sourceText, languageVersion, syntaxCursor, scriptKind);

Expand All @@ -636,8 +644,8 @@ namespace ts {
return isInvalid ? entityName : undefined;
}

export function parseJsonText(fileName: string, sourceText: string): JsonSourceFile {
initializeState(sourceText, ScriptTarget.ES2015, /*syntaxCursor*/ undefined, ScriptKind.JSON);
export function parseJsonText(fileName: string, sourceText: string, languageVersion: ScriptTarget = ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): JsonSourceFile {
initializeState(sourceText, languageVersion, syntaxCursor, ScriptKind.JSON);
// Set source file so that errors will be reported with this file name
sourceFile = createSourceFile(fileName, ScriptTarget.ES2015, ScriptKind.JSON);
const result = <JsonSourceFile>sourceFile;
Expand All @@ -656,6 +664,10 @@ namespace ts {
parseExpected(SyntaxKind.OpenBraceToken);
}

if (setParentNodes) {
fixupParentReferences(sourceFile);
}

sourceFile.parseDiagnostics = parseDiagnostics;
clearState();
return result;
Expand Down
1 change: 1 addition & 0 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2222,6 +2222,7 @@ namespace ts {
switch (extension) {
case Extension.Ts:
case Extension.Dts:
case Extension.Json:
// These are always allowed.
return undefined;
case Extension.Tsx:
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4072,7 +4072,8 @@ namespace ts {
Tsx = ".tsx",
Dts = ".d.ts",
Js = ".js",
Jsx = ".jsx"
Jsx = ".jsx",
Json = ".json"
}

export interface ResolvedModuleWithFailedLookupLocations {
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,10 @@ namespace ts {
return (file.externalModuleIndicator || file.commonJsModuleIndicator) !== undefined;
}

export function isJsonSourceFile(file: SourceFile): file is JsonSourceFile {
return file.scriptKind === ScriptKind.JSON;
}

export function isConstEnumDeclaration(node: Node): boolean {
return node.kind === SyntaxKind.EnumDeclaration && isConst(node);
}
Expand Down
2 changes: 1 addition & 1 deletion src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ namespace FourSlash {
// resolveReference file-option is not specified then do not resolve any files and include all inputFiles
this.inputFiles.forEach((file, fileName) => {
if (!Harness.isDefaultLibraryFile(fileName)) {
this.languageServiceAdapterHost.addScript(fileName, file, /*isRootFile*/ true);
this.languageServiceAdapterHost.addScript(fileName, file, /*isRootFile*/ !ts.fileExtensionIs(fileName, ts.Extension.Json));
}
});
this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName,
Expand Down
4 changes: 2 additions & 2 deletions src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,7 @@ namespace Harness {
}


const programFileNames = programFiles.map(file => file.unitName);
const programFileNames = programFiles.map(file => file.unitName).filter(fileName => !ts.fileExtensionIs(fileName, ts.Extension.Json));

const compilerHost = createCompilerHost(
programFiles.concat(otherFiles),
Expand Down Expand Up @@ -1718,7 +1718,7 @@ namespace Harness {
// .d.ts file, add to declFiles emit
this.declFilesCode.push(emittedFile);
}
else if (isJS(emittedFile.fileName) || isJSX(emittedFile.fileName)) {
else if (isJS(emittedFile.fileName) || isJSX(emittedFile.fileName) || ts.fileExtensionIs(emittedFile.fileName, ts.Extension.Json)) {
// .js file, add to files
this.files.push(emittedFile);
}
Expand Down
2 changes: 1 addition & 1 deletion src/harness/typeWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class TypeWriterWalker {
}

private visitNode(node: ts.Node): void {
if (ts.isPartOfExpression(node) || node.kind === ts.SyntaxKind.Identifier) {
if (ts.isPartOfExpression(node) || node.kind === ts.SyntaxKind.Identifier || ts.isDeclarationName(node)) {
this.logTypeAndSymbol(node);
}

Expand Down
4 changes: 4 additions & 0 deletions src/harness/unittests/reuseProgramStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,10 @@ namespace ts {
"File 'node_modules/a.jsx' does not exist.",
"File 'node_modules/a/index.js' does not exist.",
"File 'node_modules/a/index.jsx' does not exist.",
"Loading module 'a' from 'node_modules' folder, target file type 'Json'.",
"File 'node_modules/a/package.json' does not exist.",
"File 'node_modules/a.json' does not exist.",
"File 'node_modules/a/index.json' does not exist.",
"======== Module name 'a' was not resolved. ========"
],
"initialProgram: execute module resolution normally.");
Expand Down
4 changes: 4 additions & 0 deletions src/harness/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,10 @@ namespace ts.projectSystem {
"Directory '/a/b/node_modules' does not exist, skipping all lookups in it.",
"Directory '/a/node_modules' does not exist, skipping all lookups in it.",
"Directory '/node_modules' does not exist, skipping all lookups in it.",
"Loading module 'lib' from 'node_modules' folder, target file type 'Json'.",
"Directory '/a/b/node_modules' does not exist, skipping all lookups in it.",
"Directory '/a/node_modules' does not exist, skipping all lookups in it.",
"Directory '/node_modules' does not exist, skipping all lookups in it.",
"======== Module name 'lib' was not resolved. ========",
`Auto discovery for typings is enabled in project '${proj.getProjectName()}'. Running extra resolution pass for module 'lib' using cache location '/a/cache'.`,
"File '/a/cache/node_modules/lib.d.ts' does not exist.",
Expand Down
2 changes: 1 addition & 1 deletion src/services/jsTyping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ namespace ts.JsTyping {
}

// depth of 2, so we access `node_modules/foo` but not `node_modules/foo/bar`
const fileNames = host.readDirectory(packagesFolderPath, [".json"], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 2);
const fileNames = host.readDirectory(packagesFolderPath, [Extension.Json], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 2);
if (log) log(`Searching for typing names in ${packagesFolderPath}; all files: ${JSON.stringify(fileNames)}`);
const packageNames: string[] = [];
for (const fileName of fileNames) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/signatureHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,4 @@ namespace ts.SignatureHelp {
};
}
}
}
}
2 changes: 2 additions & 0 deletions tests/baselines/reference/ambientDeclarations.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ var q = M1.fn();
// Ambient external module in the global module
// Ambient external module with a string literal name that is a top level external module name
declare module 'external1' {
>'external1' : Symbol('external1', Decl(ambientDeclarations.ts, 67, 16))

var q;
>q : Symbol(q, Decl(ambientDeclarations.ts, 72, 7))
}
Expand Down
2 changes: 2 additions & 0 deletions tests/baselines/reference/ambientDeclarations.types
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ var q = M1.fn();
// Ambient external module in the global module
// Ambient external module with a string literal name that is a top level external module name
declare module 'external1' {
>'external1' : typeof 'external1'

var q;
>q : any
}
Expand Down
4 changes: 4 additions & 0 deletions tests/baselines/reference/ambientDeclarationsExternal.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ var n: number;
=== tests/cases/conformance/ambient/decls.ts ===
// Ambient external module with export assignment
declare module 'equ' {
>'equ' : Symbol('equ', Decl(decls.ts, 0, 0))

var x;
>x : Symbol(x, Decl(decls.ts, 2, 7))

Expand All @@ -28,6 +30,8 @@ declare module 'equ' {
}

declare module 'equ2' {
>'equ2' : Symbol('equ2', Decl(decls.ts, 4, 1))

var x: number;
>x : Symbol(x, Decl(decls.ts, 7, 7))
}
Expand Down
4 changes: 4 additions & 0 deletions tests/baselines/reference/ambientDeclarationsExternal.types
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ var n: number;
=== tests/cases/conformance/ambient/decls.ts ===
// Ambient external module with export assignment
declare module 'equ' {
>'equ' : typeof 'equ'

var x;
>x : any

Expand All @@ -28,6 +30,8 @@ declare module 'equ' {
}

declare module 'equ2' {
>'equ2' : typeof 'equ2'

var x: number;
>x : number
}
Expand Down
Loading