Skip to content

Commit ea1028b

Browse files
committed
Allow classes to be ignored via JSON file, Closes #40
1 parent 4336ed9 commit ea1028b

7 files changed

Lines changed: 112 additions & 31 deletions

File tree

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,31 @@ Generates component file for a package
103103
Usage:
104104
componentsjs-generator
105105
Options:
106-
-p path/to/package The directory of the package to look in, defaults to working directory
107-
-s lib Relative path to directory containing source files, defaults to 'lib'
108-
-c components Relative path to directory that will contain components files, defaults to 'components'
109-
-e jsonld Extension for components files (without .), defaults to 'jsonld'
110-
--help Show information about this command
106+
-p path/to/package The directory of the package to look in, defaults to working directory
107+
-s lib Relative path to directory containing source files, defaults to 'lib'
108+
-c components Relative path to directory that will contain components files, defaults to 'components'
109+
-e jsonld Extension for components files (without .), defaults to 'jsonld'
110+
-i ignore-classes.json Relative path to an optional file with class names to ignore
111+
--help Show information about this command
111112
```
112113

113114
**Note:** This generator will read `.d.ts` files,
114115
so it is important that you invoke the TypeScript compiler (`tsc`) _before_ using this tool.
115116

117+
### Ignoring classes
118+
119+
If you don't want components to be generated for certain classes,
120+
then you can pass a JSON file to the `-i` option containing an array of class names to skip.
121+
122+
For example, invoking `componentsjs-generator -i ignore-classes.json` will skip `BadClass` if the contents of `ignore-classes.json` are:
123+
```json
124+
[
125+
"BadClass"
126+
]
127+
```
128+
129+
If you are looking for a way to ignore parameters, see the `@ignored` argument tag below.
130+
116131
## How it works
117132

118133
For each exported TypeScript class,

bin/componentsjs-generator.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env node
2+
import * as fs from 'fs';
23
import * as minimist from 'minimist';
34
import { Generator } from '../lib/generate/Generator';
45
import { ResolutionContext } from '../lib/resolution/ResolutionContext';
@@ -8,11 +9,12 @@ function showHelp(): void {
89
Usage:
910
componentsjs-generator
1011
Options:
11-
-p path/to/package The directory of the package to look in, defaults to working directory
12-
-s lib Relative path to directory containing source files, defaults to 'lib'
13-
-c components Relative path to directory that will contain components files, defaults to 'components'
14-
-e jsonld Extension for components files (without .), defaults to 'jsonld'
15-
--help Show information about this command
12+
-p path/to/package The directory of the package to look in, defaults to working directory
13+
-s lib Relative path to directory containing source files, defaults to 'lib'
14+
-c components Relative path to directory that will contain components files, defaults to 'components'
15+
-e jsonld Extension for components files (without .), defaults to 'jsonld'
16+
-i ignore-classes.json Relative path to an optional file with class names to ignore
17+
--help Show information about this command
1618
`);
1719
process.exit(1);
1820
}
@@ -30,6 +32,13 @@ if (args.help) {
3032
},
3133
fileExtension: args.e || 'jsonld',
3234
level: args.l || 'info',
35+
ignoreClasses: args.i ?
36+
// eslint-disable-next-line no-sync
37+
JSON.parse(fs.readFileSync(args.i, 'utf8')).reduce((acc: Record<string, boolean>, entry: string) => {
38+
acc[entry] = true;
39+
return acc;
40+
}, {}) :
41+
[],
3342
});
3443
generator
3544
.generateComponents()

lib/generate/Generator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export class Generator {
1818
private readonly resolutionContext: ResolutionContext;
1919
private readonly pathDestination: PathDestinationDefinition;
2020
private readonly fileExtension: string;
21+
private readonly ignoreClasses: Record<string, boolean>;
2122

2223
public constructor(args: GeneratorArgs) {
2324
this.resolutionContext = args.resolutionContext;
2425
this.pathDestination = args.pathDestination;
2526
this.fileExtension = args.fileExtension;
27+
this.ignoreClasses = args.ignoreClasses;
2628
}
2729

2830
public async generateComponents(): Promise<void> {
@@ -32,7 +34,7 @@ export class Generator {
3234

3335
const classLoader = new ClassLoader({ resolutionContext: this.resolutionContext });
3436
const classFinder = new ClassFinder({ classLoader });
35-
const classIndexer = new ClassIndexer({ classLoader, classFinder });
37+
const classIndexer = new ClassIndexer({ classLoader, classFinder, ignoreClasses: this.ignoreClasses });
3638

3739
// Find all relevant classes
3840
const packageExports = await classFinder.getPackageExports(packageMetadata.typesPath);
@@ -77,4 +79,5 @@ export interface GeneratorArgs {
7779
pathDestination: PathDestinationDefinition;
7880
fileExtension: string;
7981
level: string;
82+
ignoreClasses: Record<string, boolean>;
8083
}

lib/parse/ClassIndexer.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,14 @@ import type { ClassIndex, ClassLoaded, ClassReference } from './ClassIndex';
66
import type { ClassLoader } from './ClassLoader';
77

88
export class ClassIndexer {
9-
/**
10-
* Errors that do not require an import, and are assumed to be known globally.
11-
*/
12-
private static readonly SUPERCLASS_BLACKLIST: Record<string, boolean> = {
13-
Error: true,
14-
};
15-
169
private readonly classLoader: ClassLoader;
1710
private readonly classFinder: ClassFinder;
11+
private readonly ignoreClasses: Record<string, boolean>;
1812

1913
public constructor(args: ClassIndexerArgs) {
2014
this.classLoader = args.classLoader;
2115
this.classFinder = args.classFinder;
16+
this.ignoreClasses = args.ignoreClasses;
2217
}
2318

2419
/**
@@ -29,7 +24,9 @@ export class ClassIndexer {
2924
const classIndex: ClassIndex<ClassLoaded> = {};
3025

3126
for (const [ className, classReference ] of Object.entries(classReferences)) {
32-
classIndex[className] = await this.loadClassChain(classReference);
27+
if (!(className in this.ignoreClasses)) {
28+
classIndex[className] = await this.loadClassChain(classReference);
29+
}
3330
}
3431

3532
return classIndex;
@@ -47,11 +44,15 @@ export class ClassIndexer {
4744
// If the class has a super class, load it recursively
4845
const superClassName = this.classLoader.getSuperClassName(classReferenceLoaded.declaration,
4946
classReferenceLoaded.fileName);
50-
if (superClassName && !(superClassName in ClassIndexer.SUPERCLASS_BLACKLIST)) {
51-
classReferenceLoaded.superClass = await this.loadClassChain({
52-
localName: superClassName,
53-
fileName: classReferenceLoaded.fileName,
54-
});
47+
if (superClassName && !(superClassName in this.ignoreClasses)) {
48+
try {
49+
classReferenceLoaded.superClass = await this.loadClassChain({
50+
localName: superClassName,
51+
fileName: classReferenceLoaded.fileName,
52+
});
53+
} catch (error: unknown) {
54+
throw new Error(`Failed to load super class ${superClassName} of ${classReference.localName} in ${classReference.fileName}:\n${(<Error> error).message}`);
55+
}
5556
}
5657

5758
return classReferenceLoaded;
@@ -61,4 +62,5 @@ export class ClassIndexer {
6162
export interface ClassIndexerArgs {
6263
classLoader: ClassLoader;
6364
classFinder: ClassFinder;
65+
ignoreClasses: Record<string, boolean>;
6466
}

lib/parse/ClassLoader.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,14 @@ export class ClassLoader {
6565
*/
6666
public async loadClassDeclaration<CI extends boolean>(classReference: ClassReference, considerInterfaces: CI):
6767
Promise<CI extends true ? (ClassLoaded | InterfaceLoaded) : ClassLoaded> {
68-
const ast = await this.resolutionContext.parseTypescriptFile(classReference.fileName);
68+
// Load the class as an AST
69+
let ast;
70+
try {
71+
ast = await this.resolutionContext.parseTypescriptFile(classReference.fileName);
72+
} catch (error: unknown) {
73+
throw new Error(`Could not load ${considerInterfaces ? 'class or interface' : 'class'} ${classReference.localName} from ${classReference.fileName}:\n${(<Error> error).message}`);
74+
}
75+
6976
const {
7077
exportedClasses,
7178
exportedInterfaces,
@@ -188,6 +195,16 @@ export class ClassLoader {
188195
return this.getClassElements(fileName, ast);
189196
}
190197

198+
/**
199+
* Convert the given import path to an absolute file path.
200+
* @param currentFilePath Absolute path to a file in which the import path occurs.
201+
* @param importPath Possibly relative path that is being imported.
202+
*/
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);
206+
}
207+
191208
/**
192209
* Get all class elements in a file.
193210
* @param fileName A file path.
@@ -223,7 +240,7 @@ export class ClassLoader {
223240
for (const specifier of statement.specifiers) {
224241
exportedImportedElements[specifier.exported.name] = {
225242
localName: specifier.local.name,
226-
fileName: Path.join(Path.dirname(fileName), statement.source.value),
243+
fileName: this.importTargetToAbsolutePath(fileName, statement.source.value),
227244
};
228245
}
229246
} else {
@@ -237,7 +254,7 @@ export class ClassLoader {
237254
if (statement.source &&
238255
statement.source.type === AST_NODE_TYPES.Literal &&
239256
typeof statement.source.value === 'string') {
240-
exportedImportedAll.push(Path.join(Path.dirname(fileName), statement.source.value));
257+
exportedImportedAll.push(this.importTargetToAbsolutePath(fileName, statement.source.value));
241258
}
242259
} else if (statement.type === AST_NODE_TYPES.ClassDeclaration && statement.id) {
243260
// Form: `declare class A {}`
@@ -253,7 +270,7 @@ export class ClassLoader {
253270
if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
254271
importedElements[specifier.local.name] = {
255272
localName: specifier.imported.name,
256-
fileName: Path.join(Path.dirname(fileName), statement.source.value),
273+
fileName: this.importTargetToAbsolutePath(fileName, statement.source.value),
257274
};
258275
}
259276
}

test/parse/ClassIndexer.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import { ResolutionContextMocked } from '../ResolutionContextMocked';
55

66
describe('ClassIndexer', () => {
77
const resolutionContext = new ResolutionContextMocked({});
8+
let ignoreClasses: Record<string, boolean>;
89
let classLoader: ClassLoader;
910
let classFinder: ClassFinder;
1011
let indexer: ClassIndexer;
1112

1213
beforeEach(() => {
14+
ignoreClasses = {};
1315
classLoader = new ClassLoader({ resolutionContext });
1416
classFinder = new ClassFinder({ classLoader });
15-
indexer = new ClassIndexer({ classLoader, classFinder });
17+
indexer = new ClassIndexer({ classLoader, classFinder, ignoreClasses });
1618
});
1719

1820
describe('createIndex', () => {
@@ -42,6 +44,26 @@ describe('ClassIndexer', () => {
4244
});
4345
});
4446

47+
it('should throw on a direct class reference to an unknown file', async() => {
48+
await expect(indexer.createIndex({
49+
Unknown: {
50+
localName: 'Unknown',
51+
fileName: 'unknown',
52+
},
53+
})).rejects.toThrow(new Error(`Could not load class Unknown from unknown:
54+
Could not find mocked path for unknown.d.ts`));
55+
});
56+
57+
it('should not throw on a direct class reference to an unknown file when it is ignored', async() => {
58+
ignoreClasses.Unknown = true;
59+
expect(await indexer.createIndex({
60+
Unknown: {
61+
localName: 'Unknown',
62+
fileName: 'unknown',
63+
},
64+
})).toMatchObject({});
65+
});
66+
4567
it('should load an indirect class reference', async() => {
4668
resolutionContext.contentsOverrides = {
4769
'x.d.ts': `export * from './y'`,
@@ -285,10 +307,22 @@ export * from './Z'
285307
});
286308
});
287309

288-
it('for an exported class extending Error should ignore the link', async() => {
310+
it('for an exported class extending an unknown class should error', async() => {
311+
resolutionContext.contentsOverrides = {
312+
'file.d.ts': `
313+
export class A extends Unknown{}
314+
`,
315+
};
316+
await expect(indexer.loadClassChain({ localName: 'A', fileName: 'file' }))
317+
.rejects.toThrow(new Error(`Failed to load super class Unknown of A in file:
318+
Could not load class Unknown from file`));
319+
});
320+
321+
it('for an exported class extending an unknown class should not error if it is ignored', async() => {
322+
ignoreClasses.Unknown = true;
289323
resolutionContext.contentsOverrides = {
290324
'file.d.ts': `
291-
export class A extends Error{}
325+
export class A extends Unknown{}
292326
`,
293327
};
294328
expect(await indexer.loadClassChain({ localName: 'A', fileName: 'file' }))

test/parse/ConstructorLoader.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('ConstructorLoader', () => {
1515
classIndexer = new ClassIndexer({
1616
classLoader,
1717
classFinder: new ClassFinder({ classLoader }),
18+
ignoreClasses: {},
1819
});
1920
});
2021

0 commit comments

Comments
 (0)