fix(compiler-cli): generate proper exports.* identifiers in cjs output (#22564)
When the compiler generates a reference to an exported variable in the same file, it inserts a synthetic ts.Identifier node. In CommonJS output, this synthetic node would not be properly rewritten with an `exports.` prefix. This change sets the TS original node property on the synthetic node we generate, which ensures TS knows to rewrite it in CommonJS output. PR Close #22564
This commit is contained in:
parent
e8326e600d
commit
0d8deb0795
|
@ -63,6 +63,8 @@ export function updateSourceFile(
|
||||||
sourceFile: ts.SourceFile, module: PartialModule,
|
sourceFile: ts.SourceFile, module: PartialModule,
|
||||||
context: ts.TransformationContext): [ts.SourceFile, Map<ts.Node, Node>] {
|
context: ts.TransformationContext): [ts.SourceFile, Map<ts.Node, Node>] {
|
||||||
const converter = new _NodeEmitterVisitor();
|
const converter = new _NodeEmitterVisitor();
|
||||||
|
converter.loadExportedVariableIdentifiers(sourceFile);
|
||||||
|
|
||||||
const prefixStatements = module.statements.filter(statement => !(statement instanceof ClassStmt));
|
const prefixStatements = module.statements.filter(statement => !(statement instanceof ClassStmt));
|
||||||
const classes =
|
const classes =
|
||||||
module.statements.filter(statement => statement instanceof ClassStmt) as ClassStmt[];
|
module.statements.filter(statement => statement instanceof ClassStmt) as ClassStmt[];
|
||||||
|
@ -158,6 +160,11 @@ function createLiteral(value: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isExportTypeStatement(statement: ts.Statement): boolean {
|
||||||
|
return !!statement.modifiers &&
|
||||||
|
statement.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visits an output ast and produces the corresponding TypeScript synthetic nodes.
|
* Visits an output ast and produces the corresponding TypeScript synthetic nodes.
|
||||||
*/
|
*/
|
||||||
|
@ -166,6 +173,26 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||||
private _importsWithPrefixes = new Map<string, string>();
|
private _importsWithPrefixes = new Map<string, string>();
|
||||||
private _reexports = new Map<string, {name: string, as: string}[]>();
|
private _reexports = new Map<string, {name: string, as: string}[]>();
|
||||||
private _templateSources = new Map<ParseSourceFile, ts.SourceMapSource>();
|
private _templateSources = new Map<ParseSourceFile, ts.SourceMapSource>();
|
||||||
|
private _exportedVariableIdentifiers = new Map<string, ts.Identifier>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the source file and collect exported identifiers that refer to variables.
|
||||||
|
*
|
||||||
|
* Only variables are collected because exported classes still exist in the module scope in
|
||||||
|
* CommonJS, whereas variables have their declarations moved onto the `exports` object, and all
|
||||||
|
* references are updated accordingly.
|
||||||
|
*/
|
||||||
|
loadExportedVariableIdentifiers(sourceFile: ts.SourceFile): void {
|
||||||
|
sourceFile.statements.forEach(statement => {
|
||||||
|
if (ts.isVariableStatement(statement) && isExportTypeStatement(statement)) {
|
||||||
|
statement.declarationList.declarations.forEach(declaration => {
|
||||||
|
if (ts.isIdentifier(declaration.name)) {
|
||||||
|
this._exportedVariableIdentifiers.set(declaration.name.text, declaration.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getReexports(): ts.Statement[] {
|
getReexports(): ts.Statement[] {
|
||||||
return Array.from(this._reexports.entries())
|
return Array.from(this._reexports.entries())
|
||||||
|
@ -612,7 +639,8 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _visitIdentifier(value: ExternalReference): ts.Expression {
|
private _visitIdentifier(value: ExternalReference): ts.Expression {
|
||||||
const {name, moduleName} = value;
|
// name can only be null during JIT which never executes this code.
|
||||||
|
const moduleName = value.moduleName, name = value.name !;
|
||||||
let prefixIdent: ts.Identifier|null = null;
|
let prefixIdent: ts.Identifier|null = null;
|
||||||
if (moduleName) {
|
if (moduleName) {
|
||||||
let prefix = this._importsWithPrefixes.get(moduleName);
|
let prefix = this._importsWithPrefixes.get(moduleName);
|
||||||
|
@ -622,10 +650,17 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||||
}
|
}
|
||||||
prefixIdent = ts.createIdentifier(prefix);
|
prefixIdent = ts.createIdentifier(prefix);
|
||||||
}
|
}
|
||||||
// name can only be null during JIT which never executes this code.
|
if (prefixIdent) {
|
||||||
let result: ts.Expression =
|
return ts.createPropertyAccess(prefixIdent, name);
|
||||||
prefixIdent ? ts.createPropertyAccess(prefixIdent, name !) : ts.createIdentifier(name !);
|
} else {
|
||||||
return result;
|
const id = ts.createIdentifier(name);
|
||||||
|
if (this._exportedVariableIdentifiers.has(name)) {
|
||||||
|
// In order for this new identifier node to be properly rewritten in CommonJS output,
|
||||||
|
// it must have its original node set to a parsed instance of the same identifier.
|
||||||
|
ts.setOriginalNode(id, this._exportedVariableIdentifiers.get(name));
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2158,5 +2158,30 @@ describe('ngc transformer command-line', () => {
|
||||||
expect(source).toMatch(/ngInjectableDef.*token: Service/);
|
expect(source).toMatch(/ngInjectableDef.*token: Service/);
|
||||||
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
|
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('generates exports.* references when outputting commonjs', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs"
|
||||||
|
},
|
||||||
|
"files": ["service.ts"]
|
||||||
|
}`);
|
||||||
|
const source = compileService(`
|
||||||
|
import {Inject, Injectable, InjectionToken, APP_ROOT_SCOPE} from '@angular/core';
|
||||||
|
import {Module} from './module';
|
||||||
|
|
||||||
|
export const TOKEN = new InjectionToken<string>('test token', {
|
||||||
|
scope: APP_ROOT_SCOPE,
|
||||||
|
factory: () => 'this is a test',
|
||||||
|
});
|
||||||
|
|
||||||
|
@Injectable({scope: APP_ROOT_SCOPE})
|
||||||
|
export class Service {
|
||||||
|
constructor(@Inject(TOKEN) token: any) {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(source).toMatch(/new Service\(i0\.inject\(exports\.TOKEN\)\);/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue