-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathwebpackLoader.js
281 lines (220 loc) · 8.22 KB
/
webpackLoader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const t = require("@babel/types");
module.exports = function myLoader(source) {
// app.module.ts passed in as source parameter
// import passed in options
const options = this.getOptions();
// object destructure the customComponents array
const { customComponents } = options;
const fileNames = []
const imports = []
customComponents.forEach((ComponentClass, index) => {
const className = ComponentClass.name
fileNames.push(className)
const fileName = `customComponent${index + 1}.ts`; // Generate a unique filename
imports.push(fileName)
// Create the file path using the current working directory and the filename
const filePath = path.resolve(process.cwd(), fileName); // change process.cwd() to generate files into node modules?
// Generate the code for the Angular class component
const componentCode = generateAngularComponent(ComponentClass);
// Write the code to the file
fs.writeFileSync(filePath, componentCode);
});
// read and stringify app.module file
// const code = fs.readFileSync("./src/app/app.module.ts").toString();
// generate ast for app.module file
const ast = parser.parse(source, {
sourceType: "module",
plugins: ["typescript", "decorators-legacy"],
});
let modified = false; // Flag to track modification
let modifiedNgModule = null; // Store the NgModule decorator node that is modified
const importedClassNames = fileNames; // Get the imported class names from the fileNames array
const existingClassNames = new Set()
// traversal through ast of app.module file
traverse(ast, {
Decorator(path) {
// identify where new declarations will be added
// find the NgModule object
if (
t.isCallExpression(path.node.expression) &&
t.isIdentifier(path.node.expression.callee, { name: 'NgModule' }) &&
!modified // Check if modification has not been applied yet
) {
const ngModuleArg = path.node.expression.arguments[0];
// find the declarations array
if (t.isObjectExpression(ngModuleArg)) {
const declarationsProp = ngModuleArg.properties.find((prop) =>
t.isIdentifier(prop.key, { name: 'declarations' })
);
if (
declarationsProp &&
t.isArrayExpression(declarationsProp.value)
) {
for(let i = 0; i < declarationsProp.value.elements.slice(-importedClassNames.length).length; i++){
existingClassNames.add(declarationsProp.value.elements.slice(-importedClassNames.length)[i].name)
}
// Create an identifier for each imported class and add to the declarations array
importedClassNames.forEach((className) => {
if(!existingClassNames.has(className)){
const importedClassIdentifier = t.identifier(className);
declarationsProp.value.elements.push(importedClassIdentifier);
}
// check whether each className already exists in the declarations array
});
modified = true; // Set the flag to indicate modification
modifiedNgModule = path.node; // Mark the NgModule decorator as modified
}
}
}
},
ImportDeclaration(path) {
// identify where new import declarations will be inserted
if (
t.isStringLiteral(path.node.source, { value: './app.component' }) &&
!modifiedNgModule // Skip further traversal if NgModule is already modified
) {
let counter = 1 // initialize counter to act as input for file name
importedClassNames.forEach((className) => {
if(!path.scope.bindings[className]){
// Create an import specifier for the class name
const importSpecifier = t.importSpecifier(
t.identifier(className),
t.identifier(className)
);
// Create a new import declaration for the class name
const newImportDeclaration = t.importDeclaration(
[importSpecifier],
t.stringLiteral(`../../customComponent${counter}`)
);
counter++
// Insert the new import declaration after the existing one
path.insertAfter(newImportDeclaration);
}
});
}
},
});
const newCode = `${generator(ast).code}`;
// update app.module.ts with new updated code
fs.writeFileSync("./src/app/app.module.ts", newCode)
return 'done';
};
function generateAngularComponent(ComponentClass) {
const className = ComponentClass.name;
// Get the class methods
const methods = Object.getOwnPropertyNames(ComponentClass.prototype);
// Create an instance of each component class
const instance = new ComponentClass();
// identify the html code provided by the user
const html = instance.template;
// Generate the code for the Angular class component
const componentCode = `
import { Component } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: '${toKebabCase(className)}',
template: \`${generateTemplate(html)}\`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: ${className}
}
]
})
export class ${className} implements ControlValueAccessor {
${generateProperties(instance)}
${generateMethods(ComponentClass.prototype, methods)}
writeValue(value: any) {
this.value = value
}
registerOnChange(onChange: any) {
this.onChange = onChange
}
registerOnTouched(onTouched: any){
this.onTouched = onTouched
}
setDisabledState(disabled: boolean): void {
this.disabled = disabled
}
}
`;
return componentCode;
}
// generate all methods to be added to new component
function generateMethods(instance, methods) {
const typeScript = ': any'
return (
methods.slice(1)
// filter through methods return only functions
// iterate through methods and add the function to the new component in the right format
.map((method) => {
const functionCode = instance[method].toString();
const position = functionCode.indexOf(')')
const params = functionCode.substring(functionCode.indexOf('('), position+1)
// if there are multiple parameters add : any to other parameters
if(functionCode[position - 1].match(/[A-Z]|[a-z]/g)){
return functionCode.replace(params, typescriptIfy(params))
} else {
return functionCode
}
})
.join("\n")
);
}
// add ': any' to any number of parameters
const typescriptIfy = (functionCode, result = '', typescript = ': any') => {
if(!functionCode.length){
return result
}
if(functionCode[0] === ',' || functionCode[0] === ')'){
return typescriptIfy(functionCode.slice(1), result += (typescript + functionCode[0]), typescript)
}
return typescriptIfy(functionCode.slice(1), result += functionCode[0], typescript)
}
// generate html to be added to new component
function generateTemplate(html) {
return `
${html}
`;
}
// kebab (change CustomComponent to custom-component)
function toKebabCase(str) {
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
// generate all properties to be added to new component
function generateProperties(instance) {
const typeScript = ': any'
const properties = Object.entries(instance)
// iterate through the class object and add the properties to the new component
const newProps = properties.filter((el) => el[0] !== 'template').map((el) => {
if(el[0].toString() === 'onChange'){
const position = el[1].toString().indexOf(')')
return `${el[0]} = ${[el[1].toString().slice(0, position), typeScript, el[1].toString().slice(position)].join('')}`;
}
return `${el[0]} = ${formatValue(el[1])}`;
}).join("\n")
return newProps
}
// format values of properties so that they are added in number, boolean, or string
function formatValue(value) {
if (typeof value === "string") {
return `'${value}'`;
} else {
return value;
}
}
// module.exports = {
// formatValue,
// generateProperties,
// toKebabCase,
// generateTemplate,
// typescriptIfy,
// generateMethods,
// generateAngularComponent
// };