Skip to content

Support resolveJsonModule in new module modes #46434

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

Merged
merged 2 commits into from
Oct 27, 2021
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
107 changes: 74 additions & 33 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,11 @@ namespace ts {
return usageMode === ModuleKind.ESNext && targetMode === ModuleKind.CommonJS;
}

function isOnlyImportedAsDefault(usage: Expression) {
Copy link
Member

Choose a reason for hiding this comment

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

should this name include Json somewhere, like isJsonOnlyImportedAsDefault? I thought it might be evident from its usage, but it appears to be used in a place with all kinds of imports.
I also don't understand what 'Only' means here.

Copy link

@frank-dspeed frank-dspeed Oct 26, 2021

Choose a reason for hiding this comment

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

Only means in this case that for example

import { version } from './package.json'

does not work only the default works so

import pkg from './package.json'
const { version } = pkg;

only means that no destructuring style named imports are supported.

const usageMode = getUsageModeForExpression(usage);
return usageMode === ModuleKind.ESNext && endsWith((usage as StringLiteralLike).text, Extension.Json);
}

function canHaveSyntheticDefault(file: SourceFile | undefined, moduleSymbol: Symbol, dontResolveAlias: boolean, usage: Expression) {
const usageMode = file && getUsageModeForExpression(usage);
if (file && usageMode !== undefined) {
Expand Down Expand Up @@ -2688,8 +2693,9 @@ namespace ts {
}

const file = moduleSymbol.declarations?.find(isSourceFile);
const hasDefaultOnly = isOnlyImportedAsDefault(node.parent.moduleSpecifier);
const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias, node.parent.moduleSpecifier);
if (!exportDefaultSymbol && !hasSyntheticDefault) {
if (!exportDefaultSymbol && !hasSyntheticDefault && !hasDefaultOnly) {
if (hasExportAssignmentSymbol(moduleSymbol)) {
const compilerOptionName = moduleKind >= ModuleKind.ES2015 ? "allowSyntheticDefaultImports" : "esModuleInterop";
const exportEqualsSymbol = moduleSymbol.exports!.get(InternalSymbolName.ExportEquals);
Expand All @@ -2708,7 +2714,7 @@ namespace ts {
reportNonDefaultExport(moduleSymbol, node);
}
}
else if (hasSyntheticDefault) {
else if (hasSyntheticDefault || hasDefaultOnly) {
// per emit behavior, a synthetic default overrides a "real" .default member if `__esModule` is not present
const resolved = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias);
markSymbolOfAliasDeclarationIfTypeOnly(node, moduleSymbol, resolved, /*overwriteTypeOnly*/ false);
Expand Down Expand Up @@ -2840,7 +2846,7 @@ namespace ts {
let symbolFromModule = getExportOfModule(targetSymbol, name, specifier, dontResolveAlias);
if (symbolFromModule === undefined && name.escapedText === InternalSymbolName.Default) {
const file = moduleSymbol.declarations?.find(isSourceFile);
if (canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias, moduleSpecifier)) {
if (isOnlyImportedAsDefault(moduleSpecifier) || canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias, moduleSpecifier)) {
symbolFromModule = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias);
}
}
Expand Down Expand Up @@ -3430,6 +3436,9 @@ namespace ts {
if (isSyncImport && sourceFile.impliedNodeFormat === ModuleKind.ESNext) {
error(errorNode, Diagnostics.Module_0_cannot_be_imported_using_this_construct_The_specifier_only_resolves_to_an_ES_module_which_cannot_be_imported_synchronously_Use_dynamic_import_instead, moduleReference);
}
if (mode === ModuleKind.ESNext && compilerOptions.resolveJsonModule && resolvedModule.extension === Extension.Json) {
error(errorNode, Diagnostics.JSON_imports_are_experimental_in_ES_module_mode_imports);
}
}
// merged symbol is module declaration symbol combined with all augmentations
return getMergedSymbol(sourceFile.symbol);
Expand Down Expand Up @@ -3592,39 +3601,51 @@ namespace ts {
return symbol;
}

if (getESModuleInterop(compilerOptions)) {
const referenceParent = referencingLocation.parent;
if (
(isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) ||
isImportCall(referenceParent)
) {
const type = getTypeOfSymbol(symbol);
const referenceParent = referencingLocation.parent;
if (
(isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) ||
isImportCall(referenceParent)
) {
const reference = isImportCall(referenceParent) ? referenceParent.arguments[0] : referenceParent.moduleSpecifier;
const type = getTypeOfSymbol(symbol);
const defaultOnlyType = getTypeWithSyntheticDefaultOnly(type, symbol, moduleSymbol!, reference);
if (defaultOnlyType) {
return cloneTypeAsModuleType(symbol, defaultOnlyType, referenceParent);
}

if (getESModuleInterop(compilerOptions)) {
let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call);
if (!sigs || !sigs.length) {
sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct);
}
if (sigs && sigs.length) {
const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol!, isImportCall(referenceParent) ? referenceParent.arguments[0] : referenceParent.moduleSpecifier);
// Create a new symbol which has the module's type less the call and construct signatures
const result = createSymbol(symbol.flags, symbol.escapedName);
result.declarations = symbol.declarations ? symbol.declarations.slice() : [];
result.parent = symbol.parent;
result.target = symbol;
result.originatingImport = referenceParent;
if (symbol.valueDeclaration) result.valueDeclaration = symbol.valueDeclaration;
if (symbol.constEnumOnlyModule) result.constEnumOnlyModule = true;
if (symbol.members) result.members = new Map(symbol.members);
if (symbol.exports) result.exports = new Map(symbol.exports);
const resolvedModuleType = resolveStructuredTypeMembers(moduleType as StructuredType); // Should already be resolved from the signature checks above
result.type = createAnonymousType(result, resolvedModuleType.members, emptyArray, emptyArray, resolvedModuleType.indexInfos);
return result;
if ((sigs && sigs.length) || getPropertyOfType(type, InternalSymbolName.Default)) {
const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol!, reference);
return cloneTypeAsModuleType(symbol, moduleType, referenceParent);
}
}
}
}
return symbol;
}

/**
* Create a new symbol which has the module's type less the call and construct signatures
*/
function cloneTypeAsModuleType(symbol: Symbol, moduleType: Type, referenceParent: ImportDeclaration | ImportCall) {
const result = createSymbol(symbol.flags, symbol.escapedName);
result.declarations = symbol.declarations ? symbol.declarations.slice() : [];
result.parent = symbol.parent;
result.target = symbol;
result.originatingImport = referenceParent;
if (symbol.valueDeclaration) result.valueDeclaration = symbol.valueDeclaration;
if (symbol.constEnumOnlyModule) result.constEnumOnlyModule = true;
if (symbol.members) result.members = new Map(symbol.members);
if (symbol.exports) result.exports = new Map(symbol.exports);
const resolvedModuleType = resolveStructuredTypeMembers(moduleType as StructuredType); // Should already be resolved from the signature checks above
result.type = createAnonymousType(result, resolvedModuleType.members, emptyArray, emptyArray, resolvedModuleType.indexInfos);
return result;
}

function hasExportAssignmentSymbol(moduleSymbol: Symbol): boolean {
return moduleSymbol.exports!.get(InternalSymbolName.ExportEquals) !== undefined;
}
Expand Down Expand Up @@ -30980,27 +31001,47 @@ namespace ts {
if (moduleSymbol) {
const esModuleSymbol = resolveESModuleSymbol(moduleSymbol, specifier, /*dontRecursivelyResolve*/ true, /*suppressUsageError*/ false);
if (esModuleSymbol) {
return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol, specifier));
return createPromiseReturnType(node,
getTypeWithSyntheticDefaultOnly(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol, specifier) ||
getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol, specifier)
);
}
}
return createPromiseReturnType(node, anyType);
}

function createDefaultPropertyWrapperForModule(symbol: Symbol, originalSymbol: Symbol, anonymousSymbol?: Symbol | undefined) {
const memberTable = createSymbolTable();
const newSymbol = createSymbol(SymbolFlags.Alias, InternalSymbolName.Default);
newSymbol.parent = originalSymbol;
newSymbol.nameType = getStringLiteralType("default");
newSymbol.target = resolveSymbol(symbol);
memberTable.set(InternalSymbolName.Default, newSymbol);
return createAnonymousType(anonymousSymbol, memberTable, emptyArray, emptyArray, emptyArray);
}

function getTypeWithSyntheticDefaultOnly(type: Type, symbol: Symbol, originalSymbol: Symbol, moduleSpecifier: Expression) {
const hasDefaultOnly = isOnlyImportedAsDefault(moduleSpecifier);
if (hasDefaultOnly && type && !isErrorType(type)) {
const synthType = type as SyntheticDefaultModuleType;
Copy link
Member

Choose a reason for hiding this comment

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

not related to this PR, but: is this default-wrapper behaviour likely to change as importing JSON stops being experimental?

Choose a reason for hiding this comment

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

@sandersn nope it should not stop as the behavior will stay there are currently no plans to change that inside nodejs so only default imports are supported for json type imports as the json is always a whole document there is no partial json parser.

if (!synthType.defaultOnlyType) {
const type = createDefaultPropertyWrapperForModule(symbol, originalSymbol);
synthType.defaultOnlyType = type;
}
return synthType.defaultOnlyType;
}
return undefined;
}

function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol, originalSymbol: Symbol, moduleSpecifier: Expression): Type {
if (allowSyntheticDefaultImports && type && !isErrorType(type)) {
const synthType = type as SyntheticDefaultModuleType;
if (!synthType.syntheticType) {
const file = originalSymbol.declarations?.find(isSourceFile);
const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false, moduleSpecifier);
if (hasSyntheticDefault) {
const memberTable = createSymbolTable();
const newSymbol = createSymbol(SymbolFlags.Alias, InternalSymbolName.Default);
newSymbol.parent = originalSymbol;
newSymbol.nameType = getStringLiteralType("default");
newSymbol.target = resolveSymbol(symbol);
memberTable.set(InternalSymbolName.Default, newSymbol);
const anonymousSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type);
const defaultContainingObject = createAnonymousType(anonymousSymbol, memberTable, emptyArray, emptyArray, emptyArray);
const defaultContainingObject = createDefaultPropertyWrapperForModule(symbol, originalSymbol, anonymousSymbol);
anonymousSymbol.type = defaultContainingObject;
synthType.syntheticType = isValidSpreadType(type) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*objectFlags*/ 0, /*readonly*/ false) : defaultContainingObject;
}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5997,6 +5997,10 @@
"category": "Error",
"code": 7061
},
"JSON imports are experimental in ES module mode imports.": {
"category": "Error",
"code": 7062
},

"You cannot rename this element.": {
"category": "Error",
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3333,7 +3333,9 @@ namespace ts {
}

if (options.resolveJsonModule) {
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) {
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs &&
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Node12 &&
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeNext) {
createDiagnosticForOptionName(Diagnostics.Option_resolveJsonModule_cannot_be_specified_without_node_module_resolution_strategy, "resolveJsonModule");
}
// Any emit other than common js, amd, es2015 or esnext is error
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5579,6 +5579,7 @@ namespace ts {
/* @internal */
export interface SyntheticDefaultModuleType extends Type {
syntheticType?: Type;
defaultOnlyType?: Type;
}

export interface InstantiableType extends Type {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6168,6 +6168,8 @@ namespace ts {
case ModuleKind.ES2020:
case ModuleKind.ES2022:
case ModuleKind.ESNext:
case ModuleKind.Node12:
case ModuleKind.NodeNext:
return true;
default:
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
tests/cases/conformance/node/index.mts(1,17): error TS7062: JSON imports are experimental in ES module mode imports.
tests/cases/conformance/node/index.mts(3,21): error TS7062: JSON imports are experimental in ES module mode imports.
tests/cases/conformance/node/index.ts(1,17): error TS7062: JSON imports are experimental in ES module mode imports.
tests/cases/conformance/node/index.ts(3,21): error TS7062: JSON imports are experimental in ES module mode imports.


==== tests/cases/conformance/node/index.ts (2 errors) ====
import pkg from "./package.json"
~~~~~~~~~~~~~~~~
!!! error TS7062: JSON imports are experimental in ES module mode imports.
export const name = pkg.name;
import * as ns from "./package.json";
~~~~~~~~~~~~~~~~
!!! error TS7062: JSON imports are experimental in ES module mode imports.
export const thing = ns;
export const name2 = ns.default.name;
==== tests/cases/conformance/node/index.cts (0 errors) ====
import pkg from "./package.json"
export const name = pkg.name;
import * as ns from "./package.json";
export const thing = ns;
export const name2 = ns.default.name;
==== tests/cases/conformance/node/index.mts (2 errors) ====
import pkg from "./package.json"
~~~~~~~~~~~~~~~~
!!! error TS7062: JSON imports are experimental in ES module mode imports.
export const name = pkg.name;
import * as ns from "./package.json";
~~~~~~~~~~~~~~~~
!!! error TS7062: JSON imports are experimental in ES module mode imports.
export const thing = ns;
export const name2 = ns.default.name;
==== tests/cases/conformance/node/package.json (0 errors) ====
{
"name": "pkg",
"version": "0.0.1",
"type": "module",
"default": "misedirection"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//// [tests/cases/conformance/node/nodeModulesResolveJsonModule.ts] ////

//// [index.ts]
import pkg from "./package.json"
export const name = pkg.name;
import * as ns from "./package.json";
export const thing = ns;
export const name2 = ns.default.name;
//// [index.cts]
import pkg from "./package.json"
export const name = pkg.name;
import * as ns from "./package.json";
export const thing = ns;
export const name2 = ns.default.name;
//// [index.mts]
import pkg from "./package.json"
export const name = pkg.name;
import * as ns from "./package.json";
export const thing = ns;
export const name2 = ns.default.name;
//// [package.json]
{
"name": "pkg",
"version": "0.0.1",
"type": "module",
"default": "misedirection"
}

//// [package.json]
{
"name": "pkg",
"version": "0.0.1",
"type": "module",
"default": "misedirection"
}
//// [index.js]
import pkg from "./package.json";
export const name = pkg.name;
import * as ns from "./package.json";
export const thing = ns;
export const name2 = ns.default.name;
//// [index.cjs]
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.name2 = exports.thing = exports.name = void 0;
const package_json_1 = __importDefault(require("./package.json"));
exports.name = package_json_1.default.name;
const ns = __importStar(require("./package.json"));
exports.thing = ns;
exports.name2 = ns.default.name;
//// [index.mjs]
import pkg from "./package.json";
export const name = pkg.name;
import * as ns from "./package.json";
export const thing = ns;
export const name2 = ns.default.name;


//// [index.d.ts]
export declare const name: string;
export declare const thing: {
default: {
name: string;
version: string;
type: string;
default: string;
};
};
export declare const name2: string;
//// [index.d.cts]
export declare const name: string;
export declare const thing: {
default: {
name: string;
version: string;
type: string;
default: string;
};
name: string;
version: string;
type: string;
};
export declare const name2: string;
//// [index.d.mts]
export declare const name: string;
export declare const thing: {
default: {
name: string;
version: string;
type: string;
default: string;
};
};
export declare const name2: string;
Loading