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.
@@ -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
+ };