Skip to content

Commit b272e35

Browse files
CLI: Fix migration tool dependencies issue and some refactor (#1005)
* Fix migration tool dependencies and small refactor * Apply suggestions from code review Co-authored-by: Arman Boyakhchyan <[email protected]> * smart typescript fallback * Update packages/devextreme-schematics/src/migrate-config-components/template-migrator.ts Co-authored-by: Arman Boyakhchyan <[email protected]> --------- Co-authored-by: Arman Boyakhchyan <[email protected]>
1 parent 35feaf8 commit b272e35

File tree

4 files changed

+242
-93
lines changed

4 files changed

+242
-93
lines changed

packages/devextreme-cli/src/applications/application.angular.js

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,48 @@ async function runNgCommand(commandArguments, commandOptions, commandConfig) {
5353
}
5454

5555
function localPackageExists(packageName) {
56-
// Check local node_modules first
5756
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
5857
if(fs.existsSync(nodeModulesPath)) {
5958
const packageJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
6059
if(fs.existsSync(packageJsonPath)) {
6160
return true;
6261
}
6362
}
63+
return false;
64+
}
65+
66+
function getLocalCollectionPath(packageName) {
67+
const nodeModulesPath = path.join(process.cwd(), 'node_modules', packageName, 'src', 'collection.json');
68+
if(fs.existsSync(nodeModulesPath)) {
69+
return nodeModulesPath;
70+
}
71+
return null;
72+
}
73+
74+
function getCollectionPath(packageName) {
75+
const localPath = getLocalCollectionPath(packageName);
76+
if(localPath) {
77+
return localPath;
78+
}
6479

65-
// Check if globally installed by trying to resolve the package
6680
try {
67-
require.resolve(`${packageName}/package.json`);
81+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
82+
const collectionPath = path.join(path.dirname(packageJsonPath), 'src', 'collection.json');
83+
if(fs.existsSync(collectionPath)) {
84+
return collectionPath;
85+
}
86+
} catch(e) {}
87+
88+
return null;
89+
}
90+
91+
function schematicsCliExists() {
92+
try {
93+
require.resolve('@angular-devkit/schematics-cli/package.json');
6894
return true;
6995
} catch(e) {
70-
// Package not found globally
96+
return false;
7197
}
72-
73-
return false;
7498
}
7599

76100
const hasSutableNgCli = async() => {
@@ -165,33 +189,22 @@ const addView = (viewName, options) => {
165189

166190
const migrateConfigComponents = async(options = {}) => {
167191
const collectionName = 'devextreme-schematics';
192+
const collectionPath = getCollectionPath(collectionName);
168193

169-
// Check if devextreme-schematics is installed
170-
if(!localPackageExists(collectionName)) {
194+
if(!collectionPath) {
171195
const prompts = require('prompts');
172196

173197
console.log(`\nThe '${collectionName}' package is required to run this command.`);
174198

175199
const response = await prompts({
176200
type: 'confirm',
177201
name: 'install',
178-
message: `Would you like to install '${collectionName}' now?`,
202+
message: `Would you like to install '${collectionName}@${schematicsVersion}' in the npm cache?`,
179203
initial: true
180204
});
181205

182206
if(!response.install) {
183-
console.log('Migration cancelled. Please install devextreme-schematics manually:');
184-
console.log(`npm install -g ${collectionName}@${schematicsVersion}`);
185-
process.exit(1);
186-
}
187-
188-
console.log(`Installing ${collectionName}@${schematicsVersion}...`);
189-
try {
190-
await runCommand('npm', ['install', '-g', `${collectionName}@${schematicsVersion}`], { stdio: 'inherit' });
191-
console.log('Installation completed successfully.');
192-
} catch(error) {
193-
console.error('Failed to install devextreme-schematics. Please install manually:');
194-
console.error(`npm install -g ${collectionName}@${schematicsVersion}`);
207+
console.log('Migration cancelled. Install devextreme-schematics manually and rerun the command.');
195208
process.exit(1);
196209
}
197210
}
@@ -200,14 +213,22 @@ const migrateConfigComponents = async(options = {}) => {
200213
...options
201214
};
202215

203-
if(schematicOptions.include && typeof schematicOptions.include === 'string') {
204-
schematicOptions.include = schematicOptions.include.split(',').map(s => s.trim());
216+
const hasSchematicsCli = schematicsCliExists();
217+
const commandArguments = ['--yes'];
218+
219+
if(!hasSchematicsCli) {
220+
commandArguments.push('-p', '@angular-devkit/schematics-cli');
205221
}
206-
if(schematicOptions.scriptInclude && typeof schematicOptions.scriptInclude === 'string') {
207-
schematicOptions.scriptInclude = schematicOptions.scriptInclude.split(',').map(s => s.trim());
222+
223+
if(!collectionPath) {
224+
commandArguments.push('-p', `${collectionName}@${schematicsVersion}`);
208225
}
209226

210-
const commandArguments = ['schematics', `${collectionName}:migrate-config-components`];
227+
const collectionSpecifier = collectionPath
228+
? `${collectionPath.replace(/\\/g, '/')}:migrate-config-components`
229+
: `${collectionName}:migrate-config-components`;
230+
231+
commandArguments.push('schematics', collectionSpecifier);
211232

212233
const { [depsVersionTagOptionName]: _, ...optionsToArguments } = schematicOptions; // eslint-disable-line no-unused-vars
213234
for(let option in optionsToArguments) {

packages/devextreme-schematics/src/migrate-config-components/index.ts

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,15 @@ import { applyHostAwareTemplateMigrations, applyInlineComponentTemplateMigration
44
import mapping from './mappings/deprecated-config-map.json';
55

66
export interface Options {
7-
include?: string[];
8-
dry?: boolean;
9-
scriptInclude?: string[];
7+
include?: string | string[];
8+
dry?: boolean | string;
9+
scriptInclude?: string | string[];
1010
}
1111

1212
export function migrateConfigComponents(options: Options = {}): Rule {
13-
// Accept --include as array or comma-separated string
14-
let include: string[] = ['**/*.html'];
15-
const rawInclude = options.include;
16-
if (Array.isArray(rawInclude) && rawInclude.length) {
17-
include = rawInclude;
18-
} else if (typeof rawInclude === 'string' && rawInclude) {
19-
let str = (rawInclude as string).trim();
20-
if (str.startsWith('[') && str.endsWith(']')) {
21-
str = str.slice(1, -1);
22-
}
23-
include = str.split(',').map((s: string) => s.trim()).filter((s: string) => !!s);
24-
}
25-
26-
// Coerce string 'true'/'false' to boolean for dry option
27-
let dryFlag: boolean = false;
28-
if (typeof options.dry === 'string') {
29-
dryFlag = options.dry === 'true';
30-
} else {
31-
dryFlag = !!options.dry;
32-
}
33-
34-
// Accept --script-include as array or comma-separated string
35-
let scriptGlobs: string[] = ['**/*.ts', '**/*.js'];
36-
const rawScriptInclude = options.scriptInclude;
37-
if (Array.isArray(rawScriptInclude) && rawScriptInclude.length) {
38-
scriptGlobs = rawScriptInclude;
39-
} else if (typeof rawScriptInclude === 'string' && rawScriptInclude) {
40-
let str = (rawScriptInclude as string).trim();
41-
if (str.startsWith('[') && str.endsWith(']')) {
42-
str = str.slice(1, -1);
43-
}
44-
scriptGlobs = str.split(',').map((s: string) => s.trim()).filter((s: string) => !!s);
45-
}
13+
const include = normalizeGlobOption(options.include, ['**/*.html']);
14+
const dryFlag = normalizeBoolean(options.dry);
15+
const scriptGlobs = normalizeGlobOption(options.scriptInclude, ['**/*.ts', '**/*.js']);
4616

4717
return async (tree: Tree, ctx: SchematicContext): Promise<void> => {
4818
ctx.logger.info(`[config-migrator] Starting…`);
@@ -78,3 +48,30 @@ function componentToSelectorGuess(componentName: string): string {
7848
const core = componentName.replace(/Component$/, '').replace(/^Dx/, 'dx-');
7949
return core.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
8050
}
51+
52+
function normalizeGlobOption(
53+
value: Options['include'] | Options['scriptInclude'],
54+
fallback: string[]
55+
): string[] {
56+
if (Array.isArray(value)) {
57+
return value.filter(Boolean);
58+
}
59+
if (typeof value === 'string' && value.trim()) {
60+
const trimmed = value.trim();
61+
const inner = trimmed.startsWith('[') && trimmed.endsWith(']')
62+
? trimmed.slice(1, -1)
63+
: trimmed;
64+
return inner
65+
.split(',')
66+
.map(segment => segment.trim())
67+
.filter(Boolean);
68+
}
69+
return fallback.slice();
70+
}
71+
72+
function normalizeBoolean(value: Options['dry']): boolean {
73+
if (typeof value === 'string') {
74+
return value.toLowerCase() === 'true';
75+
}
76+
return !!value;
77+
}

packages/devextreme-schematics/src/migrate-config-components/template-migrator.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,13 @@
11
import { Tree } from '@angular-devkit/schematics';
22
import * as parse5 from 'parse5';
3-
import * as path from 'path';
43
import picomatch from 'picomatch';
4+
import { resolveTypeScript } from '../utility/typescript-resolver';
55

66
// Dynamically require TypeScript if available; skip inline template migration if not.
7-
let ts: any = null;
8-
const tsResolutionErrors: string[] = [];
9-
const tsResolutionPaths = [
10-
__dirname,
11-
process.cwd(),
12-
path.dirname(require.main?.filename || ''),
13-
];
14-
for (const p of tsResolutionPaths) {
15-
try {
16-
// tslint:disable-next-line:no-var-requires
17-
ts = require(require.resolve('typescript', { paths: [p] }));
18-
break;
19-
} catch (err) {
20-
tsResolutionErrors.push(`Failed to import TypeScript from ${p}: ${err?.message || err}`);
21-
}
22-
}
23-
if (!ts) {
24-
try {
25-
// tslint:disable-next-line:no-var-requires
26-
ts = require('typescript');
27-
} catch (err) {
28-
tsResolutionErrors.push(`Failed to import TypeScript: ${err?.message || err}`);
29-
}
30-
}
7+
// Uses a 3-level fallback: project search -> global search -> temporary install
8+
const tsResolution = resolveTypeScript();
9+
const ts: any = tsResolution.ts;
10+
const tsResolutionErrors: string[] = tsResolution.errors;
3111

3212
// Minimal parse5 types for our usage
3313
interface P5Node { [k: string]: any; }
@@ -97,14 +77,21 @@ export async function applyInlineComponentTemplateMigrations(
9777
return;
9878
}
9979
if (!ts) {
80+
const errorDetails = tsResolutionErrors.length
81+
? `Resolution attempts:\n${tsResolutionErrors.map(e => ' - ' + e).join('\n')}\n`
82+
: '';
10083
exec.logger.warn(
10184
'[config-migrator] Failed to import TypeScript. Skipping inline template migration.\n' +
102-
'Resolution attempts and errors:\n' +
103-
tsResolutionErrors.map(e => ' - ' + e).join('\n') + '\n' +
104-
'To resolve this issue, perform one of the following steps:\n' +
105-
' 1. Install the "typescript" package in your project root: `npm install typescript --save-dev`\n' +
106-
' 2. Install the "typescript" package globally on your machine: `npm install -g typescript`\n' +
107-
'Refer to the README for further troubleshooting information.'
85+
errorDetails +
86+
'The schematic attempted to import TypeScript from the following locations:\n' +
87+
' 1. Project node_modules\n' +
88+
' 2. Global node_modules\n' +
89+
' 3. Temporary installation (npm cache)\n\n' +
90+
'To resolve this issue, install TypeScript.\n\n' +
91+
'Project install:\n' +
92+
' npm install typescript --save-dev\n\n' +
93+
'Global install:\n' +
94+
' npm install -g typescript\n\n'
10895
);
10996
return;
11097
}

0 commit comments

Comments
 (0)