Skip to content

Commit 145bab5

Browse files
committed
smart typescript fallback
1 parent 336ec89 commit 145bab5

File tree

3 files changed

+162
-13
lines changed

3 files changed

+162
-13
lines changed

packages/devextreme-schematics/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
"@angular-devkit/schematics": "^17.3.17",
2727
"@schematics/angular": "^17.3.17",
2828
"parse5": "^7.3.0",
29-
"picomatch": "^4.0.3",
30-
"typescript": "~5.2.1"
29+
"picomatch": "^4.0.3"
3130
},
3231
"devDependencies": {
3332
"@types/jasmine": "~3.10.18",
@@ -36,6 +35,7 @@
3635
"@types/semver": "^7.7.1",
3736
"jasmine": "^2.99.0",
3837
"rxjs": "^6.6.7",
39-
"tslint": "^5.20.1"
38+
"tslint": "^5.20.1",
39+
"typescript": "~5.2.1"
4040
}
4141
}

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

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

56
// Dynamically require TypeScript if available; skip inline template migration if not.
6-
let ts: any = null;
7-
let tsResolutionError: string | null = null;
8-
try {
9-
// tslint:disable-next-line:no-var-requires
10-
ts = require('typescript');
11-
} catch (err) {
12-
tsResolutionError = err?.message || String(err);
13-
}
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;
1411

1512
// Minimal parse5 types for our usage
1613
interface P5Node { [k: string]: any; }
@@ -80,10 +77,18 @@ export async function applyInlineComponentTemplateMigrations(
8077
return;
8178
}
8279
if (!ts) {
80+
const errorDetails = tsResolutionErrors.length
81+
? `Resolution attempts:\n${tsResolutionErrors.map(e => ' - ' + e).join('\n')}\n`
82+
: '';
8383
exec.logger.warn(
8484
'[config-migrator] Failed to import TypeScript. Skipping inline template migration.\n' +
85-
(tsResolutionError ? `Error: ${tsResolutionError}\n` : '') +
86-
'Ensure the "typescript" package is installed in your project. Try to reinstall DevExtreme Schematics if the issue persists.'
85+
errorDetails +
86+
'The schematic attempted to find TypeScript using a 3-level fallback:\n' +
87+
' 1. Project search (your project\'s node_modules)\n' +
88+
' 2. Global search (global node_modules)\n' +
89+
' 3. Temporary install (automatic download)\n\n' +
90+
'To resolve this issue, install TypeScript in your project:\n' +
91+
' npm install typescript --save-dev\n\n'
8792
);
8893
return;
8994
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { execSync } from 'child_process';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
5+
export interface TypeScriptResolutionResult {
6+
ts: any | null;
7+
resolutionMethod: 'project' | 'global' | 'temporary' | null;
8+
errors: string[];
9+
}
10+
11+
/**
12+
* Resolves TypeScript with a 3-level fallback strategy:
13+
* 1. Project search - look in the user's project node_modules
14+
* 2. Global search - look in global node_modules
15+
* 3. Temporary install - use npx to temporarily install typescript
16+
*/
17+
export function resolveTypeScript(): TypeScriptResolutionResult {
18+
const errors: string[] = [];
19+
20+
try {
21+
const projectTs = tryResolveFromProject();
22+
if (projectTs) {
23+
return {
24+
ts: projectTs,
25+
resolutionMethod: 'project',
26+
errors: []
27+
};
28+
}
29+
} catch (err) {
30+
errors.push(`Project resolution failed: ${err?.message || err}`);
31+
}
32+
33+
try {
34+
const globalTs = tryResolveFromGlobal();
35+
if (globalTs) {
36+
return {
37+
ts: globalTs,
38+
resolutionMethod: 'global',
39+
errors: []
40+
};
41+
}
42+
} catch (err) {
43+
errors.push(`Global resolution failed: ${err?.message || err}`);
44+
}
45+
46+
try {
47+
const tempTs = tryResolveViaTemporaryInstall();
48+
if (tempTs) {
49+
return {
50+
ts: tempTs,
51+
resolutionMethod: 'temporary',
52+
errors: []
53+
};
54+
}
55+
} catch (err) {
56+
errors.push(`Temporary install failed: ${err?.message || err}`);
57+
}
58+
59+
return {
60+
ts: null,
61+
resolutionMethod: null,
62+
errors
63+
};
64+
}
65+
66+
function tryResolveFromProject(): any {
67+
const searchPaths = [
68+
process.cwd(),
69+
path.dirname(require.main?.filename || ''),
70+
];
71+
72+
for (const searchPath of searchPaths) {
73+
try {
74+
const tsPath = require.resolve('typescript', { paths: [searchPath] });
75+
// tslint:disable-next-line:no-var-requires
76+
return require(tsPath);
77+
} catch {
78+
// Continue to next path
79+
}
80+
}
81+
82+
return null;
83+
}
84+
85+
function tryResolveFromGlobal(): any {
86+
try {
87+
const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();
88+
const typescriptPath = path.join(globalPath, 'typescript');
89+
90+
if (fs.existsSync(typescriptPath)) {
91+
// tslint:disable-next-line:no-var-requires
92+
return require(typescriptPath);
93+
}
94+
} catch {
95+
// Fall through
96+
}
97+
98+
return null;
99+
}
100+
101+
function tryResolveViaTemporaryInstall(): any {
102+
try {
103+
const tmpDir = path.join(require('os').tmpdir(), 'devextreme-schematics-ts-' + Date.now());
104+
fs.mkdirSync(tmpDir, { recursive: true });
105+
106+
const packageJson = {
107+
name: 'temp-typescript-resolver',
108+
version: '1.0.0',
109+
dependencies: {
110+
typescript: 'latest'
111+
}
112+
};
113+
fs.writeFileSync(
114+
path.join(tmpDir, 'package.json'),
115+
JSON.stringify(packageJson, null, 2)
116+
);
117+
118+
execSync('npm install --silent --no-progress', {
119+
cwd: tmpDir,
120+
stdio: 'ignore',
121+
timeout: 30000
122+
});
123+
124+
const typescriptPath = path.join(tmpDir, 'node_modules', 'typescript');
125+
if (fs.existsSync(typescriptPath)) {
126+
// tslint:disable-next-line:no-var-requires
127+
const ts = require(typescriptPath);
128+
129+
setTimeout(() => {
130+
try {
131+
fs.rmSync(tmpDir, { recursive: true, force: true });
132+
} catch {
133+
// Ignore cleanup errors
134+
}
135+
}, 5000);
136+
137+
return ts;
138+
}
139+
} catch {
140+
// Fall through
141+
}
142+
143+
return null;
144+
}

0 commit comments

Comments
 (0)