This commit is contained in:
CHEVALLIER Abel
2025-11-13 16:23:22 +01:00
parent de9c515a47
commit cb235644dc
34924 changed files with 3811102 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
export interface RequireInfo {
module: string;
export?: string;
isCall?: boolean;
arguments?: KarmaConfigValue[];
}
export type KarmaConfigValue = string | boolean | number | KarmaConfigValue[] | {
[key: string]: KarmaConfigValue;
} | RequireInfo | undefined;
export interface KarmaConfigAnalysis {
settings: Map<string, KarmaConfigValue>;
hasUnsupportedValues: boolean;
}
/**
* Analyzes the content of a Karma configuration file to extract its settings.
*
* @param content The string content of the `karma.conf.js` file.
* @returns An object containing the configuration settings and a flag indicating if unsupported values were found.
*/
export declare function analyzeKarmaConfig(content: string): KarmaConfigAnalysis;

View File

@@ -0,0 +1,131 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeKarmaConfig = analyzeKarmaConfig;
const typescript_1 = __importDefault(require("../../third_party/github.com/Microsoft/TypeScript/lib/typescript"));
function isRequireInfo(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'module' in value;
}
function isSupportedPropertyAssignment(prop) {
return (typescript_1.default.isPropertyAssignment(prop) && (typescript_1.default.isIdentifier(prop.name) || typescript_1.default.isStringLiteral(prop.name)));
}
/**
* Analyzes the content of a Karma configuration file to extract its settings.
*
* @param content The string content of the `karma.conf.js` file.
* @returns An object containing the configuration settings and a flag indicating if unsupported values were found.
*/
function analyzeKarmaConfig(content) {
const sourceFile = typescript_1.default.createSourceFile('karma.conf.js', content, typescript_1.default.ScriptTarget.Latest, true);
const settings = new Map();
let hasUnsupportedValues = false;
function visit(node) {
// The Karma configuration is defined within a `config.set({ ... })` call.
if (typescript_1.default.isCallExpression(node) &&
typescript_1.default.isPropertyAccessExpression(node.expression) &&
node.expression.expression.getText(sourceFile) === 'config' &&
node.expression.name.text === 'set' &&
node.arguments.length === 1 &&
typescript_1.default.isObjectLiteralExpression(node.arguments[0])) {
// We found `config.set`, now we extract the properties from the object literal.
for (const prop of node.arguments[0].properties) {
if (isSupportedPropertyAssignment(prop)) {
const key = prop.name.text;
const value = extractValue(prop.initializer);
settings.set(key, value);
}
else {
hasUnsupportedValues = true;
}
}
}
else {
typescript_1.default.forEachChild(node, visit);
}
}
function extractValue(node) {
switch (node.kind) {
case typescript_1.default.SyntaxKind.StringLiteral:
return node.text;
case typescript_1.default.SyntaxKind.NumericLiteral:
return Number(node.text);
case typescript_1.default.SyntaxKind.TrueKeyword:
return true;
case typescript_1.default.SyntaxKind.FalseKeyword:
return false;
case typescript_1.default.SyntaxKind.Identifier: {
const identifier = node.text;
if (identifier === '__dirname' || identifier === '__filename') {
return identifier;
}
break;
}
case typescript_1.default.SyntaxKind.CallExpression: {
const callExpr = node;
// Handle require('...')
if (typescript_1.default.isIdentifier(callExpr.expression) &&
callExpr.expression.text === 'require' &&
callExpr.arguments.length === 1 &&
typescript_1.default.isStringLiteral(callExpr.arguments[0])) {
return { module: callExpr.arguments[0].text };
}
// Handle calls on a require, e.g. require('path').join()
const calleeValue = extractValue(callExpr.expression);
if (isRequireInfo(calleeValue)) {
return {
...calleeValue,
isCall: true,
arguments: callExpr.arguments.map(extractValue),
};
}
break;
}
case typescript_1.default.SyntaxKind.PropertyAccessExpression: {
const propAccessExpr = node;
// Handle config constants like `config.LOG_INFO`
if (typescript_1.default.isIdentifier(propAccessExpr.expression) &&
propAccessExpr.expression.text === 'config') {
return `config.${propAccessExpr.name.text}`;
}
const value = extractValue(propAccessExpr.expression);
if (isRequireInfo(value)) {
const currentExport = value.export
? `${value.export}.${propAccessExpr.name.text}`
: propAccessExpr.name.text;
return { ...value, export: currentExport };
}
break;
}
case typescript_1.default.SyntaxKind.ArrayLiteralExpression:
return node.elements.map(extractValue);
case typescript_1.default.SyntaxKind.ObjectLiteralExpression: {
const obj = {};
for (const prop of node.properties) {
if (isSupportedPropertyAssignment(prop)) {
// Recursively extract values for nested objects.
obj[prop.name.text] = extractValue(prop.initializer);
}
else {
hasUnsupportedValues = true;
}
}
return obj;
}
}
// For complex expressions (like variables) that we don't need to resolve,
// we mark the analysis as potentially incomplete.
hasUnsupportedValues = true;
return undefined;
}
visit(sourceFile);
return { settings, hasUnsupportedValues };
}

View File

@@ -0,0 +1,63 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { KarmaConfigAnalysis, KarmaConfigValue } from './karma-config-analyzer';
/**
* Represents the difference between two Karma configurations.
*/
export interface KarmaConfigDiff {
/** A map of settings that were added in the project's configuration. */
added: Map<string, KarmaConfigValue>;
/** A map of settings that were removed from the project's configuration. */
removed: Map<string, KarmaConfigValue>;
/** A map of settings that were modified between the two configurations. */
modified: Map<string, {
projectValue: KarmaConfigValue;
defaultValue: KarmaConfigValue;
}>;
/** A boolean indicating if the comparison is reliable (i.e., no unsupported values were found). */
isReliable: boolean;
}
/**
* Generates the default Karma configuration file content as a string.
* @param relativePathToWorkspaceRoot The relative path from the Karma config file to the workspace root.
* @param projectName The name of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed.
* @returns The content of the default `karma.conf.js` file.
*/
export declare function generateDefaultKarmaConfig(relativePathToWorkspaceRoot: string, projectName: string, needDevkitPlugin: boolean): Promise<string>;
/**
* Compares two Karma configuration analyses and returns the difference.
* @param projectAnalysis The analysis of the project's configuration.
* @param defaultAnalysis The analysis of the default configuration to compare against.
* @returns A diff object representing the changes between the two configurations.
*/
export declare function compareKarmaConfigs(projectAnalysis: KarmaConfigAnalysis, defaultAnalysis: KarmaConfigAnalysis): KarmaConfigDiff;
/**
* Checks if there are any differences in the provided Karma configuration diff.
* @param diff The Karma configuration diff object to check.
* @returns True if there are any differences; false otherwise.
*/
export declare function hasDifferences(diff: KarmaConfigDiff): boolean;
/**
* Compares a project's Karma configuration with the default configuration.
* @param projectConfigContent The content of the project's `karma.conf.js` file.
* @param projectRoot The root directory of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed for the default config.
* @param karmaConfigPath The path to the Karma configuration file, used to resolve relative paths.
* @returns A diff object representing the changes.
*/
export declare function compareKarmaConfigToDefault(projectConfigContent: string, projectName: string, karmaConfigPath: string, needDevkitPlugin: boolean): Promise<KarmaConfigDiff>;
/**
* Compares a project's Karma configuration with the default configuration.
* @param projectAnalysis The analysis of the project's configuration.
* @param projectRoot The root directory of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed for the default config.
* @param karmaConfigPath The path to the Karma configuration file, used to resolve relative paths.
* @returns A diff object representing the changes.
*/
export declare function compareKarmaConfigToDefault(projectAnalysis: KarmaConfigAnalysis, projectName: string, karmaConfigPath: string, needDevkitPlugin: boolean): Promise<KarmaConfigDiff>;

View File

@@ -0,0 +1,89 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateDefaultKarmaConfig = generateDefaultKarmaConfig;
exports.compareKarmaConfigs = compareKarmaConfigs;
exports.hasDifferences = hasDifferences;
exports.compareKarmaConfigToDefault = compareKarmaConfigToDefault;
const promises_1 = require("node:fs/promises");
const posix_1 = __importDefault(require("node:path/posix"));
const node_util_1 = require("node:util");
const paths_1 = require("../../utility/paths");
const karma_config_analyzer_1 = require("./karma-config-analyzer");
/**
* Generates the default Karma configuration file content as a string.
* @param relativePathToWorkspaceRoot The relative path from the Karma config file to the workspace root.
* @param projectName The name of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed.
* @returns The content of the default `karma.conf.js` file.
*/
async function generateDefaultKarmaConfig(relativePathToWorkspaceRoot, projectName, needDevkitPlugin) {
const templatePath = require.resolve('../../config/files/karma.conf.js.template');
let template = await (0, promises_1.readFile)(templatePath, 'utf-8');
// TODO: Replace this with the actual schematic templating logic.
template = template
.replace(/<%= relativePathToWorkspaceRoot %>/g, posix_1.default.normalize(relativePathToWorkspaceRoot).replace(/\\/g, '/'))
.replace(/<%= folderName %>/g, projectName);
const devkitPluginRegex = /<% if \(needDevkitPlugin\) { %>(.*?)<% } %>/gs;
const replacement = needDevkitPlugin ? '$1' : '';
template = template.replace(devkitPluginRegex, replacement);
return template;
}
/**
* Compares two Karma configuration analyses and returns the difference.
* @param projectAnalysis The analysis of the project's configuration.
* @param defaultAnalysis The analysis of the default configuration to compare against.
* @returns A diff object representing the changes between the two configurations.
*/
function compareKarmaConfigs(projectAnalysis, defaultAnalysis) {
const added = new Map();
const removed = new Map();
const modified = new Map();
const allKeys = new Set([...projectAnalysis.settings.keys(), ...defaultAnalysis.settings.keys()]);
for (const key of allKeys) {
const projectValue = projectAnalysis.settings.get(key);
const defaultValue = defaultAnalysis.settings.get(key);
if (projectValue !== undefined && defaultValue === undefined) {
added.set(key, projectValue);
}
else if (projectValue === undefined && defaultValue !== undefined) {
removed.set(key, defaultValue);
}
else if (projectValue !== undefined && defaultValue !== undefined) {
if (!(0, node_util_1.isDeepStrictEqual)(projectValue, defaultValue)) {
modified.set(key, { projectValue, defaultValue });
}
}
}
return {
added,
removed,
modified,
isReliable: !projectAnalysis.hasUnsupportedValues && !defaultAnalysis.hasUnsupportedValues,
};
}
/**
* Checks if there are any differences in the provided Karma configuration diff.
* @param diff The Karma configuration diff object to check.
* @returns True if there are any differences; false otherwise.
*/
function hasDifferences(diff) {
return diff.added.size > 0 || diff.removed.size > 0 || diff.modified.size > 0;
}
async function compareKarmaConfigToDefault(projectConfigOrAnalysis, projectName, karmaConfigPath, needDevkitPlugin) {
const projectAnalysis = typeof projectConfigOrAnalysis === 'string'
? (0, karma_config_analyzer_1.analyzeKarmaConfig)(projectConfigOrAnalysis)
: projectConfigOrAnalysis;
const defaultContent = await generateDefaultKarmaConfig((0, paths_1.relativePathToWorkspaceRoot)(posix_1.default.dirname(karmaConfigPath)), projectName, needDevkitPlugin);
const defaultAnalysis = (0, karma_config_analyzer_1.analyzeKarmaConfig)(defaultContent);
return compareKarmaConfigs(projectAnalysis, defaultAnalysis);
}

View File

@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import type { Rule } from '@angular-devkit/schematics';
export default function (): Rule;

View File

@@ -0,0 +1,62 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const workspace_1 = require("../../utility/workspace");
const workspace_models_1 = require("../../utility/workspace-models");
const karma_config_analyzer_1 = require("./karma-config-analyzer");
const karma_config_comparer_1 = require("./karma-config-comparer");
function updateProjects(tree) {
return (0, workspace_1.updateWorkspace)(async (workspace) => {
const removableKarmaConfigs = new Map();
for (const [projectName, project] of workspace.projects) {
for (const [, target] of project.targets) {
let needDevkitPlugin = false;
switch (target.builder) {
case workspace_models_1.Builders.Karma:
needDevkitPlugin = true;
break;
case workspace_models_1.Builders.BuildKarma:
break;
default:
continue;
}
for (const [, options] of (0, workspace_1.allTargetOptions)(target, false)) {
const karmaConfig = options['karmaConfig'];
if (typeof karmaConfig !== 'string') {
continue;
}
let isRemovable = removableKarmaConfigs.get(karmaConfig);
if (isRemovable === undefined && tree.exists(karmaConfig)) {
const content = tree.readText(karmaConfig);
const analysis = (0, karma_config_analyzer_1.analyzeKarmaConfig)(content);
if (analysis.hasUnsupportedValues) {
// Cannot safely determine if the file is removable.
isRemovable = false;
}
else {
const diff = await (0, karma_config_comparer_1.compareKarmaConfigToDefault)(analysis, projectName, karmaConfig, needDevkitPlugin);
isRemovable = !(0, karma_config_comparer_1.hasDifferences)(diff) && diff.isReliable;
}
removableKarmaConfigs.set(karmaConfig, isRemovable);
if (isRemovable) {
tree.delete(karmaConfig);
}
}
if (isRemovable) {
delete options['karmaConfig'];
}
}
}
}
});
}
function default_1() {
return updateProjects;
}

View File

@@ -0,0 +1,38 @@
{
"encapsulation": false,
"schematics": {
"replace-provide-server-rendering-import": {
"version": "20.0.0",
"factory": "./replace-provide-server-rendering-import/migration",
"description": "Migrate imports of 'provideServerRendering' from '@angular/platform-server' to '@angular/ssr'."
},
"replace-provide-server-routing": {
"version": "20.0.0",
"factory": "./replace-provide-server-routing/migration",
"description": "Migrate 'provideServerRendering' to use 'withRoutes', and remove 'provideServerRouting' and 'provideServerRoutesConfig' from '@angular/ssr'."
},
"update-module-resolution": {
"version": "20.0.0",
"factory": "./update-module-resolution/migration",
"description": "Update 'moduleResolution' to 'bundler' in TypeScript configurations. You can read more about this, here: https://www.typescriptlang.org/tsconfig/#moduleResolution"
},
"previous-style-guide": {
"version": "20.0.0",
"factory": "./previous-style-guide/migration",
"description": "Update workspace generation defaults to maintain previous style guide behavior."
},
"use-application-builder": {
"version": "20.0.0",
"factory": "./use-application-builder/migration",
"description": "Migrate application projects to the new build system. Application projects that are using the '@angular-devkit/build-angular' package's 'browser' and/or 'browser-esbuild' builders will be migrated to use the new 'application' builder. You can read more about this, including known issues and limitations, here: https://angular.dev/tools/cli/build-system-migration",
"optional": true,
"recommended": true,
"documentation": "tools/cli/build-system-migration"
},
"remove-default-karma-config": {
"version": "20.2.0",
"factory": "./karma/migration",
"description": "Remove any karma configuration files that only contain the default content. The default configuration is automatically available without a specific project file."
}
}
}

View File

@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import type { Rule } from '@angular-devkit/schematics';
export default function (): Rule;

View File

@@ -0,0 +1,42 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const workspace_1 = require("../../utility/workspace");
const TYPE_SCHEMATICS = ['component', 'directive', 'service'];
const SEPARATOR_SCHEMATICS = ['guard', 'interceptor', 'module', 'pipe', 'resolver'];
function default_1() {
return (0, workspace_1.updateWorkspace)((workspace) => {
let schematicsDefaults = workspace.extensions['schematics'];
// Ensure "schematics" field is an object
if (!schematicsDefaults ||
typeof schematicsDefaults !== 'object' ||
Array.isArray(schematicsDefaults)) {
schematicsDefaults = workspace.extensions['schematics'] = {};
}
// Add "type" value for each schematic to continue generating a type suffix.
// New default is an empty type value.
for (const schematicName of TYPE_SCHEMATICS) {
const schematic = (schematicsDefaults[`@schematics/angular:${schematicName}`] ??= {});
if (typeof schematic === 'object' && !Array.isArray(schematic) && !('type' in schematic)) {
schematic['type'] = schematicName;
}
}
// Add "typeSeparator" value for each schematic to continue generating "." before type.
// New default is an "-" type value.
for (const schematicName of SEPARATOR_SCHEMATICS) {
const schematic = (schematicsDefaults[`@schematics/angular:${schematicName}`] ??= {});
if (typeof schematic === 'object' &&
!Array.isArray(schematic) &&
!('typeSeparator' in schematic)) {
schematic['typeSeparator'] = '.';
}
}
});
}

View File

@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { Rule } from '@angular-devkit/schematics';
export default function (): Rule;

View File

@@ -0,0 +1,98 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const typescript_1 = __importDefault(require("../../third_party/github.com/Microsoft/TypeScript/lib/typescript"));
const dependency_1 = require("../../utility/dependency");
const latest_versions_1 = require("../../utility/latest-versions");
function* visit(directory) {
for (const path of directory.subfiles) {
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
const entry = directory.file(path);
if (entry) {
const content = entry.content;
if (content.includes('provideServerRendering') &&
content.includes('@angular/platform-server')) {
// Only need to rename the import so we can just string replacements.
yield [entry.path, content.toString()];
}
}
}
}
for (const path of directory.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
yield* visit(directory.dir(path));
}
}
function default_1() {
return async (tree) => {
let rule;
for (const [filePath, content] of visit(tree.root)) {
let updatedContent = content;
const ssrImports = new Set();
const platformServerImports = new Set();
const sourceFile = typescript_1.default.createSourceFile(filePath, content, typescript_1.default.ScriptTarget.Latest, true);
sourceFile.forEachChild((node) => {
if (typescript_1.default.isImportDeclaration(node)) {
const moduleSpecifier = node.moduleSpecifier.getText(sourceFile);
if (moduleSpecifier.includes('@angular/platform-server')) {
const importClause = node.importClause;
if (importClause &&
importClause.namedBindings &&
typescript_1.default.isNamedImports(importClause.namedBindings)) {
const namedImports = importClause.namedBindings.elements.map((e) => e.getText(sourceFile));
namedImports.forEach((importName) => {
if (importName === 'provideServerRendering') {
ssrImports.add(importName);
}
else {
platformServerImports.add(importName);
}
});
}
updatedContent = updatedContent.replace(node.getFullText(sourceFile), '');
}
else if (moduleSpecifier.includes('@angular/ssr')) {
const importClause = node.importClause;
if (importClause &&
importClause.namedBindings &&
typescript_1.default.isNamedImports(importClause.namedBindings)) {
importClause.namedBindings.elements.forEach((e) => {
ssrImports.add(e.getText(sourceFile));
});
}
updatedContent = updatedContent.replace(node.getFullText(sourceFile), '');
}
}
});
if (platformServerImports.size > 0) {
updatedContent =
`import { ${Array.from(platformServerImports).sort().join(', ')} } from '@angular/platform-server';\n` +
updatedContent;
}
if (ssrImports.size > 0) {
updatedContent =
`import { ${Array.from(ssrImports).sort().join(', ')} } from '@angular/ssr';\n` +
updatedContent;
}
if (content !== updatedContent) {
tree.overwrite(filePath, updatedContent);
if (rule === undefined) {
rule = (0, dependency_1.addDependency)('@angular/ssr', latest_versions_1.latestVersions.AngularSSR);
}
}
}
return rule;
};
}

View File

@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { Rule } from '@angular-devkit/schematics';
export default function (): Rule;

View File

@@ -0,0 +1,125 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const ts = __importStar(require("../../third_party/github.com/Microsoft/TypeScript/lib/typescript"));
const dependencies_1 = require("../../utility/dependencies");
function* visit(directory) {
for (const path of directory.subfiles) {
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
const entry = directory.file(path);
if (entry) {
const content = entry.content;
if ((content.includes('provideServerRouting') ||
content.includes('provideServerRoutesConfig')) &&
content.includes('@angular/ssr')) {
// Only need to rename the import so we can just string replacements.
yield [entry.path, content.toString()];
}
}
}
}
for (const path of directory.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
yield* visit(directory.dir(path));
}
}
function default_1() {
return async (tree) => {
if (!(0, dependencies_1.getPackageJsonDependency)(tree, '@angular/ssr')) {
return;
}
for (const [filePath, content] of visit(tree.root)) {
const recorder = tree.beginUpdate(filePath);
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
function visit(node) {
if (ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
node.name.text === 'providers' &&
ts.isArrayLiteralExpression(node.initializer)) {
const providersArray = node.initializer;
const newProviders = providersArray.elements
.filter((el) => {
return !(ts.isCallExpression(el) &&
ts.isIdentifier(el.expression) &&
el.expression.text === 'provideServerRendering');
})
.map((el) => {
if (ts.isCallExpression(el) &&
ts.isIdentifier(el.expression) &&
(el.expression.text === 'provideServerRouting' ||
el.expression.text === 'provideServerRoutesConfig')) {
const [withRouteVal, ...others] = el.arguments.map((arg) => arg.getText());
return `provideServerRendering(withRoutes(${withRouteVal})${others.length ? ', ' + others.join(', ') : ''})`;
}
return el.getText();
});
// Update the 'providers' array in the source file
recorder.remove(providersArray.getStart(), providersArray.getWidth());
recorder.insertRight(providersArray.getStart(), `[${newProviders.join(', ')}]`);
}
ts.forEachChild(node, visit);
}
// Visit all nodes to update 'providers'
visit(sourceFile);
// Update imports by removing 'provideServerRouting'
const importDecl = sourceFile.statements.find((stmt) => ts.isImportDeclaration(stmt) &&
ts.isStringLiteral(stmt.moduleSpecifier) &&
stmt.moduleSpecifier.text === '@angular/ssr');
if (importDecl?.importClause?.namedBindings) {
const namedBindings = importDecl?.importClause.namedBindings;
if (ts.isNamedImports(namedBindings)) {
const elements = namedBindings.elements;
const updatedElements = elements
.map((el) => el.getText())
.filter((x) => x !== 'provideServerRouting' && x !== 'provideServerRoutesConfig');
updatedElements.push('withRoutes');
recorder.remove(namedBindings.getStart(), namedBindings.getWidth());
recorder.insertLeft(namedBindings.getStart(), `{ ${updatedElements.sort().join(', ')} }`);
}
}
tree.commitUpdate(recorder);
}
};
}

View File

@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { Rule } from '@angular-devkit/schematics';
export default function (): Rule;

View File

@@ -0,0 +1,52 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const json_file_1 = require("../../utility/json-file");
const workspace_1 = require("../../utility/workspace");
function default_1() {
return async (host) => {
const uniqueTsConfigs = new Set();
if (host.exists('tsconfig.json')) {
// Workspace level tsconfig
uniqueTsConfigs.add('tsconfig.json');
}
const workspace = await (0, workspace_1.getWorkspace)(host);
for (const [, target] of (0, workspace_1.allWorkspaceTargets)(workspace)) {
for (const [, opt] of (0, workspace_1.allTargetOptions)(target)) {
if (typeof opt?.tsConfig === 'string') {
uniqueTsConfigs.add(opt.tsConfig);
}
}
}
for (const tsConfig of uniqueTsConfigs) {
if (host.exists(tsConfig)) {
updateModuleResolution(host, tsConfig);
}
}
};
}
function updateModuleResolution(host, tsConfigPath) {
const json = new json_file_1.JSONFile(host, tsConfigPath);
const jsonPath = ['compilerOptions'];
const compilerOptions = json.get(jsonPath);
if (compilerOptions && typeof compilerOptions === 'object') {
const { moduleResolution, module } = compilerOptions;
if (typeof moduleResolution !== 'string' || moduleResolution.toLowerCase() === 'bundler') {
return;
}
if (typeof module === 'string' && module.toLowerCase() === 'preserve') {
return;
}
json.modify(jsonPath, {
...compilerOptions,
'moduleResolution': 'bundler',
});
}
}

View File

@@ -0,0 +1,19 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Scans a CSS or Sass file and locates all valid import/use directive values as defined by the
* syntax specification.
* @param contents A string containing a CSS or Sass file to scan.
* @returns An iterable that yields each CSS directive value found.
*/
export declare function findImports(contents: string, sass: boolean): Iterable<{
start: number;
end: number;
specifier: string;
fromUse?: boolean;
}>;

View File

@@ -0,0 +1,117 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.findImports = findImports;
/**
* Determines if a unicode code point is a CSS whitespace character.
* @param code The unicode code point to test.
* @returns true, if the code point is CSS whitespace; false, otherwise.
*/
function isWhitespace(code) {
// Based on https://www.w3.org/TR/css-syntax-3/#whitespace
switch (code) {
case 0x0009: // tab
case 0x0020: // space
case 0x000a: // line feed
case 0x000c: // form feed
case 0x000d: // carriage return
return true;
default:
return false;
}
}
/**
* Scans a CSS or Sass file and locates all valid import/use directive values as defined by the
* syntax specification.
* @param contents A string containing a CSS or Sass file to scan.
* @returns An iterable that yields each CSS directive value found.
*/
function* findImports(contents, sass) {
yield* find(contents, '@import ');
if (sass) {
for (const result of find(contents, '@use ')) {
yield { ...result, fromUse: true };
}
}
}
/**
* Scans a CSS or Sass file and locates all valid function/directive values as defined by the
* syntax specification.
* @param contents A string containing a CSS or Sass file to scan.
* @param prefix The prefix to start a valid segment.
* @returns An iterable that yields each CSS url function value found.
*/
function* find(contents, prefix) {
let pos = 0;
let width = 1;
let current = -1;
const next = () => {
pos += width;
current = contents.codePointAt(pos) ?? -1;
width = current > 0xffff ? 2 : 1;
return current;
};
// Based on https://www.w3.org/TR/css-syntax-3/#consume-ident-like-token
while ((pos = contents.indexOf(prefix, pos)) !== -1) {
// Set to position of the last character in prefix
pos += prefix.length - 1;
width = 1;
// Consume all leading whitespace
while (isWhitespace(next())) {
/* empty */
}
// Initialize URL state
const url = { start: pos, end: -1, specifier: '' };
let complete = false;
// If " or ', then consume the value as a string
if (current === 0x0022 || current === 0x0027) {
const ending = current;
// Based on https://www.w3.org/TR/css-syntax-3/#consume-string-token
while (!complete) {
switch (next()) {
case -1: // EOF
return;
case 0x000a: // line feed
case 0x000c: // form feed
case 0x000d: // carriage return
// Invalid
complete = true;
break;
case 0x005c: // \ -- character escape
// If not EOF or newline, add the character after the escape
switch (next()) {
case -1:
return;
case 0x000a: // line feed
case 0x000c: // form feed
case 0x000d: // carriage return
// Skip when inside a string
break;
default:
// TODO: Handle hex escape codes
url.specifier += String.fromCodePoint(current);
break;
}
break;
case ending:
// Full string position should include the quotes for replacement
url.end = pos + 1;
complete = true;
yield url;
break;
default:
url.specifier += String.fromCodePoint(current);
break;
}
}
next();
continue;
}
}
}

View File

@@ -0,0 +1,12 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { Rule } from '@angular-devkit/schematics';
/**
* Migration main entrypoint
*/
export default function (): Rule;

View File

@@ -0,0 +1,275 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const schematics_1 = require("@angular-devkit/schematics");
const posix_1 = require("node:path/posix");
const dependency_1 = require("../../utility/dependency");
const json_file_1 = require("../../utility/json-file");
const latest_versions_1 = require("../../utility/latest-versions");
const workspace_1 = require("../../utility/workspace");
const workspace_models_1 = require("../../utility/workspace-models");
const stylesheet_updates_1 = require("./stylesheet-updates");
function* updateBuildTarget(projectName, buildTarget, serverTarget, tree, context) {
// Update builder target and options
buildTarget.builder = workspace_models_1.Builders.Application;
for (const [, options] of (0, workspace_1.allTargetOptions)(buildTarget, false)) {
if (options['index'] === '') {
options['index'] = false;
}
// Rename and transform options
options['browser'] = options['main'];
if (serverTarget && typeof options['browser'] === 'string') {
options['server'] = (0, posix_1.dirname)(options['browser']) + '/main.server.ts';
}
options['serviceWorker'] = options['ngswConfigPath'] ?? options['serviceWorker'];
if (typeof options['polyfills'] === 'string') {
options['polyfills'] = [options['polyfills']];
}
let outputPath = options['outputPath'];
if (typeof outputPath === 'string') {
if (!/\/browser\/?$/.test(outputPath)) {
// TODO: add prompt.
context.logger.warn(`The output location of the browser build has been updated from "${outputPath}" to ` +
`"${(0, posix_1.join)(outputPath, 'browser')}". ` +
'You might need to adjust your deployment pipeline or, as an alternative, ' +
'set outputPath.browser to "" in order to maintain the previous functionality.');
}
else {
outputPath = outputPath.replace(/\/browser\/?$/, '');
}
options['outputPath'] = {
base: outputPath,
};
if (typeof options['resourcesOutputPath'] === 'string') {
const media = options['resourcesOutputPath'].replaceAll('/', '');
if (media && media !== 'media') {
options['outputPath'] = {
base: outputPath,
media,
};
}
}
}
// Delete removed options
delete options['vendorChunk'];
delete options['commonChunk'];
delete options['resourcesOutputPath'];
delete options['buildOptimizer'];
delete options['main'];
delete options['ngswConfigPath'];
}
// Merge browser and server tsconfig
if (serverTarget) {
const browserTsConfig = buildTarget.options?.tsConfig;
const serverTsConfig = serverTarget.options?.tsConfig;
if (typeof browserTsConfig !== 'string') {
throw new schematics_1.SchematicsException(`Cannot update project "${projectName}" to use the application builder` +
` as the browser tsconfig cannot be located.`);
}
if (typeof serverTsConfig !== 'string') {
throw new schematics_1.SchematicsException(`Cannot update project "${projectName}" to use the application builder` +
` as the server tsconfig cannot be located.`);
}
const browserJson = new json_file_1.JSONFile(tree, browserTsConfig);
const serverJson = new json_file_1.JSONFile(tree, serverTsConfig);
const filesPath = ['files'];
const files = new Set([
...(browserJson.get(filesPath) ?? []),
...(serverJson.get(filesPath) ?? []),
]);
// Server file will be added later by the means of the ssr schematic.
files.delete('server.ts');
browserJson.modify(filesPath, Array.from(files));
const typesPath = ['compilerOptions', 'types'];
browserJson.modify(typesPath, Array.from(new Set([
...(browserJson.get(typesPath) ?? []),
...(serverJson.get(typesPath) ?? []),
])));
// Delete server tsconfig
yield deleteFile(serverTsConfig);
}
// Update server file
const ssrMainFile = serverTarget?.options?.['main'];
if (typeof ssrMainFile === 'string') {
// Do not delete the server main file if it's the same as the browser file.
if (buildTarget.options?.browser !== ssrMainFile) {
yield deleteFile(ssrMainFile);
}
yield (0, schematics_1.externalSchematic)('@schematics/angular', 'ssr', {
project: projectName,
});
}
}
function updateProjects(tree, context) {
return (0, workspace_1.updateWorkspace)((workspace) => {
const rules = [];
for (const [name, project] of workspace.projects) {
if (project.extensions.projectType !== workspace_models_1.ProjectType.Application) {
// Only interested in application projects since these changes only effects application builders
continue;
}
const buildTarget = project.targets.get('build');
if (!buildTarget || buildTarget.builder === workspace_models_1.Builders.Application) {
continue;
}
if (buildTarget.builder !== workspace_models_1.Builders.BrowserEsbuild &&
buildTarget.builder !== workspace_models_1.Builders.Browser) {
context.logger.error(`Cannot update project "${name}" to use the application builder.` +
` Only "${workspace_models_1.Builders.BrowserEsbuild}" and "${workspace_models_1.Builders.Browser}" can be automatically migrated.`);
continue;
}
const serverTarget = project.targets.get('server');
rules.push(...updateBuildTarget(name, buildTarget, serverTarget, tree, context));
// Delete all redundant targets
for (const [key, target] of project.targets) {
switch (target.builder) {
case workspace_models_1.Builders.Server:
case workspace_models_1.Builders.Prerender:
case workspace_models_1.Builders.AppShell:
case workspace_models_1.Builders.SsrDevServer:
project.targets.delete(key);
break;
}
}
// Update CSS/Sass import specifiers
const projectSourceRoot = (0, posix_1.join)(project.root, project.sourceRoot ?? 'src');
(0, stylesheet_updates_1.updateStyleImports)(tree, projectSourceRoot, buildTarget);
}
// Check for @angular-devkit/build-angular Webpack usage
let hasAngularDevkitUsage = false;
for (const [, target] of (0, workspace_1.allWorkspaceTargets)(workspace)) {
switch (target.builder) {
case workspace_models_1.Builders.Application:
case workspace_models_1.Builders.DevServer:
case workspace_models_1.Builders.ExtractI18n:
case workspace_models_1.Builders.Karma:
case workspace_models_1.Builders.NgPackagr:
// Ignore application, dev server, and i18n extraction for devkit usage check.
// Both will be replaced if no other usage is found.
continue;
}
if (target.builder.startsWith('@angular-devkit/build-angular:')) {
hasAngularDevkitUsage = true;
break;
}
}
// Use @angular/build directly if there is no devkit package usage
if (!hasAngularDevkitUsage) {
const karmaConfigFiles = new Set();
for (const [, target] of (0, workspace_1.allWorkspaceTargets)(workspace)) {
switch (target.builder) {
case workspace_models_1.Builders.Application:
target.builder = '@angular/build:application';
break;
case workspace_models_1.Builders.DevServer:
target.builder = '@angular/build:dev-server';
break;
case workspace_models_1.Builders.ExtractI18n:
target.builder = '@angular/build:extract-i18n';
break;
case workspace_models_1.Builders.Karma:
target.builder = '@angular/build:karma';
for (const [, karmaOptions] of (0, workspace_1.allTargetOptions)(target)) {
// Remove "builderMode" option since the builder will always use "application"
delete karmaOptions['builderMode'];
// Collect custom karma configurations for @angular-devkit/build-angular plugin removal
const karmaConfig = karmaOptions['karmaConfig'];
if (karmaConfig && typeof karmaConfig === 'string') {
karmaConfigFiles.add(karmaConfig);
}
}
break;
case workspace_models_1.Builders.NgPackagr:
target.builder = '@angular/build:ng-packagr';
break;
}
}
// Add direct @angular/build dependencies and remove @angular-devkit/build-angular
rules.push((0, dependency_1.addDependency)('@angular/build', latest_versions_1.latestVersions.DevkitBuildAngular, {
type: dependency_1.DependencyType.Dev,
existing: dependency_1.ExistingBehavior.Replace,
}), (0, dependency_1.removeDependency)('@angular-devkit/build-angular'));
// Add less dependency if any projects contain a Less stylesheet file.
// This check does not consider Node.js packages due to the performance
// cost of searching such a large directory structure. A build time error
// will provide instructions to install the package in this case.
if ((0, stylesheet_updates_1.hasLessStylesheets)(tree)) {
rules.push((0, dependency_1.addDependency)('less', latest_versions_1.latestVersions['less'], {
type: dependency_1.DependencyType.Dev,
existing: dependency_1.ExistingBehavior.Skip,
}));
}
// Add postcss dependency if any projects have a custom postcss configuration file.
// The build system only supports files in a project root or workspace root with
// names of either 'postcss.config.json' or '.postcssrc.json'.
if ((0, stylesheet_updates_1.hasPostcssConfiguration)(tree, workspace)) {
rules.push((0, dependency_1.addDependency)('postcss', latest_versions_1.latestVersions['postcss'], {
type: dependency_1.DependencyType.Dev,
existing: dependency_1.ExistingBehavior.Replace,
}));
}
for (const karmaConfigFile of karmaConfigFiles) {
if (!tree.exists(karmaConfigFile)) {
continue;
}
try {
const originalKarmaConfigText = tree.readText(karmaConfigFile);
const updatedKarmaConfigText = originalKarmaConfigText
.replaceAll(`require('@angular-devkit/build-angular/plugins/karma'),`, '')
.replaceAll(`require('@angular-devkit/build-angular/plugins/karma')`, '');
if (updatedKarmaConfigText.includes('@angular-devkit/build-angular/plugins')) {
throw new Error('Migration does not support found usage of "@angular-devkit/build-angular".');
}
else {
tree.overwrite(karmaConfigFile, updatedKarmaConfigText);
}
}
catch (error) {
const reason = error instanceof Error ? `Reason: ${error.message}` : '';
context.logger.warn(`Unable to update custom karma configuration file ("${karmaConfigFile}"). ` +
reason +
'\nReferences to the "@angular-devkit/build-angular" package within the file may need to be removed manually.');
}
}
}
return (0, schematics_1.chain)(rules);
});
}
function deleteFile(path) {
return (tree) => {
tree.delete(path);
};
}
function updateJsonFile(path, updater) {
return (tree, ctx) => {
if (tree.exists(path)) {
updater(new json_file_1.JSONFile(tree, path));
}
else {
ctx.logger.info(`Skipping updating '${path}' as it does not exist.`);
}
};
}
/**
* Migration main entrypoint
*/
function default_1() {
return (0, schematics_1.chain)([
updateProjects,
// Delete package.json helper scripts
updateJsonFile('package.json', (pkgJson) => ['build:ssr', 'dev:ssr', 'serve:ssr', 'prerender'].forEach((s) => pkgJson.remove(['scripts', s]))),
// Update main tsconfig
updateJsonFile('tsconfig.json', (rootJson) => {
rootJson.modify(['compilerOptions', 'esModuleInterop'], true);
rootJson.modify(['compilerOptions', 'downlevelIteration'], undefined);
rootJson.modify(['compilerOptions', 'allowSyntheticDefaultImports'], undefined);
}),
]);
}

View File

@@ -0,0 +1,36 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { Tree } from '@angular-devkit/schematics';
import { TargetDefinition, WorkspaceDefinition } from '../../utility/workspace';
/**
* Searches the schematic tree for files that have a `.less` extension.
* This is used to determine if the `less` package should be added as a dependency.
*
* @param tree A Schematics tree instance to search.
* @returns `true` if Less stylesheet files are found; otherwise, `false`.
*/
export declare function hasLessStylesheets(tree: Tree): boolean;
/**
* Searches for a PostCSS configuration file within the workspace root or any of the project roots.
* This is used to determine if the `postcss` package should be added as a dependency.
*
* @param tree A Schematics tree instance to search.
* @param workspace A Workspace to check for projects.
* @returns `true` if a PostCSS configuration file is found; otherwise, `false`.
*/
export declare function hasPostcssConfiguration(tree: Tree, workspace: WorkspaceDefinition): boolean;
/**
* The main orchestrator function for updating stylesheets.
* It iterates through all stylesheets in a project, analyzes them, and applies the necessary
* changes to the files and the build configuration.
*
* @param tree A Schematics tree instance.
* @param projectSourceRoot The source root of the project being updated.
* @param buildTarget The build target of the project being updated.
*/
export declare function updateStyleImports(tree: Tree, projectSourceRoot: string, buildTarget: TargetDefinition): void;

View File

@@ -0,0 +1,219 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasLessStylesheets = hasLessStylesheets;
exports.hasPostcssConfiguration = hasPostcssConfiguration;
exports.updateStyleImports = updateStyleImports;
const posix_1 = require("node:path/posix");
const css_import_lexer_1 = require("./css-import-lexer");
/** A list of all supported SASS style extensions.
* Order of extension is important and matches Sass behavior.
*/
const SASS_EXTENSIONS = ['.scss', '.sass', '.css'];
/** The prefix used to indicate a SASS partial file. */
const SASS_PARTIAL_PREFIX = '_';
/**
* Searches the schematic tree for files that have a `.less` extension.
* This is used to determine if the `less` package should be added as a dependency.
*
* @param tree A Schematics tree instance to search.
* @returns `true` if Less stylesheet files are found; otherwise, `false`.
*/
function hasLessStylesheets(tree) {
const directories = [tree.getDir('/')];
let current;
while ((current = directories.pop())) {
for (const path of current.subfiles) {
if (path.endsWith('.less')) {
return true;
}
}
for (const path of current.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
directories.push(current.dir(path));
}
}
return false;
}
/**
* Searches for a PostCSS configuration file within the workspace root or any of the project roots.
* This is used to determine if the `postcss` package should be added as a dependency.
*
* @param tree A Schematics tree instance to search.
* @param workspace A Workspace to check for projects.
* @returns `true` if a PostCSS configuration file is found; otherwise, `false`.
*/
function hasPostcssConfiguration(tree, workspace) {
const projectRoots = [...workspace.projects.values()].map((p) => p.root).filter(Boolean);
const searchDirectories = new Set(['', ...projectRoots]);
for (const dir of searchDirectories) {
if (tree.exists((0, posix_1.join)(dir, 'postcss.config.json')) ||
tree.exists((0, posix_1.join)(dir, '.postcssrc.json'))) {
return true;
}
}
return false;
}
/**
* Recursively visits all stylesheet files in a directory and yields their path and content.
*
* @param directory The directory to visit.
*/
function* visitStylesheets(directory) {
for (const path of directory.subfiles) {
if (path.endsWith('.css') || path.endsWith('.scss') || path.endsWith('.sass')) {
const entry = directory.file(path);
if (entry) {
yield [entry.path, entry.content.toString()];
}
}
}
for (const path of directory.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
yield* visitStylesheets(directory.dir(path));
}
}
/**
* Determines if a Sass import is likely intended to be relative to the workspace root.
* This is considered true if the import cannot be resolved relative to the containing file,
* but can be resolved relative to the workspace root.
*
* @param specifier The import specifier to check.
* @param filePath The path of the file containing the import.
* @param tree A Schematics tree instance.
* @param fromImport Whether the specifier is from an `@import` rule.
* @returns `true` if the import is likely workspace-relative; otherwise, `false`.
*/
function isWorkspaceRelativeSassImport(specifier, filePath, tree, fromImport) {
const relativeBase = (0, posix_1.dirname)(filePath);
const potentialWorkspacePaths = [...potentialSassImports(specifier, '/', fromImport)];
if (potentialWorkspacePaths.some((p) => tree.exists(p))) {
const potentialRelativePaths = [...potentialSassImports(specifier, relativeBase, fromImport)];
return potentialRelativePaths.every((p) => !tree.exists(p));
}
return false;
}
/**
* Analyzes a single stylesheet's content for import patterns that need to be updated.
*
* @param filePath The path of the stylesheet file.
* @param content The content of the stylesheet file.
* @param tree A Schematics tree instance.
* @returns A `StylesheetAnalysis` object containing the results of the analysis.
*/
function analyzeStylesheet(filePath, content, tree) {
const isSass = filePath.endsWith('.scss') || filePath.endsWith('.sass');
const analysis = {
needsWorkspaceIncludePath: false,
externalDependencies: new Set(),
contentChanges: [],
};
for (const { start, specifier, fromUse } of (0, css_import_lexer_1.findImports)(content, isSass)) {
if (specifier.startsWith('~')) {
analysis.contentChanges.push({ start: start + 1, length: 1 });
}
else if (specifier.startsWith('^')) {
analysis.contentChanges.push({ start: start + 1, length: 1 });
analysis.externalDependencies.add(specifier.slice(1));
}
else if (isSass && isWorkspaceRelativeSassImport(specifier, filePath, tree, !fromUse)) {
analysis.needsWorkspaceIncludePath = true;
}
}
return analysis;
}
/**
* The main orchestrator function for updating stylesheets.
* It iterates through all stylesheets in a project, analyzes them, and applies the necessary
* changes to the files and the build configuration.
*
* @param tree A Schematics tree instance.
* @param projectSourceRoot The source root of the project being updated.
* @param buildTarget The build target of the project being updated.
*/
function updateStyleImports(tree, projectSourceRoot, buildTarget) {
const allExternalDeps = new Set();
let projectNeedsIncludePath = false;
for (const [path, content] of visitStylesheets(tree.getDir(projectSourceRoot))) {
const { needsWorkspaceIncludePath, externalDependencies, contentChanges } = analyzeStylesheet(path, content, tree);
if (needsWorkspaceIncludePath) {
projectNeedsIncludePath = true;
}
for (const dep of externalDependencies) {
allExternalDeps.add(dep);
}
if (contentChanges.length > 0) {
const updater = tree.beginUpdate(path);
// Apply changes in reverse to avoid index shifting
for (const change of contentChanges.sort((a, b) => b.start - a.start)) {
updater.remove(change.start, change.length);
}
tree.commitUpdate(updater);
}
}
if (projectNeedsIncludePath) {
buildTarget.options ??= {};
const styleOptions = (buildTarget.options['stylePreprocessorOptions'] ??= {});
const includePaths = (styleOptions['includePaths'] ??= []);
if (Array.isArray(includePaths)) {
includePaths.push('.');
}
}
if (allExternalDeps.size > 0) {
buildTarget.options ??= {};
const externalDeps = (buildTarget.options['externalDependencies'] ??=
[]);
if (Array.isArray(externalDeps)) {
externalDeps.push(...allExternalDeps);
}
}
}
/**
* A helper generator that yields potential Sass import candidates for a given filename and extensions.
*
* @param directory The directory in which to resolve the candidates.
* @param filename The base filename of the import.
* @param extensions The file extensions to try.
* @param fromImport Whether the specifier is from an `@import` rule.
* @returns An iterable of potential import file paths.
*/
function* yieldSassImportCandidates(directory, filename, extensions, fromImport) {
if (fromImport) {
for (const ext of extensions) {
yield (0, posix_1.join)(directory, filename + '.import' + ext);
yield (0, posix_1.join)(directory, SASS_PARTIAL_PREFIX + filename + '.import' + ext);
}
}
for (const ext of extensions) {
yield (0, posix_1.join)(directory, filename + ext);
yield (0, posix_1.join)(directory, SASS_PARTIAL_PREFIX + filename + ext);
}
}
/**
* Generates a sequence of potential file paths that the Sass compiler would attempt to resolve
* for a given import specifier, following the official Sass resolution algorithm.
* Based on https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart
*
* @param specifier The import specifier to resolve.
* @param base The base path from which to resolve the specifier.
* @param fromImport Whether the specifier is from an `@import` rule.
* @returns An iterable of potential file paths.
*/
function* potentialSassImports(specifier, base, fromImport) {
const directory = (0, posix_1.join)(base, (0, posix_1.dirname)(specifier));
const extension = (0, posix_1.extname)(specifier);
const hasStyleExtension = SASS_EXTENSIONS.includes(extension);
const filename = (0, posix_1.basename)(specifier, hasStyleExtension ? extension : undefined);
const extensionsToTry = hasStyleExtension ? [extension] : SASS_EXTENSIONS;
yield* yieldSassImportCandidates(directory, filename, extensionsToTry, fromImport);
}