Skip to content

Commit 58a460f

Browse files
artlowelnona-luypaertalexandrevryghem
committed
130715: Created generate-decorator-registries script to generator decorator maps
Co-authored-by: Nona Luypaert <nona.luypaert@atmire.com> Co-authored-by: Alexandre Vryghem <alexandre@atmire.com>
1 parent bcc437d commit 58a460f

9 files changed

Lines changed: 381 additions & 1 deletion

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ yarn-error.log
4242
junit.xml
4343

4444
/src/mirador-viewer/config.local.js
45+
46+
## ignore the auto-generated decorator registries
47+
decorator-registries/

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
1414
"serve:ssr": "node dist/server/main",
1515
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
16+
"generate:decorator:registries": "ts-node --project ./tsconfig.ts-node.json scripts/generate-decorator-registries.ts",
1617
"build": "ng build --configuration development",
1718
"build:stats": "ng build --stats-json",
1819
"build:prod": "cross-env NODE_ENV=production npm run build:ssr",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { DecoratorParam } from './decorator-param.interface';
2+
3+
/**
4+
* The configuration of dynamic component decorators. This is used to generate the registry files.
5+
*/
6+
export interface DecoratorConfig {
7+
name: string;
8+
params: DecoratorParam[];
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* The configuration of a parameter from a decorator
3+
*/
4+
export interface DecoratorParam {
5+
/**
6+
* The name of the parameter
7+
*/
8+
name: string;
9+
10+
/**
11+
* The default value if any of the decorator param
12+
*/
13+
default?: string;
14+
15+
/**
16+
* The property of the provided value that should be used instead to generate the Map. So, for example, if the
17+
* decorator value is a {@link ResourceType}, you may want to use the `ResourceType.value` instead of the whole
18+
* {@link ResourceType} object. In this case the {@link DecoratorParam#property} would be `value`.
19+
*/
20+
property?: string;
21+
}
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import {
2+
existsSync,
3+
mkdirSync,
4+
readdirSync,
5+
readFileSync,
6+
rmSync,
7+
writeFileSync,
8+
} from 'fs';
9+
import { sync } from 'glob';
10+
import {
11+
basename,
12+
dirname,
13+
join,
14+
relative,
15+
resolve,
16+
} from 'path';
17+
import {
18+
createSourceFile,
19+
forEachChild,
20+
getDecorators,
21+
Identifier,
22+
ImportDeclaration,
23+
isCallExpression,
24+
isClassDeclaration,
25+
isEnumDeclaration,
26+
isExpressionWithTypeArguments,
27+
isIdentifier,
28+
isPropertyAccessExpression,
29+
isStringLiteral,
30+
ScriptTarget,
31+
StringLiteral,
32+
SyntaxKind,
33+
} from 'typescript';
34+
35+
import { DECORATORS } from '../src/app/decorators';
36+
import { DecoratorConfig } from './config/decorator-config.interface';
37+
38+
const COMPONENTS_DIR = resolve(__dirname, '../src');
39+
const REGISTRY_OUTPUT_DIR = resolve(__dirname, '../src/decorator-registries');
40+
41+
/**
42+
* Scans the code base for enums and extracts their values.
43+
*
44+
* @returns A nested map of the enums and their values.
45+
*/
46+
const generateEnumValues = () => {
47+
const enumValues = {};
48+
49+
const fileNames = sync(`${COMPONENTS_DIR}/**/*.ts`, { ignore: `${COMPONENTS_DIR}/**/*.spec.ts` });
50+
51+
fileNames.forEach((filePath: string) => {
52+
const fileName = basename(filePath);
53+
const sourceFile = createSourceFile(fileName, readFileSync(filePath, 'utf8'), ScriptTarget.Latest);
54+
55+
if (!sourceFile.isDeclarationFile) {
56+
forEachChild(sourceFile, node => {
57+
if (isEnumDeclaration(node)) {
58+
const enumName = node.name.text;
59+
enumValues[enumName] = {};
60+
61+
for (const value of node.members) {
62+
const valueName = value.name.getText(sourceFile);
63+
if (value.initializer && isStringLiteral(value.initializer)) {
64+
enumValues[enumName][valueName] = value.initializer.text;
65+
}
66+
}
67+
}
68+
});
69+
}
70+
});
71+
72+
return enumValues;
73+
};
74+
75+
const enumValues = generateEnumValues();
76+
77+
const getDecoratorConstName = (decorator: string): string => {
78+
return decorator
79+
.replace(/([A-Z])/g, '_$1')
80+
.toUpperCase()
81+
.replace(/^_/, '');
82+
};
83+
84+
const getDecoratorFileName = (decorator: string): string => {
85+
return decorator
86+
.replace(/([A-Z])/g, '-$1')
87+
.toLowerCase()
88+
.replace(/^-/, '')
89+
.concat('-registry.ts');
90+
};
91+
92+
/**
93+
* Generates and writes a registry TypeScript file for decorator components.
94+
*
95+
* @param decoratorConfig - Decorator configuration that's currently being processed.
96+
* @param {Array<{ name: string, filePath: string, args: any[], imports: Map<string, string> }>} components - An array of objects, each representing a component.
97+
*
98+
* @returns {void} This function does not return a value. It writes a file to the output directory.
99+
*/
100+
const writeRegistryFile = (
101+
decoratorConfig: DecoratorConfig,
102+
components: Array<{ name: string, filePath: string, args: any[], imports: Map<string, string> }>,
103+
): void => {
104+
const mapName = getDecoratorConstName(decoratorConfig.name) + '_MAP';
105+
const functionName = `${decoratorConfig.name}CreateMap`;
106+
const mapVarName = `${decoratorConfig.name}Map`;
107+
let content = '';
108+
109+
const imports: Map<string, Set<string>> = new Map();
110+
components.forEach(component => {
111+
for (const componentImport of component.imports.keys()) {
112+
const importPath: string = component.imports.get(componentImport);
113+
if (!imports.get(importPath)) {
114+
imports.set(importPath, new Set());
115+
}
116+
imports.get(importPath).add(componentImport);
117+
}
118+
});
119+
120+
if (imports.size > 0) {
121+
content += `${ Array.from(imports.keys()).sort().map((path: string) => `import { ${Array.from(imports.get(path)).join(', ')} } from '${path}';`).join('\n')}\n\n`;
122+
}
123+
124+
content += `function ${functionName}(): Map<any, any> {\n`;
125+
content += ` const ${mapVarName} = new Map();\n\n`;
126+
127+
const mapPathsSoFar = new Set<string>();
128+
129+
for (const component of components) {
130+
const argsArray = decoratorConfig.params.map((param, index) => (index < component.args.length && component.args[index] !== undefined) ? component.args[index] : param.default);
131+
132+
let currentMapPath = mapVarName;
133+
let currentPathKey = '';
134+
135+
for (let i = 0; i < argsArray.length - 1; i++) {
136+
const key = argsArray[i];
137+
let keyString: string;
138+
if (typeof key === 'string' && key.includes('${')) {
139+
keyString = `\`${key}\``;
140+
} else if (typeof key === 'string') {
141+
keyString = `'${key.replace(/'/g, '\\\'')}'`;
142+
} else if (key && typeof key === 'object' && 'classRef' in key) {
143+
const param = decoratorConfig.params[argsArray.length - 1];
144+
if (param.property) {
145+
keyString = `${key.classRef}.${param.property}`;
146+
} else {
147+
keyString = key.classRef;
148+
}
149+
} else {
150+
keyString = String(key);
151+
}
152+
153+
const newPath = currentPathKey + '|' + (typeof key === 'object' && 'classRef' in key ? (key.classRef ? key.classRef : key) : String(key));
154+
155+
if (!mapPathsSoFar.has(newPath)) {
156+
content += ` ${currentMapPath}.set(${keyString}, new Map());\n`;
157+
mapPathsSoFar.add(newPath);
158+
}
159+
currentMapPath += `.get(${keyString})`;
160+
currentPathKey = newPath;
161+
}
162+
163+
const finalKey = argsArray[argsArray.length - 1];
164+
let finalKeyString: string;
165+
if (typeof finalKey === 'string' && finalKey.includes('${')) {
166+
finalKeyString = `\`${finalKey}\``;
167+
} else if (typeof finalKey === 'string') {
168+
finalKeyString = `'${finalKey.replace(/'/g, '\\\'')}'`;
169+
} else if (finalKey && typeof finalKey === 'object' && 'classRef' in finalKey) {
170+
const param = decoratorConfig.params[argsArray.length - 1];
171+
if (param.property) {
172+
finalKeyString = `${finalKey.classRef}.${param.property}`;
173+
} else {
174+
finalKeyString = finalKey.classRef;
175+
}
176+
} else {
177+
finalKeyString = String(finalKey);
178+
}
179+
180+
const lazyImport = `() => import('${component.filePath}').then(c => c.${component.name})`;
181+
content += ` ${currentMapPath}.set(${finalKeyString}, ${lazyImport});\n`;
182+
}
183+
184+
content += `\n return ${mapVarName};\n`;
185+
content += `}\n\n`;
186+
content += `export const ${mapName} = ${functionName}();\n`;
187+
188+
189+
const filePath = join(REGISTRY_OUTPUT_DIR, getDecoratorFileName(decoratorConfig.name));
190+
if (!existsSync(filePath) || readFileSync(filePath, 'utf8') !== content) {
191+
writeFileSync(filePath, content, 'utf8');
192+
}
193+
};
194+
195+
const generateRegistries = (
196+
decoratorConfigs: DecoratorConfig[],
197+
): Map<string, Array<{ name: string, filePath: string, args: any[], imports: Map<string, string> }>> => {
198+
// Initialize the map using decorator names as keys, and empty lists as values
199+
const decoratorMap = new Map<string, Array<{ name: string, filePath: string, args: any[], imports: Map<string, string> }>>();
200+
decoratorConfigs.forEach(config => {
201+
decoratorMap.set(config.name, []);
202+
});
203+
204+
// Get all TypeScript files recursively, excluding spec files
205+
const fileNames = sync(`${COMPONENTS_DIR}/**/*.ts`, { ignore: `${COMPONENTS_DIR}/**/*.spec.ts` });
206+
207+
fileNames.forEach((filePath: string) => {
208+
const fileName = basename(filePath);
209+
const sourceFile = createSourceFile(fileName, readFileSync(filePath, 'utf8'), ScriptTarget.Latest);
210+
211+
// The key of the map is the import name, and the value is the path
212+
const imports: Map<string, string> = new Map();
213+
forEachChild(sourceFile, (node: ImportDeclaration) => {
214+
if (node.kind === SyntaxKind.ImportDeclaration && node.importClause?.namedBindings?.kind === SyntaxKind.NamedImports) {
215+
node.importClause.namedBindings.elements.forEach(element => {
216+
imports.set(element.name.text, (node.moduleSpecifier as StringLiteral).text);
217+
});
218+
}
219+
});
220+
221+
// Walk the AST to find class declarations with decorators
222+
forEachChild(sourceFile, node => {
223+
if (isClassDeclaration(node) && node.name) {
224+
const decorators = getDecorators(node);
225+
const componentName = node.name.text;
226+
227+
decorators?.forEach((decorator) => {
228+
if (isCallExpression(decorator.expression)) {
229+
const currentDecoratorName = (decorator.expression.expression as Identifier).text;
230+
231+
const decoratorConfig = decoratorConfigs.find(config => config.name === currentDecoratorName);
232+
if (decoratorConfig) {
233+
const args: any[] = [];
234+
const argImports: Map<string, string> = new Map();
235+
decorator.expression.arguments.forEach((arg) => {
236+
// e.g. @decorator('range')
237+
if (isStringLiteral(arg)) {
238+
args.push(arg.text);
239+
// e.g. @decorator(ItemSearchResult)
240+
} else if (isIdentifier(arg)) {
241+
args.push({ classRef: arg.text });
242+
243+
if (imports.has(arg.text)) {
244+
let absoluteImportPath = imports.get(arg.text);
245+
if (!absoluteImportPath.includes('src/app')) {
246+
absoluteImportPath = resolve(dirname(filePath), imports.get(arg.text));
247+
}
248+
const newRelativePath = relative(REGISTRY_OUTPUT_DIR, absoluteImportPath);
249+
argImports.set(arg.text, newRelativePath);
250+
}
251+
// e.g. @decorator(Enum.property)
252+
} else if (isPropertyAccessExpression(arg)) {
253+
const propertyName = arg.name.text;
254+
const objectName = (arg.expression as Identifier).text;
255+
const enumValue = enumValues[objectName]?.[propertyName];
256+
args.push(enumValue || `${objectName}.${propertyName}`);
257+
// e.g. @decorator(PaginatedList<AdminNotifySearchResult>)
258+
} else if (isExpressionWithTypeArguments(arg)) {
259+
args.push(arg.typeArguments[0].getText(sourceFile));
260+
} else if (arg.kind === SyntaxKind.TrueKeyword) {
261+
args.push(true);
262+
} else if (arg.kind === SyntaxKind.FalseKeyword) {
263+
args.push(false);
264+
}
265+
});
266+
267+
// Add to the map under the current decorator's name
268+
decoratorMap.get(currentDecoratorName)?.push({
269+
name: componentName,
270+
filePath: `../${relative(COMPONENTS_DIR, filePath).replace(/\.ts$/, '')}`,
271+
args,
272+
imports: argImports,
273+
});
274+
}
275+
}
276+
});
277+
}
278+
});
279+
});
280+
281+
return decoratorMap;
282+
};
283+
284+
const main = (): void => {
285+
mkdirSync(REGISTRY_OUTPUT_DIR, { recursive: true });
286+
const registriesToDelete: Set<string> = new Set(readdirSync(REGISTRY_OUTPUT_DIR));
287+
288+
// Generate map with decorator names as keys, lists of component metadata objects as values
289+
// 1 component metadata object contains: the name of the component, the full path of
290+
// the component file, and the arguments used in the decorator on that component
291+
const decoratorMap = generateRegistries(DECORATORS);
292+
293+
// Write registry files for each decorator
294+
DECORATORS.forEach(decoratorConfig => {
295+
registriesToDelete.delete(getDecoratorFileName(decoratorConfig.name));
296+
const componentsForDecorator = decoratorMap.get(decoratorConfig.name);
297+
if (componentsForDecorator && componentsForDecorator.length > 0) {
298+
writeRegistryFile(decoratorConfig, componentsForDecorator);
299+
} else {
300+
console.warn(`No components found for decorator '${decoratorConfig.name}'`);
301+
}
302+
});
303+
304+
registriesToDelete.forEach((fileName: string) => rmSync(join(REGISTRY_OUTPUT_DIR, fileName)));
305+
306+
console.debug(`Generated decorator registry files in ${REGISTRY_OUTPUT_DIR}`);
307+
};
308+
309+
main();

src/app/core/shared/context.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
export enum Context {
66
/** Default context */
7-
Any = 'undefined',
7+
Any = '*',
88

99
/** General item page context */
1010
ItemPage = 'itemPage',

src/app/decorators.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { DecoratorConfig } from '../../scripts/config/decorator-config.interface';
2+
3+
/**
4+
* This list contains the dynamic components decorator configuration that will be used to generate the registry files
5+
* in `src/decorator-registries`.
6+
*
7+
* If you want to create a new decorator, you need to extend this list and add your custom decorator to it. Afterwards
8+
* a registry file will be generated inside the `src/decorator-registries` folder, which exports the Map that you can
9+
* then use this inside your decorator file to dynamically retrieve the desired component.
10+
*/
11+
export const DECORATORS: DecoratorConfig[] = [
12+
];

0 commit comments

Comments
 (0)