-
Notifications
You must be signed in to change notification settings - Fork 12k
fix(@angular/cli): direct Angular schematic dependency requests to known versions #19857
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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.io/license | ||
*/ | ||
import { RuleFactory, SchematicsException, Tree } from '@angular-devkit/schematics'; | ||
import { NodeModulesEngineHost } from '@angular-devkit/schematics/tools'; | ||
import { readFileSync } from 'fs'; | ||
import { parse as parseJson } from 'jsonc-parser'; | ||
import { dirname, resolve } from 'path'; | ||
import { Script } from 'vm'; | ||
|
||
/** | ||
* Environment variable to control schematic package redirection | ||
* Default: Angular schematics only | ||
*/ | ||
const schematicRedirectVariable = process.env['NG_SCHEMATIC_REDIRECT']?.toLowerCase(); | ||
|
||
function shouldWrapSchematic(schematicFile: string): boolean { | ||
// Check environment variable if present | ||
if (schematicRedirectVariable !== undefined) { | ||
switch (schematicRedirectVariable) { | ||
case '0': | ||
case 'false': | ||
case 'off': | ||
case 'none': | ||
return false; | ||
case 'all': | ||
return true; | ||
} | ||
} | ||
|
||
// Never wrap `@schematics/update` when executed directly | ||
// It communicates with the update command via `global` | ||
if (/[\/\\]node_modules[\/\\]@schematics[\/\\]update[\/\\]/.test(schematicFile)) { | ||
return false; | ||
} | ||
|
||
// Default is only first-party Angular schematic packages | ||
// Angular schematics are safe to use in the wrapped VM context | ||
return /[\/\\]node_modules[\/\\]@(?:angular|schematics|nguniversal)[\/\\]/.test(schematicFile); | ||
} | ||
|
||
export class SchematicEngineHost extends NodeModulesEngineHost { | ||
protected _resolveReferenceString(refString: string, parentPath: string) { | ||
const [path, name] = refString.split('#', 2); | ||
// Mimic behavior of ExportStringRef class used in default behavior | ||
const fullPath = path[0] === '.' ? resolve(parentPath ?? process.cwd(), path) : path; | ||
|
||
const schematicFile = require.resolve(fullPath, { paths: [parentPath] }); | ||
|
||
if (shouldWrapSchematic(schematicFile)) { | ||
const schematicPath = dirname(schematicFile); | ||
|
||
const moduleCache = new Map<string, unknown>(); | ||
const factoryInitializer = wrap( | ||
schematicFile, | ||
schematicPath, | ||
moduleCache, | ||
name || 'default', | ||
) as () => RuleFactory<{}>; | ||
|
||
const factory = factoryInitializer(); | ||
if (!factory || typeof factory !== 'function') { | ||
return null; | ||
} | ||
|
||
return { ref: factory, path: schematicPath }; | ||
} | ||
|
||
// All other schematics use default behavior | ||
return super._resolveReferenceString(refString, parentPath); | ||
} | ||
} | ||
|
||
/** | ||
* Minimal shim modules for legacy deep imports of `@schematics/angular` | ||
*/ | ||
const legacyModules: Record<string, unknown> = { | ||
'@schematics/angular/utility/config': { | ||
getWorkspace(host: Tree) { | ||
const path = '/.angular.json'; | ||
const data = host.read(path); | ||
if (!data) { | ||
throw new SchematicsException(`Could not find (${path})`); | ||
} | ||
|
||
return parseJson(data.toString(), [], { allowTrailingComma: true }); | ||
}, | ||
}, | ||
'@schematics/angular/utility/project': { | ||
buildDefaultPath(project: { sourceRoot?: string; root: string; projectType: string }): string { | ||
const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`; | ||
|
||
return `${root}${project.projectType === 'application' ? 'app' : 'lib'}`; | ||
}, | ||
}, | ||
}; | ||
|
||
/** | ||
* Wrap a JavaScript file in a VM context to allow specific Angular dependencies to be redirected. | ||
* This VM setup is ONLY intended to redirect dependencies. | ||
* | ||
* @param schematicFile A JavaScript schematic file path that should be wrapped. | ||
* @param schematicDirectory A directory that will be used as the location of the JavaScript file. | ||
* @param moduleCache A map to use for caching repeat module usage and proper `instanceof` support. | ||
* @param exportName An optional name of a specific export to return. Otherwise, return all exports. | ||
*/ | ||
function wrap( | ||
schematicFile: string, | ||
schematicDirectory: string, | ||
moduleCache: Map<string, unknown>, | ||
exportName?: string, | ||
): () => unknown { | ||
const { createRequire, createRequireFromPath } = require('module'); | ||
// Node.js 10.x does not support `createRequire` so fallback to `createRequireFromPath` | ||
// `createRequireFromPath` is deprecated in 12+ and can be removed once 10.x support is removed | ||
const scopedRequire = createRequire?.(schematicFile) || createRequireFromPath(schematicFile); | ||
|
||
const customRequire = function (id: string) { | ||
if (legacyModules[id]) { | ||
// Provide compatibility modules for older versions of @angular/cdk | ||
return legacyModules[id]; | ||
} else if (id.startsWith('@angular-devkit/') || id.startsWith('@schematics/')) { | ||
// Resolve from inside the `@angular/cli` project | ||
const packagePath = require.resolve(id); | ||
|
||
return require(packagePath); | ||
} else if (id.startsWith('.') || id.startsWith('@angular/cdk')) { | ||
clydin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Wrap relative files inside the schematic collection | ||
// Also wrap `@angular/cdk`, it contains helper utilities that import core schematic packages | ||
|
||
// Resolve from the original file | ||
const modulePath = scopedRequire.resolve(id); | ||
|
||
// Use cached module if available | ||
const cachedModule = moduleCache.get(modulePath); | ||
if (cachedModule) { | ||
return cachedModule; | ||
} | ||
|
||
// Do not wrap vendored third-party packages or JSON files | ||
if ( | ||
!/[\/\\]node_modules[\/\\]@schematics[\/\\]angular[\/\\]third_party[\/\\]/.test( | ||
modulePath, | ||
) && | ||
!modulePath.endsWith('.json') | ||
) { | ||
// Wrap module and save in cache | ||
const wrappedModule = wrap(modulePath, dirname(modulePath), moduleCache)(); | ||
moduleCache.set(modulePath, wrappedModule); | ||
|
||
return wrappedModule; | ||
} | ||
} | ||
|
||
// All others are required directly from the original file | ||
return scopedRequire(id); | ||
}; | ||
|
||
// Setup a wrapper function to capture the module's exports | ||
const schematicCode = readFileSync(schematicFile, 'utf8'); | ||
// `module` is required due to @angular/localize ng-add being in UMD format | ||
const headerCode = '(function() {\nvar exports = {};\nvar module = { exports };\n'; | ||
const footerCode = exportName ? `\nreturn exports['${exportName}'];});` : '\nreturn exports;});'; | ||
|
||
const script = new Script(headerCode + schematicCode + footerCode, { | ||
filename: schematicFile, | ||
lineOffset: 3, | ||
}); | ||
|
||
const context = { | ||
__dirname: schematicDirectory, | ||
__filename: schematicFile, | ||
Buffer, | ||
console, | ||
process, | ||
get global() { | ||
return this; | ||
}, | ||
require: customRequire, | ||
}; | ||
|
||
const exportsFactory = script.runInNewContext(context); | ||
|
||
return exportsFactory; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { join } from 'path'; | ||
import { expectFileToMatch } from '../../utils/fs'; | ||
import { ng } from '../../utils/process'; | ||
import { installPackage, uninstallPackage } from '../../utils/packages'; | ||
import { isPrereleaseCli } from '../../utils/project'; | ||
|
||
export default async function () { | ||
const componentDir = join('src', 'app', 'test-component'); | ||
|
||
// Install old and incompatible version | ||
// Must directly use npm registry since old versions are not hosted locally | ||
await installPackage('@schematics/angular@7', 'https://registry.npmjs.org') | ||
|
||
const tag = await isPrereleaseCli() ? '@next' : ''; | ||
await ng('add', `@angular/material${tag}`); | ||
await expectFileToMatch('package.json', /@angular\/material/); | ||
|
||
// Clean up existing cdk package | ||
// Not doing so can cause adding material to fail if an incompatible cdk is present | ||
await uninstallPackage('@angular/cdk'); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call to add a shim