devextreme-schematics 1.11.2 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ import { Tree } from '@angular-devkit/schematics';
2
+ import * as parse5 from 'parse5';
3
+ import * as path from 'path';
4
+ import picomatch from 'picomatch';
5
+
6
+ // 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
+ }
31
+
32
+ // Minimal parse5 types for our usage
33
+ interface P5Node { [k: string]: any; }
34
+ interface P5Element extends P5Node { tagName?: string; }
35
+ interface P5Document extends P5Node {
36
+ documentElement?: P5Element;
37
+ }
38
+
39
+ export interface HostRule {
40
+ hostSelector: string;
41
+ configMap: Record<string, string>;
42
+ }
43
+
44
+ export interface RunnerOptions {
45
+ includeGlobs: string[];
46
+ rules: HostRule[];
47
+ }
48
+
49
+ export interface ExecOptions {
50
+ dryRun: boolean;
51
+ logger: { info: (s: string) => void; warn: (s: string) => void };
52
+ }
53
+
54
+ // Transform external HTML template files matched by includeGlobs.
55
+ export async function applyHostAwareTemplateMigrations(
56
+ tree: Tree,
57
+ runner: RunnerOptions,
58
+ exec: ExecOptions
59
+ ): Promise<void> {
60
+ const matcher = picomatch(runner.includeGlobs.length ? runner.includeGlobs : ['**/*.html']);
61
+
62
+ tree.visit(filePath => {
63
+ if (!matcher(filePath)) {
64
+ return;
65
+ }
66
+ if (!filePath.endsWith('.html')) {
67
+ return;
68
+ }
69
+
70
+ const buffer = tree.read(filePath);
71
+ if (!buffer) {
72
+ return;
73
+ }
74
+
75
+ const source = buffer.toString('utf8');
76
+ const { updated, changeCount } = transformTemplate(source, runner.rules);
77
+ if (changeCount === 0) {
78
+ return;
79
+ }
80
+ if (exec.dryRun) {
81
+ exec.logger.info(`[dry] ${filePath} → ${changeCount} changes`);
82
+ } else {
83
+ exec.logger.info(`${filePath} → ${changeCount} changes`);
84
+ tree.overwrite(filePath, updated);
85
+ }
86
+ });
87
+ }
88
+
89
+ // Apply migrations inside inline component templates found in TS/JS files.
90
+ export async function applyInlineComponentTemplateMigrations(
91
+ tree: Tree,
92
+ runner: RunnerOptions,
93
+ exec: ExecOptions,
94
+ scriptGlobs: string[]
95
+ ): Promise<void> {
96
+ if (!scriptGlobs.length) {
97
+ return;
98
+ }
99
+ if (!ts) {
100
+ exec.logger.warn(
101
+ '[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.'
108
+ );
109
+ return;
110
+ }
111
+ const matcher = picomatch(scriptGlobs);
112
+ tree.visit(filePath => {
113
+ if (!matcher(filePath)) {
114
+ return;
115
+ }
116
+ if (!(filePath.endsWith('.ts') || filePath.endsWith('.js'))) {
117
+ return;
118
+ }
119
+
120
+ const buffer = tree.read(filePath);
121
+ if (!buffer) {
122
+ return;
123
+ }
124
+ const sourceText = buffer.toString('utf8');
125
+
126
+ const sf = ts.createSourceFile(
127
+ filePath, sourceText, ts.ScriptTarget.ES2022, true,
128
+ filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS);
129
+
130
+ interface TemplateEdit { start: number; end: number; text: string; changes: number; }
131
+ const edits: TemplateEdit[] = [];
132
+
133
+ function visit(node: any) {
134
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
135
+ const call = node.expression;
136
+ if (ts.isIdentifier(call.expression) && call.expression.text === 'Component' && call.arguments.length) {
137
+ const arg = call.arguments[0];
138
+ if (ts.isObjectLiteralExpression(arg)) {
139
+ for (const prop of arg.properties) {
140
+ if (!ts.isPropertyAssignment(prop)) {
141
+ continue;
142
+ }
143
+ const name = prop.name;
144
+ const propName = ts.isIdentifier(name) ? name.text : ts.isStringLiteral(name) ? name.text : undefined;
145
+ if (propName !== 'template') {
146
+ continue;
147
+ }
148
+ const init = prop.initializer;
149
+ if (ts.isStringLiteral(init)) {
150
+ const raw = init.text;
151
+ const { updated, changeCount } = transformTemplate(raw, runner.rules);
152
+ if (changeCount > 0) {
153
+ const quote = init.getText().startsWith("'") ? '"' : init.getText()[0];
154
+ const newLiteral = quote + updated.replace(new RegExp(quote, 'g'), '\\' + quote) + quote;
155
+ edits.push({ start: init.getStart(), end: init.getEnd(), text: newLiteral, changes: changeCount });
156
+ }
157
+ } else if (ts.isNoSubstitutionTemplateLiteral(init)) {
158
+ const raw = init.text;
159
+ const { updated, changeCount } = transformTemplate(raw, runner.rules);
160
+ if (changeCount > 0) {
161
+ const newLiteral = '`' + escapeBackticks(updated) + '`';
162
+ edits.push({ start: init.getStart(), end: init.getEnd(), text: newLiteral, changes: changeCount });
163
+ }
164
+ } else if (ts.isTemplateExpression(init)) {
165
+ const { placeholderContent, placeholders } = flattenTemplateExpression(init, sourceText);
166
+ const { updated, changeCount } = transformTemplate(placeholderContent, runner.rules);
167
+ if (changeCount > 0) {
168
+ let rebuilt = updated;
169
+ placeholders.forEach(placeholder => {
170
+ rebuilt = rebuilt.replace(new RegExp(placeholder.token, 'g'), placeholder.fullText);
171
+ });
172
+ const newLiteral = '`' + escapeBackticks(rebuilt) + '`';
173
+ edits.push({ start: init.getStart(), end: init.getEnd(), text: newLiteral, changes: changeCount });
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ts.forEachChild(node, visit);
181
+ }
182
+ visit(sf);
183
+
184
+ if (!edits.length) {
185
+ return;
186
+ }
187
+ edits.sort((a, b) => b.start - a.start); // edits from last to first
188
+ let updatedFile = sourceText;
189
+ for (const edit of edits) {
190
+ updatedFile = updatedFile.slice(0, edit.start) + edit.text + updatedFile.slice(edit.end);
191
+ }
192
+ const totalChanges = edits.reduce((acc, edit) => acc + edit.changes, 0);
193
+ if (exec.dryRun) {
194
+ exec.logger.info(
195
+ `[dry] ${filePath} (inline templates) → ${totalChanges} changes in ${edits.length} template(s)`);
196
+ } else {
197
+ exec.logger.info(
198
+ `${filePath} (inline templates) → ${totalChanges} changes in ${edits.length} template(s)`);
199
+ tree.overwrite(filePath, updatedFile);
200
+ }
201
+ });
202
+ }
203
+
204
+ export function transformTemplate(
205
+ source: string,
206
+ rules: RunnerOptions['rules']
207
+ ): { updated: string; changeCount: number } {
208
+ const document = parse5.parse(source, { sourceCodeLocationInfo: true }) as unknown as P5Document;
209
+ const replacements: Array<{ start: number; end: number; text: string }> = [];
210
+
211
+ const hostSelectorSet = new Set(rules.map(rule => rule.hostSelector));
212
+
213
+ const seenOpens = new Set<number>();
214
+ const seenCloses = new Set<number>();
215
+
216
+ function walkHost(root: P5Element, visitFn: (node: P5Node) => void) {
217
+ function recursiveWalk(node: P5Node) {
218
+ visitFn(node);
219
+ const childNodes = (node as any).childNodes as P5Node[] | undefined;
220
+ if (!childNodes) {
221
+ return;
222
+ }
223
+ for (const childNode of childNodes) {
224
+ if (isElement(childNode) && hostSelectorSet.has((childNode as any).tagName) && childNode !== root) {
225
+ continue;
226
+ }
227
+ recursiveWalk(childNode);
228
+ }
229
+ }
230
+ recursiveWalk(root);
231
+ }
232
+
233
+ for (const rule of rules) {
234
+ const hosts = findElementsByTag(document, rule.hostSelector);
235
+ for (const host of hosts) {
236
+ if (!(host as any).sourceCodeLocation) {
237
+ continue;
238
+ }
239
+ walkHost(host, (node) => {
240
+ if (!isElement(node)) {
241
+ return;
242
+ }
243
+ const oldName: string | undefined = (node as any).tagName;
244
+ if (!oldName) {
245
+ return;
246
+ }
247
+ const newName = rule.configMap[oldName];
248
+ if (!newName) {
249
+ return;
250
+ }
251
+ const loc: any = (node as any).sourceCodeLocation;
252
+ if (!loc) {
253
+ return;
254
+ }
255
+ if (loc.startTag) {
256
+ const openStart = loc.startTag.startOffset + 1;
257
+ const openEnd = openStart + oldName.length;
258
+ if (!seenOpens.has(openStart)) {
259
+ seenOpens.add(openStart);
260
+ replacements.push({ start: openStart, end: openEnd, text: newName });
261
+ }
262
+ }
263
+ if (loc.endTag) {
264
+ const endStart = loc.endTag.startOffset + 2;
265
+ const endEnd = endStart + oldName.length;
266
+ if (!seenCloses.has(endStart)) {
267
+ seenCloses.add(endStart);
268
+ replacements.push({ start: endStart, end: endEnd, text: newName });
269
+ }
270
+ }
271
+ });
272
+ }
273
+ }
274
+
275
+ if (!replacements.length) {
276
+ return { updated: source, changeCount: 0 };
277
+ }
278
+ replacements.sort((a, b) => b.start - a.start);
279
+ let updated = source;
280
+ for (const replacement of replacements) {
281
+ updated = updated.slice(0, replacement.start) + replacement.text + updated.slice(replacement.end);
282
+ }
283
+ return { updated, changeCount: replacements.length };
284
+ }
285
+
286
+ // Helpers
287
+
288
+ function flattenTemplateExpression(
289
+ node: any,
290
+ source: string
291
+ ): { placeholderContent: string; placeholders: Array<{ token: string; fullText: string }> } {
292
+ const placeholders: Array<{ token: string; fullText: string }> = [];
293
+ let content = node.head.text;
294
+ node.templateSpans.forEach((span: any, i: number) => {
295
+ const token = `__NG_EXPR_PLACEHOLDER_${i}__`;
296
+ const fullText = '${' + source.slice(span.expression.getStart(), span.expression.getEnd()) + '}';
297
+ placeholders.push({ token, fullText });
298
+ content += token + span.literal.text;
299
+ });
300
+ return { placeholderContent: content, placeholders };
301
+ }
302
+
303
+ function escapeBackticks(text: string): string {
304
+ // First escape backslashes, then escape backticks
305
+ return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
306
+ }
307
+
308
+ // Utilities
309
+
310
+ function isElement(n: P5Node): n is P5Element {
311
+ return (n as any).tagName !== undefined;
312
+ }
313
+
314
+ function walk(node: P5Node, visit: (n: P5Node) => void) {
315
+ visit(node);
316
+ const childNodes = (node as any).childNodes as P5Node[] | undefined;
317
+ if (!childNodes) {
318
+ return;
319
+ }
320
+ for (const childNode of childNodes) {
321
+ walk(childNode, visit);
322
+ }
323
+ }
324
+
325
+ function findElementsByTag(doc: P5Document | P5Element, tag: string): P5Element[] {
326
+ const result: P5Element[] = [];
327
+ walk(doc as unknown as P5Node, (node) => {
328
+ if (isElement(node) && node.tagName === tag) {
329
+ result.push(node);
330
+ }
331
+ });
332
+ return result;
333
+ }
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.latestVersions = void 0;
4
4
  // TODO: implement
5
5
  exports.latestVersions = {
6
- 'devextreme': '25.1.5',
7
- 'devextreme-angular': '25.1.5',
6
+ 'devextreme': '25.1.6',
7
+ 'devextreme-angular': '25.1.6',
8
8
  'devextreme-cli': 'latest',
9
9
  'sass-embedded': '1.66.0'
10
10
  };
@@ -1,7 +1,7 @@
1
1
  // TODO: implement
2
2
  export const latestVersions = {
3
- 'devextreme': '25.1.5',
4
- 'devextreme-angular': '25.1.5',
3
+ 'devextreme': '25.1.6',
4
+ 'devextreme-angular': '25.1.6',
5
5
  'devextreme-cli': 'latest',
6
6
  'sass-embedded': '1.66.0'
7
7
  };