|
8 | 8 |
|
9 | 9 | import type { workspaces } from '@angular-devkit/core';
|
10 | 10 | import {
|
| 11 | + DirEntry, |
11 | 12 | Rule,
|
12 | 13 | SchematicContext,
|
13 | 14 | SchematicsException,
|
14 | 15 | Tree,
|
15 | 16 | chain,
|
16 | 17 | externalSchematic,
|
17 | 18 | } from '@angular-devkit/schematics';
|
18 |
| -import { dirname, join } from 'node:path/posix'; |
| 19 | +import { basename, dirname, extname, join } from 'node:path/posix'; |
19 | 20 | import { JSONFile } from '../../utility/json-file';
|
20 | 21 | import { allTargetOptions, updateWorkspace } from '../../utility/workspace';
|
21 | 22 | import { Builders, ProjectType } from '../../utility/workspace-models';
|
| 23 | +import { findImports } from './css-import-lexer'; |
22 | 24 |
|
23 | 25 | function* updateBuildTarget(
|
24 | 26 | projectName: string,
|
@@ -193,12 +195,131 @@ function updateProjects(tree: Tree, context: SchematicContext) {
|
193 | 195 | break;
|
194 | 196 | }
|
195 | 197 | }
|
| 198 | + |
| 199 | + // Update CSS/Sass import specifiers |
| 200 | + const projectSourceRoot = join(project.root, project.sourceRoot ?? 'src'); |
| 201 | + updateStyleImports(tree, projectSourceRoot, buildTarget); |
196 | 202 | }
|
197 | 203 |
|
198 | 204 | return chain(rules);
|
199 | 205 | });
|
200 | 206 | }
|
201 | 207 |
|
| 208 | +function* visit( |
| 209 | + directory: DirEntry, |
| 210 | +): IterableIterator<[fileName: string, contents: string, sass: boolean]> { |
| 211 | + for (const path of directory.subfiles) { |
| 212 | + const sass = path.endsWith('.scss'); |
| 213 | + if (path.endsWith('.css') || sass) { |
| 214 | + const entry = directory.file(path); |
| 215 | + if (entry) { |
| 216 | + const content = entry.content; |
| 217 | + |
| 218 | + yield [entry.path, content.toString(), sass]; |
| 219 | + } |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + for (const path of directory.subdirs) { |
| 224 | + if (path === 'node_modules' || path.startsWith('.')) { |
| 225 | + continue; |
| 226 | + } |
| 227 | + |
| 228 | + yield* visit(directory.dir(path)); |
| 229 | + } |
| 230 | +} |
| 231 | + |
| 232 | +// Based on https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart |
| 233 | +function* potentialSassImports( |
| 234 | + specifier: string, |
| 235 | + base: string, |
| 236 | + fromImport: boolean, |
| 237 | +): Iterable<string> { |
| 238 | + const directory = join(base, dirname(specifier)); |
| 239 | + const extension = extname(specifier); |
| 240 | + const hasStyleExtension = extension === '.scss' || extension === '.sass' || extension === '.css'; |
| 241 | + // Remove the style extension if present to allow adding the `.import` suffix |
| 242 | + const filename = basename(specifier, hasStyleExtension ? extension : undefined); |
| 243 | + |
| 244 | + if (hasStyleExtension) { |
| 245 | + if (fromImport) { |
| 246 | + yield join(directory, filename + '.import' + extension); |
| 247 | + yield join(directory, '_' + filename + '.import' + extension); |
| 248 | + } |
| 249 | + yield join(directory, filename + extension); |
| 250 | + yield join(directory, '_' + filename + extension); |
| 251 | + } else { |
| 252 | + if (fromImport) { |
| 253 | + yield join(directory, filename + '.import.scss'); |
| 254 | + yield join(directory, filename + '.import.sass'); |
| 255 | + yield join(directory, filename + '.import.css'); |
| 256 | + yield join(directory, '_' + filename + '.import.scss'); |
| 257 | + yield join(directory, '_' + filename + '.import.sass'); |
| 258 | + yield join(directory, '_' + filename + '.import.css'); |
| 259 | + } |
| 260 | + yield join(directory, filename + '.scss'); |
| 261 | + yield join(directory, filename + '.sass'); |
| 262 | + yield join(directory, filename + '.css'); |
| 263 | + yield join(directory, '_' + filename + '.scss'); |
| 264 | + yield join(directory, '_' + filename + '.sass'); |
| 265 | + yield join(directory, '_' + filename + '.css'); |
| 266 | + } |
| 267 | +} |
| 268 | + |
| 269 | +function updateStyleImports( |
| 270 | + tree: Tree, |
| 271 | + projectSourceRoot: string, |
| 272 | + buildTarget: workspaces.TargetDefinition, |
| 273 | +) { |
| 274 | + const external = new Set<string>(); |
| 275 | + let needWorkspaceIncludePath = false; |
| 276 | + for (const file of visit(tree.getDir(projectSourceRoot))) { |
| 277 | + const [path, content, sass] = file; |
| 278 | + const relativeBase = dirname(path); |
| 279 | + |
| 280 | + let updater; |
| 281 | + for (const { start, specifier, fromUse } of findImports(content, sass)) { |
| 282 | + if (specifier[0] === '~') { |
| 283 | + updater ??= tree.beginUpdate(path); |
| 284 | + // start position includes the opening quote |
| 285 | + updater.remove(start + 1, 1); |
| 286 | + } else if (specifier[0] === '^') { |
| 287 | + updater ??= tree.beginUpdate(path); |
| 288 | + // start position includes the opening quote |
| 289 | + updater.remove(start + 1, 1); |
| 290 | + // Add to externalDependencies |
| 291 | + external.add(specifier.slice(1)); |
| 292 | + } else if ( |
| 293 | + sass && |
| 294 | + [...potentialSassImports(specifier, relativeBase, !fromUse)].every( |
| 295 | + (v) => !tree.exists(v), |
| 296 | + ) && |
| 297 | + [...potentialSassImports(specifier, '/', !fromUse)].some((v) => tree.exists(v)) |
| 298 | + ) { |
| 299 | + needWorkspaceIncludePath = true; |
| 300 | + } |
| 301 | + } |
| 302 | + if (updater) { |
| 303 | + tree.commitUpdate(updater); |
| 304 | + } |
| 305 | + } |
| 306 | + |
| 307 | + if (needWorkspaceIncludePath) { |
| 308 | + buildTarget.options ??= {}; |
| 309 | + buildTarget.options['stylePreprocessorOptions'] ??= {}; |
| 310 | + ((buildTarget.options['stylePreprocessorOptions'] as { includePaths?: string[] })[ |
| 311 | + 'includePaths' |
| 312 | + ] ??= []).push('.'); |
| 313 | + } |
| 314 | + |
| 315 | + if (external.size > 0) { |
| 316 | + buildTarget.options ??= {}; |
| 317 | + ((buildTarget.options['externalDependencies'] as string[] | undefined) ??= []).push( |
| 318 | + ...external, |
| 319 | + ); |
| 320 | + } |
| 321 | +} |
| 322 | + |
202 | 323 | function deleteFile(path: string): Rule {
|
203 | 324 | return (tree) => {
|
204 | 325 | tree.delete(path);
|
|
0 commit comments