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

fix(multiple): ensure re-exported module symbols can be imported #30667

Merged
merged 1 commit into from
Mar 20, 2025
Merged
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
27 changes: 27 additions & 0 deletions integration/module-tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
load("//tools:defaults2.bzl", "ts_project")
load("//integration/module-tests:index.bzl", "module_test")

ts_project(
name = "test_lib",
testonly = True,
srcs = glob(["*.mts"]),
source_map = False,
tsconfig = "tsconfig.json",
deps = [
"//:node_modules/@angular/compiler-cli",
"//:node_modules/@types/node",
"//:node_modules/typescript",
],
)

module_test(
name = "test",
npm_packages = {
"//src/cdk:npm_package": "src/cdk/npm_package",
"//src/material:npm_package": "src/material/npm_package",
},
skipped_entry_points = [
# This entry-point is JIT and would fail the AOT test.
"@angular/material/dialog/testing",
],
)
60 changes: 60 additions & 0 deletions integration/module-tests/find-all-modules.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as ts from 'typescript';

export async function findAllEntryPointsAndExportedModules(packagePath: string) {
const packageJsonRaw = await fs.readFile(path.join(packagePath, 'package.json'), 'utf8');
const packageJson = JSON.parse(packageJsonRaw) as {
name: string;
exports: Record<string, Record<string, string>>;
};
const tasks: Promise<{importPath: string; symbolName: string}[]>[] = [];

for (const [subpath, conditions] of Object.entries(packageJson.exports)) {
if (conditions.types === undefined) {
continue;
}

tasks.push(
(async () => {
const dtsFile = path.join(packagePath, conditions.types);
const dtsBundleFile = ts.createSourceFile(
dtsFile,
await fs.readFile(dtsFile, 'utf8'),
ts.ScriptTarget.ESNext,
false,
);

return scanExportsForModules(dtsBundleFile).map(e => ({
importPath: path.posix.join(packageJson.name, subpath),
symbolName: e,
}));
})(),
);
}

const moduleExports = (await Promise.all(tasks)).flat();

return {
name: packageJson.name,
packagePath,
moduleExports,
};
}

function scanExportsForModules(sf: ts.SourceFile): string[] {
const moduleExports: string[] = [];
const visit = (node: ts.Node) => {
if (ts.isExportDeclaration(node) && ts.isNamedExports(node.exportClause)) {
moduleExports.push(
...node.exportClause.elements
.filter(e => e.name.text.endsWith('Module'))
.map(e => e.name.text),
);
}
};

ts.forEachChild(sf, visit);

return moduleExports;
}
24 changes: 24 additions & 0 deletions integration/module-tests/index.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load("@aspect_rules_js//js:defs.bzl", "js_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")

def module_test(name, npm_packages, skipped_entry_points = [], additional_deps = []):
write_file(
name = "%s_config" % name,
out = "%s_config.json" % name,
content = [json.encode({
"packages": [pkg[1] for pkg in npm_packages.items()],
"skipEntryPoints": skipped_entry_points,
})],
)

js_test(
name = "test",
data = [
":%s_config" % name,
"//integration/module-tests:test_lib_rjs",
"//:node_modules/@angular/common",
"//:node_modules/@angular/core",
] + additional_deps + [pkg[0] for pkg in npm_packages.items()],
entry_point = ":index.mjs",
fixed_args = ["$(rootpath :%s_config)" % name],
)
86 changes: 86 additions & 0 deletions integration/module-tests/index.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {performCompilation} from '@angular/compiler-cli';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import ts from 'typescript';
import {findAllEntryPointsAndExportedModules} from './find-all-modules.mjs';

async function main() {
const [configPath] = process.argv.slice(2);
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ng-module-test-'));
const config = JSON.parse(await fs.readFile(configPath, 'utf8')) as {
packages: string[];
skipEntryPoints: string[];
};

const packages = await Promise.all(
config.packages.map(pkgPath => findAllEntryPointsAndExportedModules(pkgPath)),
);

const exports = packages
.map(p => p.moduleExports)
.flat()
.filter(e => !config.skipEntryPoints.includes(e.importPath));

const testFile = `
import {NgModule, Component} from '@angular/core';
${exports.map(e => `import {${e.symbolName}} from '${e.importPath}';`).join('\n')}

@NgModule({
exports: [
${exports.map(e => e.symbolName).join(', ')}
]
})
export class TestModule {}

@Component({imports: [TestModule], template: ''})
export class TestComponent {}
`;

await fs.writeFile(path.join(tmpDir, 'test.ts'), testFile);

// Prepare node modules to resolve e.g. `@angular/core`
await fs.symlink(path.resolve('./node_modules'), path.join(tmpDir, 'node_modules'));
// Prepare node modules to resolve e.g. `@angular/cdk`. This is possible
// as we are inside the sandbox, inside our test runfiles directory.
for (const {packagePath, name} of packages) {
await fs.symlink(path.resolve(packagePath), `./node_modules/${name}`);
}

const result = performCompilation({
options: {
rootDir: tmpDir,
skipLibCheck: true,
noEmit: true,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
strictTemplates: true,
preserveSymlinks: true,
strict: true,
// Note: HMR is needed as it will disable the Angular compiler's tree-shaking of used
// directives/components. This is critical for this test as it allows us to simply all
// modules and automatically validate that all symbols are reachable/importable.
_enableHmr: true,
},
rootNames: [path.join(tmpDir, 'test.ts')],
});

console.error(
ts.formatDiagnosticsWithColorAndContext(result.diagnostics, {
getCanonicalFileName: f => f,
getCurrentDirectory: () => '/',
getNewLine: () => '\n',
}),
);

await fs.rm(tmpDir, {recursive: true, force: true, maxRetries: 2});

if (result.diagnostics.length > 0) {
process.exitCode = 1;
}
}

main().catch(e => {
console.error('Error', e);
process.exitCode = 1;
});
8 changes: 8 additions & 0 deletions integration/module-tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"types": ["node"]
}
}
11 changes: 11 additions & 0 deletions src/cdk/dialog/dialog-module.ts
Original file line number Diff line number Diff line change
@@ -24,3 +24,14 @@ import {CdkDialogContainer} from './dialog-container';
providers: [Dialog],
})
export class DialogModule {}

// Re-export needed by the Angular compiler.
// See: https://github.com/angular/components/issues/30663.
// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
// consuming libraries might have references to them in their own partial compilation output.
export {
CdkPortal as ɵɵCdkPortal,
CdkPortalOutlet as ɵɵCdkPortalOutlet,
TemplatePortalDirective as ɵɵTemplatePortalDirective,
PortalHostDirective as ɵɵPortalHostDirective,
} from '../portal';
6 changes: 6 additions & 0 deletions src/cdk/drag-drop/drag-drop-module.ts
Original file line number Diff line number Diff line change
@@ -31,3 +31,9 @@ const DRAG_DROP_DIRECTIVES = [
providers: [DragDrop],
})
export class DragDropModule {}

// Re-export needed by the Angular compiler.
// See: https://github.com/angular/components/issues/30663.
// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
// consuming libraries might have references to them in their own partial compilation output.
export {CdkScrollable as ɵɵCdkScrollable} from '../scrolling';
13 changes: 13 additions & 0 deletions src/cdk/overlay/overlay-module.ts
Original file line number Diff line number Diff line change
@@ -23,3 +23,16 @@ import {
providers: [Overlay, CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY_PROVIDER],
})
export class OverlayModule {}

// Re-export needed by the Angular compiler.
// See: https://github.com/angular/components/issues/30663.
// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
// consuming libraries might have references to them in their own partial compilation output.
export {
CdkScrollableModule as ɵɵCdkScrollableModule,
CdkFixedSizeVirtualScroll as ɵɵCdkFixedSizeVirtualScroll,
CdkVirtualForOf as ɵɵCdkVirtualForOf,
CdkVirtualScrollViewport as ɵɵCdkVirtualScrollViewport,
CdkVirtualScrollableWindow as ɵɵCdkVirtualScrollableWindow,
CdkVirtualScrollableElement as ɵɵCdkVirtualScrollableElement,
} from '../scrolling';
6 changes: 6 additions & 0 deletions src/cdk/scrolling/scrolling-module.ts
Original file line number Diff line number Diff line change
@@ -45,3 +45,9 @@ export class CdkScrollableModule {}
],
})
export class ScrollingModule {}

// Re-export needed by the Angular compiler.
// See: https://github.com/angular/components/issues/30663.
// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
// consuming libraries might have references to them in their own partial compilation output.
export {Dir as ɵɵDir} from '../bidi';
Loading
Oops, something went wrong.