Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/heft-typescript-plugin",
"comment": "Add `emitModulePackageJson` option for `additionalModuleKindsToEmit` entries. When enabled, a `package.json` with the appropriate `\"type\"` field is written to the output folder after compilation, ensuring Node.js correctly interprets `.js` files regardless of the nearest ancestor package.json `\"type\"` setting.",
"type": "minor"
}
],
"packageName": "@rushstack/heft-typescript-plugin",
"email": "iclanton@users.noreply.github.com"
}
2 changes: 2 additions & 0 deletions common/reviews/api/heft-typescript-plugin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface _ICompilerCapabilities {

// @beta (undocumented)
export interface IEmitModuleKind {
// (undocumented)
emitModulePackageJson?: boolean;
// (undocumented)
jsExtensionOverride?: string;
// (undocumented)
Expand Down
92 changes: 75 additions & 17 deletions heft-plugins/heft-typescript-plugin/src/TypeScriptBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ export class TypeScriptBuilder {
this._cleanupWorker();
//#endregion

this._emitModulePackageJsonFiles(ts);
this._logEmitPerformance(ts);

//#region FINAL_ANALYSIS
Expand Down Expand Up @@ -557,6 +558,8 @@ export class TypeScriptBuilder {
this._cleanupWorker();
//#endregion

this._emitModulePackageJsonFiles(ts);

if (pendingTranspilePromises.size) {
const emitResults: TTypescript.EmitResult[] = await Promise.all(pendingTranspilePromises.values());
for (const { diagnostics } of emitResults) {
Expand Down Expand Up @@ -735,7 +738,8 @@ export class TypeScriptBuilder {
ts.ModuleKind.CommonJS,
tsconfig.options.outDir!,
/* isPrimary */ tsconfig.options.module === ts.ModuleKind.CommonJS,
'.cjs'
'.cjs',
/* emitModulePackageJson */ false
);

const cjsReason: IModuleKindReason = {
Expand All @@ -754,7 +758,8 @@ export class TypeScriptBuilder {
ts.ModuleKind.ESNext,
tsconfig.options.outDir!,
/* isPrimary */ tsconfig.options.module === ts.ModuleKind.ESNext,
'.mjs'
'.mjs',
/* emitModulePackageJson */ false
);

const mjsReason: IModuleKindReason = {
Expand All @@ -773,7 +778,8 @@ export class TypeScriptBuilder {
tsconfig.options.module,
tsconfig.options.outDir!,
/* isPrimary */ true,
/* jsExtensionOverride */ undefined
/* jsExtensionOverride */ undefined,
/* emitModulePackageJson */ false
);

const tsConfigReason: IModuleKindReason = {
Expand All @@ -788,16 +794,14 @@ export class TypeScriptBuilder {
}

if (this._configuration.additionalModuleKindsToEmit) {
for (const additionalModuleKindToEmit of this._configuration.additionalModuleKindsToEmit) {
const moduleKind: TTypescript.ModuleKind = this._parseModuleKind(
ts,
additionalModuleKindToEmit.moduleKind
);
for (const { moduleKind: moduleKindString, outFolderName, emitModulePackageJson = false } of this
._configuration.additionalModuleKindsToEmit) {
const moduleKind: TTypescript.ModuleKind = this._parseModuleKind(ts, moduleKindString);

const outDirKey: string = `${additionalModuleKindToEmit.outFolderName}:.js`;
const outDirKey: string = `${outFolderName}:.js`;
const moduleKindReason: IModuleKindReason = {
kind: ts.ModuleKind[moduleKind] as keyof typeof TTypescript.ModuleKind,
outDir: additionalModuleKindToEmit.outFolderName,
outDir: outFolderName,
extension: '.js',
reason: `additionalModuleKindsToEmit`
};
Expand All @@ -807,18 +811,19 @@ export class TypeScriptBuilder {

if (existingKind) {
throw new Error(
`Module kind "${additionalModuleKindToEmit.moduleKind}" is already emitted at ${existingKind.outDir} with extension '${existingKind.extension}' by option ${existingKind.reason}.`
`Module kind "${moduleKind}" is already emitted at ${existingKind.outDir} with extension '${existingKind.extension}' by option ${existingKind.reason}.`
);
} else if (existingDir) {
throw new Error(
`Output folder "${additionalModuleKindToEmit.outFolderName}" already contains module kind ${existingDir.kind} with extension '${existingDir.extension}', specified by option ${existingDir.reason}.`
`Output folder "${outFolderName}" already contains module kind ${existingDir.kind} with extension '${existingDir.extension}', specified by option ${existingDir.reason}.`
);
} else {
const outFolderKey: string | undefined = this._addModuleKindToEmit(
moduleKind,
additionalModuleKindToEmit.outFolderName,
outFolderName,
/* isPrimary */ false,
undefined
undefined,
emitModulePackageJson
);

if (outFolderKey) {
Expand All @@ -834,7 +839,8 @@ export class TypeScriptBuilder {
moduleKind: TTypescript.ModuleKind,
outFolderPath: string,
isPrimary: boolean,
jsExtensionOverride: string | undefined
jsExtensionOverride: string | undefined,
emitModulePackageJson: boolean
): string | undefined {
let outFolderName: string;
if (path.isAbsolute(outFolderPath)) {
Expand Down Expand Up @@ -885,8 +891,8 @@ export class TypeScriptBuilder {
outFolderPath,
moduleKind,
jsExtensionOverride,

isPrimary
isPrimary,
emitModulePackageJson
});

return `${outFolderName}:${jsExtensionOverride || '.js'}`;
Expand Down Expand Up @@ -972,6 +978,7 @@ export class TypeScriptBuilder {
`Emitting program "${innerCompilerOptions!.configFilePath}"`
);

this._emitModulePackageJsonFiles(ts);
this._logEmitPerformance(ts);

// Reset performance counters
Expand Down Expand Up @@ -1128,6 +1135,57 @@ export class TypeScriptBuilder {
return host;
}

/**
* For each module kind configured with `emitModulePackageJson: true`, writes a
* `package.json` with the appropriate `"type"` field to ensure Node.js correctly
* interprets `.js` files in the output folder.
*/
private _emitModulePackageJsonFiles(ts: ExtendedTypeScript): void {
for (const { emitModulePackageJson, moduleKind, outFolderPath } of this._moduleKindsToEmit) {
if (!emitModulePackageJson) {
continue;
}

// "module" and "commonjs" are the only recognized values. See
// https://nodejs.org/api/packages.html#type
let moduleType: string | undefined;
switch (moduleKind) {
// UMD contains a CommonJS wrapper, so it should be treated as CommonJS for package.json generation purposes
case ts.ModuleKind.UMD:
case ts.ModuleKind.CommonJS: {
moduleType = 'commonjs';
break;
}

case ts.ModuleKind.AMD:
case ts.ModuleKind.None:
case ts.ModuleKind.Preserve:
case ts.ModuleKind.System: {
moduleType = undefined;
break;
}

default: {
moduleType = 'module';
break;
}
}

if (moduleType) {
const packageJsonPath: string = `${outFolderPath}package.json`;
const packageJsonContent: string = `{\n "type": "${moduleType}"\n}\n`;

ts.sys.writeFile(packageJsonPath, packageJsonContent);
this._typescriptTerminal.writeVerboseLine(`Wrote ${packageJsonPath} with "type": "${moduleType}"`);
} else {
throw new Error(
`Unsupported module kind ${ts.ModuleKind[moduleKind]} for package.json generation. ` +
`Remove the \`emitModulePackageJson\` option for this module kind.`
);
}
}
}

private _parseModuleKind(ts: ExtendedTypeScript, moduleKindName: string): TTypescript.ModuleKind {
switch (moduleKindName.toLowerCase()) {
case 'commonjs':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface IEmitModuleKind {
moduleKind: 'commonjs' | 'amd' | 'umd' | 'system' | 'es2015' | 'esnext';
outFolderName: string;
jsExtensionOverride?: string;
emitModulePackageJson?: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"outFolderName": {
"type": "string",
"pattern": "[^\\\\\\/]"
},

"emitModulePackageJson": {
"description": "If true, a package.json file will be written to the output folder with the appropriate \"type\" field for the specified module kind. This ensures that Node.js correctly interprets .js files in the output folder regardless of the nearest ancestor package.json \"type\" setting. Only valid for CommonJS, UMD, and ES module kinds.",
"type": "boolean"
}
},
"required": ["moduleKind", "outFolderName"]
Expand Down
6 changes: 6 additions & 0 deletions heft-plugins/heft-typescript-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,10 @@ export interface ICachedEmitModuleKind {
* Declarations are only emitted for the primary module kind.
*/
isPrimary: boolean;

/**
* If true, a package.json with the appropriate "type" field will be written
* to the output folder after emit.
*/
emitModulePackageJson: boolean;
}