scss-variable-extractor 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -12
- package/bin/cli.js +395 -9
- package/package.json +6 -2
- package/src/angular-parser.js +381 -0
- package/src/bootstrap-migrator.js +602 -0
- package/src/config.js +11 -2
- package/src/index.js +11 -1
- package/src/ng-refactorer.js +578 -0
- package/test/angular-parser.test.js +230 -0
- package/test/bootstrap-migrator.test.js +175 -0
- package/test/fixtures/angular.json +123 -0
- package/test/fixtures/apps/subapp/src/app/bootstrap-component/bootstrap-component.component.html +36 -0
- package/test/fixtures/apps/subapp/src/app/component-c/component-c.component.scss +47 -0
- package/test/ng-refactorer.test.js +184 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parses angular.json to extract project configuration
|
|
6
|
+
* @param {string} angularJsonPath - Path to angular.json file
|
|
7
|
+
* @returns {Object} - Parsed Angular configuration
|
|
8
|
+
*/
|
|
9
|
+
function parseAngularJson(angularJsonPath = null) {
|
|
10
|
+
// Find angular.json
|
|
11
|
+
const possiblePaths = [
|
|
12
|
+
angularJsonPath,
|
|
13
|
+
'angular.json',
|
|
14
|
+
path.join(process.cwd(), 'angular.json')
|
|
15
|
+
].filter(Boolean);
|
|
16
|
+
|
|
17
|
+
let angularJsonFile = null;
|
|
18
|
+
for (const filePath of possiblePaths) {
|
|
19
|
+
if (fs.existsSync(filePath)) {
|
|
20
|
+
angularJsonFile = filePath;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!angularJsonFile) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(angularJsonFile, 'utf8');
|
|
31
|
+
const angularJson = JSON.parse(content);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
raw: angularJson,
|
|
35
|
+
projects: extractProjects(angularJson),
|
|
36
|
+
defaultProject: angularJson.defaultProject || null,
|
|
37
|
+
version: angularJson.version || 1,
|
|
38
|
+
cli: angularJson.cli || {},
|
|
39
|
+
schematics: angularJson.schematics || {}
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn(`Warning: Could not parse angular.json: ${error.message}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts project information from angular.json
|
|
49
|
+
* @param {Object} angularJson - Parsed angular.json
|
|
50
|
+
* @returns {Array} - Array of project configurations
|
|
51
|
+
*/
|
|
52
|
+
function extractProjects(angularJson) {
|
|
53
|
+
const projects = [];
|
|
54
|
+
|
|
55
|
+
if (!angularJson.projects) {
|
|
56
|
+
return projects;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Object.entries(angularJson.projects).forEach(([name, config]) => {
|
|
60
|
+
const projectInfo = {
|
|
61
|
+
name,
|
|
62
|
+
type: config.projectType || 'application',
|
|
63
|
+
root: config.root || '',
|
|
64
|
+
sourceRoot: config.sourceRoot || path.join(config.root || '', 'src'),
|
|
65
|
+
prefix: config.prefix || 'app',
|
|
66
|
+
stylePreprocessor: getStylePreprocessor(config),
|
|
67
|
+
styleExt: getStyleExtension(config),
|
|
68
|
+
inlineStyle: getInlineStyle(config),
|
|
69
|
+
inlineTemplate: getInlineTemplate(config),
|
|
70
|
+
architect: config.architect || {},
|
|
71
|
+
schematics: config.schematics || {}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
projects.push(projectInfo);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return projects;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets the style preprocessor from project config
|
|
82
|
+
* @param {Object} projectConfig - Project configuration
|
|
83
|
+
* @returns {string} - Style preprocessor (scss, sass, less, css)
|
|
84
|
+
*/
|
|
85
|
+
function getStylePreprocessor(projectConfig) {
|
|
86
|
+
// Check schematics first
|
|
87
|
+
if (projectConfig.schematics) {
|
|
88
|
+
const componentSchematics =
|
|
89
|
+
projectConfig.schematics['@schematics/angular:component'] ||
|
|
90
|
+
projectConfig.schematics['@angular/material:component'] ||
|
|
91
|
+
{};
|
|
92
|
+
|
|
93
|
+
if (componentSchematics.style) {
|
|
94
|
+
return componentSchematics.style;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (componentSchematics.styleext) {
|
|
98
|
+
return componentSchematics.styleext;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check architect build options
|
|
103
|
+
if (projectConfig.architect && projectConfig.architect.build) {
|
|
104
|
+
const buildOptions = projectConfig.architect.build.options || {};
|
|
105
|
+
|
|
106
|
+
if (buildOptions.stylePreprocessorOptions) {
|
|
107
|
+
// Try to infer from includePaths or other indicators
|
|
108
|
+
const styles = buildOptions.styles || [];
|
|
109
|
+
for (const style of styles) {
|
|
110
|
+
const styleStr = typeof style === 'string' ? style : style.input || '';
|
|
111
|
+
if (styleStr.endsWith('.scss')) return 'scss';
|
|
112
|
+
if (styleStr.endsWith('.sass')) return 'sass';
|
|
113
|
+
if (styleStr.endsWith('.less')) return 'less';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check styles array
|
|
118
|
+
const styles = buildOptions.styles || [];
|
|
119
|
+
for (const style of styles) {
|
|
120
|
+
const styleStr = typeof style === 'string' ? style : style.input || '';
|
|
121
|
+
if (styleStr.endsWith('.scss')) return 'scss';
|
|
122
|
+
if (styleStr.endsWith('.sass')) return 'sass';
|
|
123
|
+
if (styleStr.endsWith('.less')) return 'less';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return 'scss'; // Default
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Gets the style file extension
|
|
132
|
+
* @param {Object} projectConfig - Project configuration
|
|
133
|
+
* @returns {string} - File extension
|
|
134
|
+
*/
|
|
135
|
+
function getStyleExtension(projectConfig) {
|
|
136
|
+
const preprocessor = getStylePreprocessor(projectConfig);
|
|
137
|
+
return preprocessor === 'css' ? 'css' : preprocessor;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets the inline style preference
|
|
142
|
+
* @param {Object} projectConfig - Project configuration
|
|
143
|
+
* @returns {boolean} - Whether to use inline styles
|
|
144
|
+
*/
|
|
145
|
+
function getInlineStyle(projectConfig) {
|
|
146
|
+
if (projectConfig.schematics) {
|
|
147
|
+
const componentSchematics =
|
|
148
|
+
projectConfig.schematics['@schematics/angular:component'] ||
|
|
149
|
+
projectConfig.schematics['@angular/material:component'] ||
|
|
150
|
+
{};
|
|
151
|
+
|
|
152
|
+
return componentSchematics.inlineStyle === true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gets the inline template preference
|
|
159
|
+
* @param {Object} projectConfig - Project configuration
|
|
160
|
+
* @returns {boolean} - Whether to use inline templates
|
|
161
|
+
*/
|
|
162
|
+
function getInlineTemplate(projectConfig) {
|
|
163
|
+
if (projectConfig.schematics) {
|
|
164
|
+
const componentSchematics =
|
|
165
|
+
projectConfig.schematics['@schematics/angular:component'] ||
|
|
166
|
+
projectConfig.schematics['@angular/material:component'] ||
|
|
167
|
+
{};
|
|
168
|
+
|
|
169
|
+
return componentSchematics.inlineTemplate === true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets project by name
|
|
176
|
+
* @param {Object} angularConfig - Parsed Angular configuration
|
|
177
|
+
* @param {string} projectName - Project name
|
|
178
|
+
* @returns {Object|null} - Project configuration
|
|
179
|
+
*/
|
|
180
|
+
function getProject(angularConfig, projectName) {
|
|
181
|
+
if (!angularConfig || !angularConfig.projects) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return angularConfig.projects.find(p => p.name === projectName) || null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Gets the default project
|
|
190
|
+
* @param {Object} angularConfig - Parsed Angular configuration
|
|
191
|
+
* @returns {Object|null} - Default project configuration
|
|
192
|
+
*/
|
|
193
|
+
function getDefaultProject(angularConfig) {
|
|
194
|
+
if (!angularConfig) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (angularConfig.defaultProject) {
|
|
199
|
+
const project = getProject(angularConfig, angularConfig.defaultProject);
|
|
200
|
+
if (project) return project;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Return first project if no default specified
|
|
204
|
+
return angularConfig.projects && angularConfig.projects.length > 0
|
|
205
|
+
? angularConfig.projects[0]
|
|
206
|
+
: null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Detects the source directory for a project
|
|
211
|
+
* @param {Object} angularConfig - Parsed Angular configuration
|
|
212
|
+
* @param {string} projectName - Project name (optional)
|
|
213
|
+
* @returns {string|null} - Source directory path
|
|
214
|
+
*/
|
|
215
|
+
function detectSourceDirectory(angularConfig, projectName = null) {
|
|
216
|
+
if (!angularConfig) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let project;
|
|
221
|
+
if (projectName) {
|
|
222
|
+
project = getProject(angularConfig, projectName);
|
|
223
|
+
} else {
|
|
224
|
+
project = getDefaultProject(angularConfig);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!project) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return project.sourceRoot || path.join(project.root || '', 'src');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Gets global style files from angular.json
|
|
236
|
+
* @param {Object} angularConfig - Parsed Angular configuration
|
|
237
|
+
* @param {string} projectName - Project name (optional)
|
|
238
|
+
* @returns {Array<string>} - Array of global style file paths
|
|
239
|
+
*/
|
|
240
|
+
function getGlobalStyleFiles(angularConfig, projectName = null) {
|
|
241
|
+
if (!angularConfig) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let project;
|
|
246
|
+
if (projectName) {
|
|
247
|
+
project = getProject(angularConfig, projectName);
|
|
248
|
+
} else {
|
|
249
|
+
project = getDefaultProject(angularConfig);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!project || !project.architect || !project.architect.build) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const buildOptions = project.architect.build.options || {};
|
|
257
|
+
const styles = buildOptions.styles || [];
|
|
258
|
+
|
|
259
|
+
return styles.map(style => {
|
|
260
|
+
if (typeof style === 'string') {
|
|
261
|
+
return style;
|
|
262
|
+
}
|
|
263
|
+
return style.input || '';
|
|
264
|
+
}).filter(Boolean);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generates enhanced configuration based on angular.json
|
|
269
|
+
* @param {string} angularJsonPath - Path to angular.json
|
|
270
|
+
* @param {Object} baseConfig - Base configuration
|
|
271
|
+
* @param {string} projectName - Project name (optional)
|
|
272
|
+
* @returns {Object} - Enhanced configuration
|
|
273
|
+
*/
|
|
274
|
+
function enhanceConfigFromAngularJson(angularJsonPath, baseConfig = {}, projectName = null) {
|
|
275
|
+
const angularConfig = parseAngularJson(angularJsonPath);
|
|
276
|
+
|
|
277
|
+
if (!angularConfig) {
|
|
278
|
+
return baseConfig;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let project;
|
|
282
|
+
if (projectName) {
|
|
283
|
+
project = getProject(angularConfig, projectName);
|
|
284
|
+
} else {
|
|
285
|
+
project = getDefaultProject(angularConfig);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!project) {
|
|
289
|
+
return baseConfig;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const enhanced = { ...baseConfig };
|
|
293
|
+
|
|
294
|
+
// Set source directory if not already set
|
|
295
|
+
if (!enhanced.src) {
|
|
296
|
+
enhanced.src = detectSourceDirectory(angularConfig, projectName);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Set output based on global styles
|
|
300
|
+
if (!enhanced.output) {
|
|
301
|
+
const globalStyles = getGlobalStyleFiles(angularConfig, projectName);
|
|
302
|
+
if (globalStyles.length > 0) {
|
|
303
|
+
// Find scss/sass file in global styles
|
|
304
|
+
const scssFile = globalStyles.find(s => s.endsWith('.scss') || s.endsWith('.sass'));
|
|
305
|
+
if (scssFile) {
|
|
306
|
+
// Extract directory and create variables file path
|
|
307
|
+
const dir = path.dirname(scssFile);
|
|
308
|
+
enhanced.output = path.join(dir, '_variables.scss');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Add Angular-specific metadata
|
|
314
|
+
enhanced.angular = {
|
|
315
|
+
project: project.name,
|
|
316
|
+
prefix: project.prefix,
|
|
317
|
+
stylePreprocessor: project.stylePreprocessor,
|
|
318
|
+
styleExt: project.styleExt,
|
|
319
|
+
inlineStyle: project.inlineStyle,
|
|
320
|
+
sourceRoot: project.sourceRoot,
|
|
321
|
+
root: project.root
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Adjust ignore patterns based on Angular structure
|
|
325
|
+
const angularIgnorePatterns = [
|
|
326
|
+
'**/node_modules/**',
|
|
327
|
+
'**/dist/**',
|
|
328
|
+
'**/.angular/**',
|
|
329
|
+
'**/out-tsc/**',
|
|
330
|
+
'**/_variables.scss',
|
|
331
|
+
'**/styles.scss', // Don't refactor main styles.scss
|
|
332
|
+
'**/theme.scss' // Don't refactor theme files
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
if (!enhanced.ignore || enhanced.ignore.length === 0) {
|
|
336
|
+
enhanced.ignore = angularIgnorePatterns;
|
|
337
|
+
} else {
|
|
338
|
+
// Merge ignore patterns
|
|
339
|
+
enhanced.ignore = [...new Set([...enhanced.ignore, ...angularIgnorePatterns])];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return enhanced;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Validates that a path is within an Angular project
|
|
347
|
+
* @param {Object} angularConfig - Parsed Angular configuration
|
|
348
|
+
* @param {string} targetPath - Path to validate
|
|
349
|
+
* @returns {Object|null} - Project info if path is valid, null otherwise
|
|
350
|
+
*/
|
|
351
|
+
function findProjectForPath(angularConfig, targetPath) {
|
|
352
|
+
if (!angularConfig || !angularConfig.projects) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const normalizedTarget = path.normalize(targetPath);
|
|
357
|
+
|
|
358
|
+
for (const project of angularConfig.projects) {
|
|
359
|
+
const projectRoot = path.normalize(project.root || '');
|
|
360
|
+
const sourceRoot = path.normalize(project.sourceRoot || '');
|
|
361
|
+
|
|
362
|
+
if (normalizedTarget.startsWith(projectRoot) || normalizedTarget.startsWith(sourceRoot)) {
|
|
363
|
+
return project;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
parseAngularJson,
|
|
372
|
+
extractProjects,
|
|
373
|
+
getProject,
|
|
374
|
+
getDefaultProject,
|
|
375
|
+
detectSourceDirectory,
|
|
376
|
+
getGlobalStyleFiles,
|
|
377
|
+
enhanceConfigFromAngularJson,
|
|
378
|
+
findProjectForPath,
|
|
379
|
+
getStylePreprocessor,
|
|
380
|
+
getStyleExtension
|
|
381
|
+
};
|