Skip to content

Commit 3676b8a

Browse files
committed
Allow components from dependencies to be used, Closes #39
1 parent 0a7db03 commit 3676b8a

23 files changed

+1631
-380
lines changed

lib/generate/Generator.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ClassLoader } from '../parse/ClassLoader';
88
import { ConstructorLoader } from '../parse/ConstructorLoader';
99
import { PackageMetadataLoader } from '../parse/PackageMetadataLoader';
1010
import { ParameterResolver } from '../parse/ParameterResolver';
11+
import { ExternalModulesLoader } from '../resolution/ExternalModulesLoader';
1112
import type { ResolutionContext } from '../resolution/ResolutionContext';
1213
import type { PathDestinationDefinition } from '../serialize/ComponentConstructor';
1314
import { ComponentConstructor } from '../serialize/ComponentConstructor';
@@ -44,14 +45,24 @@ export class Generator {
4445
const classIndexer = new ClassIndexer({ classLoader, classFinder, ignoreClasses: this.ignoreClasses });
4546

4647
// Find all relevant classes
47-
const packageExports = await classFinder.getPackageExports(packageMetadata.typesPath);
48+
const packageExports = await classFinder.getPackageExports(packageMetadata.name, packageMetadata.typesPath);
4849
const classIndex = await classIndexer.createIndex(packageExports);
4950

5051
// Load constructor data
5152
const constructorsUnresolved = new ConstructorLoader().getConstructors(classIndex);
5253
const constructors = await new ParameterResolver({ classLoader, ignoreClasses: this.ignoreClasses })
5354
.resolveAllConstructorParameters(constructorsUnresolved, classIndex);
5455

56+
// Load external components
57+
const logger = ComponentsManagerBuilder.createLogger(this.logLevel);
58+
const externalModulesLoader = new ExternalModulesLoader({
59+
pathDestination: this.pathDestination,
60+
packageMetadata,
61+
logger,
62+
});
63+
const externalPackages = externalModulesLoader.findExternalPackages(classIndex, constructors);
64+
const externalComponents = await externalModulesLoader.loadExternalComponents(require, externalPackages);
65+
5566
// Create components
5667
const contextConstructor = new ContextConstructor({
5768
packageMetadata,
@@ -63,11 +74,13 @@ export class Generator {
6374
pathDestination: this.pathDestination,
6475
classReferences: classIndex,
6576
classConstructors: constructors,
77+
externalComponents,
6678
contextParser: new ContextParser({
6779
documentLoader: new PrefetchedDocumentLoader({
68-
contexts: {},
69-
logger: ComponentsManagerBuilder.createLogger(this.logLevel),
80+
contexts: externalComponents.moduleState.contexts,
81+
logger,
7082
}),
83+
skipValidation: true,
7184
}),
7285
});
7386
const components = await componentConstructor.constructComponents();

lib/parse/ClassFinder.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@ export class ClassFinder {
1414

1515
/**
1616
* From a given types index, find all named exports.
17+
* @param packageName Package we are checking.
1718
* @param typesPath The path to the index typings file.
1819
*/
19-
public async getPackageExports(typesPath: string): Promise<ClassIndex<ClassReference>> {
20+
public async getPackageExports(packageName: string, typesPath: string): Promise<ClassIndex<ClassReference>> {
2021
let exports: ClassIndex<ClassReference> = {};
2122

2223
// Start from the package index, and collect all named exports.
2324
const paths = [ typesPath ];
2425
for (const path of paths) {
25-
const { named, unnamed } = await this.getFileExports(path);
26+
const { named, unnamed } = await this.getFileExports(packageName, path);
2627
exports = { ...exports, ...named };
2728
for (const additionalPath of unnamed) {
28-
paths.push(additionalPath);
29+
paths.push(additionalPath.fileName);
2930
}
3031
}
3132

@@ -34,10 +35,11 @@ export class ClassFinder {
3435

3536
/**
3637
* Get all named and unnamed exports from the given file.
38+
* @param packageName Package we are checking.
3739
* @param fileName The path to a typescript file.
3840
*/
39-
public async getFileExports(fileName: string):
40-
Promise<{ named: ClassIndex<ClassReference>; unnamed: string[] }> {
41+
public async getFileExports(packageName: string, fileName: string):
42+
Promise<{ named: ClassIndex<ClassReference>; unnamed: { packageName: string; fileName: string }[] }> {
4143
// Load the elements of the class
4244
const {
4345
exportedClasses,
@@ -46,12 +48,15 @@ export class ClassFinder {
4648
exportedUnknowns,
4749
declaredClasses,
4850
importedElements,
49-
} = await this.classLoader.loadClassElements(fileName);
50-
const exportDefinitions: { named: ClassIndex<ClassReference>; unnamed: string[] } = { named: {}, unnamed: []};
51+
} = await this.classLoader.loadClassElements(packageName, fileName);
52+
const exportDefinitions:
53+
{ named: ClassIndex<ClassReference>; unnamed: { packageName: string; fileName: string }[] } =
54+
{ named: {}, unnamed: []};
5155

5256
// Get all named exports
5357
for (const localName in exportedClasses) {
5458
exportDefinitions.named[localName] = {
59+
packageName,
5560
localName,
5661
fileName,
5762
};
@@ -60,6 +65,7 @@ export class ClassFinder {
6065
// Get all named exports from other files
6166
for (const [ exportedName, { localName, fileName: importedFileName }] of Object.entries(exportedImportedElements)) {
6267
exportDefinitions.named[exportedName] = {
68+
packageName,
6369
localName,
6470
fileName: importedFileName,
6571
};
@@ -72,6 +78,7 @@ export class ClassFinder {
7278
// First check declared classes
7379
if (localName in declaredClasses) {
7480
exportDefinitions.named[exportedName] = {
81+
packageName,
7582
localName,
7683
fileName,
7784
};

lib/parse/ClassIndex.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export type ClassIndex<T> = Record<string, T>;
1010
* The name and location of a class.
1111
*/
1212
export interface ClassReference {
13+
// Name of the package this class is part of.
14+
packageName: string;
1315
// The name of the class within the file.
1416
localName: string;
1517
// The name of the file the class is defined in.

lib/parse/ClassIndexer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class ClassIndexer {
4747
if (superClassName && !(superClassName in this.ignoreClasses)) {
4848
try {
4949
classReferenceLoaded.superClass = await this.loadClassChain({
50+
packageName: classReferenceLoaded.packageName,
5051
localName: superClassName,
5152
fileName: classReferenceLoaded.fileName,
5253
});

lib/parse/ClassLoader.ts

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class ClassLoader {
8181
importedElements,
8282
exportedImportedAll,
8383
exportedImportedElements,
84-
} = this.getClassElements(classReference.fileName, ast);
84+
} = this.getClassElements(classReference.packageName, classReference.fileName, ast);
8585

8686
// If the class has been exported in this file, return directly
8787
if (classReference.localName in exportedClasses) {
@@ -149,7 +149,7 @@ export class ClassLoader {
149149
// If we still haven't found the class, iterate over all export all's
150150
for (const subFile of exportedImportedAll) {
151151
try {
152-
return await this.loadClassDeclaration({ localName: classReference.localName, fileName: subFile },
152+
return await this.loadClassDeclaration({ localName: classReference.localName, ...subFile },
153153
considerInterfaces);
154154
} catch {
155155
// Ignore class not found errors
@@ -188,37 +188,88 @@ export class ClassLoader {
188188

189189
/**
190190
* Load a class, and get all class elements from it.
191+
* @param packageName Package name we are importing from.
191192
* @param fileName A file path.
192193
*/
193-
public async loadClassElements(fileName: string): Promise<ClassElements> {
194+
public async loadClassElements(packageName: string, fileName: string): Promise<ClassElements> {
194195
const ast = await this.resolutionContext.parseTypescriptFile(fileName);
195-
return this.getClassElements(fileName, ast);
196+
return this.getClassElements(packageName, fileName, ast);
196197
}
197198

198199
/**
199-
* Convert the given import path to an absolute file path.
200+
* Convert the given import path to an absolute file path, coupled with the module it is part of.
201+
* @param currentPackageName Package name we are importing from.
200202
* @param currentFilePath Absolute path to a file in which the import path occurs.
201203
* @param importPath Possibly relative path that is being imported.
202204
*/
203-
public importTargetToAbsolutePath(currentFilePath: string, importPath: string): string {
204-
// TODO: Add support for imports from other packages (#39)
205-
return Path.join(Path.dirname(currentFilePath), importPath);
205+
public importTargetToAbsolutePath(
206+
currentPackageName: string,
207+
currentFilePath: string,
208+
importPath: string,
209+
): { packageName: string; fileName: string } {
210+
// Handle import paths within the current package
211+
if (importPath.startsWith('.')) {
212+
return {
213+
packageName: currentPackageName,
214+
fileName: Path.join(Path.dirname(currentFilePath), importPath),
215+
};
216+
}
217+
218+
// Handle import paths to other packages
219+
let packageName: string;
220+
let packagePath: string | undefined;
221+
if (importPath.startsWith('@')) {
222+
const slashIndexFirst = importPath.indexOf('/');
223+
if (slashIndexFirst < 0) {
224+
throw new Error(`Invalid scoped package name for import path '${importPath}' in '${currentFilePath}'`);
225+
}
226+
const slashIndexSecond = importPath.indexOf('/', slashIndexFirst + 1);
227+
if (slashIndexSecond < 0) {
228+
// Import form: "@scope/package"
229+
packageName = importPath;
230+
} else {
231+
// Import form: "@scope/package/path"
232+
packageName = importPath.slice(0, Math.max(0, slashIndexSecond));
233+
packagePath = importPath.slice(slashIndexSecond + 1);
234+
}
235+
} else {
236+
const slashIndex = importPath.indexOf('/');
237+
if (slashIndex < 0) {
238+
// Import form: "package"
239+
packageName = importPath;
240+
} else {
241+
// Import form: "package/path"
242+
packageName = importPath.slice(0, Math.max(0, slashIndex));
243+
packagePath = importPath.slice(slashIndex + 1);
244+
}
245+
}
246+
247+
// Resolve paths
248+
const packageRoot = this.resolutionContext.resolvePackageIndex(packageName, currentFilePath);
249+
const remoteFilePath = packagePath ?
250+
Path.resolve(Path.dirname(packageRoot), packagePath) :
251+
packageRoot.slice(0, packageRoot.lastIndexOf('.'));
252+
return {
253+
packageName,
254+
fileName: remoteFilePath,
255+
};
206256
}
207257

208258
/**
209259
* Get all class elements in a file.
260+
* @param packageName Package name we are importing from.
210261
* @param fileName A file path.
211262
* @param ast The parsed file.
212263
*/
213-
public getClassElements(fileName: string, ast: AST<TSESTreeOptions>): ClassElements {
264+
public getClassElements(packageName: string, fileName: string, ast: AST<TSESTreeOptions>): ClassElements {
214265
const exportedClasses: Record<string, ClassDeclaration> = {};
215266
const exportedInterfaces: Record<string, TSInterfaceDeclaration> = {};
216-
const exportedImportedElements: Record<string, { localName: string; fileName: string }> = {};
217-
const exportedImportedAll: string[] = [];
267+
const exportedImportedElements: Record<string, ClassReference> = {};
268+
const exportedImportedAll: { packageName: string; fileName: string }[] = [];
218269
const exportedUnknowns: Record<string, string> = {};
219270
const declaredClasses: Record<string, ClassDeclaration> = {};
220271
const declaredInterfaces: Record<string, TSInterfaceDeclaration> = {};
221-
const importedElements: Record<string, { localName: string; fileName: string }> = {};
272+
const importedElements: Record<string, ClassReference> = {};
222273

223274
for (const statement of ast.body) {
224275
if (statement.type === AST_NODE_TYPES.ExportNamedDeclaration) {
@@ -240,7 +291,7 @@ export class ClassLoader {
240291
for (const specifier of statement.specifiers) {
241292
exportedImportedElements[specifier.exported.name] = {
242293
localName: specifier.local.name,
243-
fileName: this.importTargetToAbsolutePath(fileName, statement.source.value),
294+
...this.importTargetToAbsolutePath(packageName, fileName, statement.source.value),
244295
};
245296
}
246297
} else {
@@ -254,7 +305,7 @@ export class ClassLoader {
254305
if (statement.source &&
255306
statement.source.type === AST_NODE_TYPES.Literal &&
256307
typeof statement.source.value === 'string') {
257-
exportedImportedAll.push(this.importTargetToAbsolutePath(fileName, statement.source.value));
308+
exportedImportedAll.push(this.importTargetToAbsolutePath(packageName, fileName, statement.source.value));
258309
}
259310
} else if (statement.type === AST_NODE_TYPES.ClassDeclaration && statement.id) {
260311
// Form: `declare class A {}`
@@ -270,7 +321,7 @@ export class ClassLoader {
270321
if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
271322
importedElements[specifier.local.name] = {
272323
localName: specifier.imported.name,
273-
fileName: this.importTargetToAbsolutePath(fileName, statement.source.value),
324+
...this.importTargetToAbsolutePath(packageName, fileName, statement.source.value),
274325
};
275326
}
276327
}
@@ -303,15 +354,15 @@ export interface ClassElements {
303354
// Interfaces that have been declared in a file via `export interface A`
304355
exportedInterfaces: Record<string, TSInterfaceDeclaration>;
305356
// Elements that have been exported via `export { A as B } from "b"`
306-
exportedImportedElements: Record<string, { localName: string; fileName: string }>;
357+
exportedImportedElements: Record<string, ClassReference>;
307358
// Exports via `export * from "b"`
308-
exportedImportedAll: string[];
359+
exportedImportedAll: { packageName: string; fileName: string }[];
309360
// Things that have been exported via `export {A as B}`, where the target is not known
310361
exportedUnknowns: Record<string, string>;
311362
// Classes that have been declared in a file via `declare class A`
312363
declaredClasses: Record<string, ClassDeclaration>;
313364
// Interfaces that have been declared in a file via `declare interface A`
314365
declaredInterfaces: Record<string, TSInterfaceDeclaration>;
315366
// Elements that are imported from elsewhere via `import {A} from ''`
316-
importedElements: Record<string, { localName: string; fileName: string }>;
367+
importedElements: Record<string, ClassReference>;
317368
}

lib/parse/ParameterLoader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
TSTypeReference,
1010
} from '@typescript-eslint/types/dist/ts-estree';
1111
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
12-
import type { ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
12+
import type { ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
1313
import type { CommentData } from './CommentLoader';
1414
import { CommentLoader } from './CommentLoader';
1515
import type { ConstructorData } from './ConstructorLoader';
@@ -417,7 +417,7 @@ export type ParameterRangeResolved = {
417417
value: string;
418418
} | {
419419
type: 'class';
420-
value: ClassReference;
420+
value: ClassReferenceLoaded;
421421
} | {
422422
type: 'nested';
423423
value: ParameterData<ParameterRangeResolved>[];

lib/parse/ParameterResolver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ export class ParameterResolver {
100100
*/
101101
public async resolveRangeInterface(
102102
interfaceName: string,
103-
owningClass: ClassReference,
103+
owningClass: ClassReferenceLoaded,
104104
): Promise<ParameterRangeResolved> {
105105
const classOrInterface = await this.loadClassOrInterfacesChain({
106+
packageName: owningClass.packageName,
106107
localName: interfaceName,
107108
fileName: owningClass.fileName,
108109
});
@@ -160,6 +161,7 @@ export class ParameterResolver {
160161
.filter(interfaceName => !(interfaceName in this.ignoreClasses))
161162
.map(async interfaceName => {
162163
const superInterface = await this.loadClassOrInterfacesChain({
164+
packageName: classOrInterface.packageName,
163165
localName: interfaceName,
164166
fileName: classOrInterface.fileName,
165167
});

0 commit comments

Comments
 (0)