@zenit-hosting/zenit-ui 0.1.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.
Files changed (36) hide show
  1. package/README.md +400 -0
  2. package/fesm2022/zenit-hosting-zenit-ui.mjs +11289 -0
  3. package/fesm2022/zenit-hosting-zenit-ui.mjs.map +1 -0
  4. package/llms-full.txt +5120 -0
  5. package/llms.txt +179 -0
  6. package/package.json +44 -0
  7. package/schematics/collection.json +15 -0
  8. package/schematics/migrate-material/component.js +241 -0
  9. package/schematics/migrate-material/index.js +463 -0
  10. package/schematics/migrate-material/report.js +125 -0
  11. package/schematics/migrate-material/schema.js +2 -0
  12. package/schematics/migrate-material/schema.json +32 -0
  13. package/schematics/migrate-material/tables.js +138 -0
  14. package/schematics/migrate-material/template.js +871 -0
  15. package/schematics/ng-add/html.js +151 -0
  16. package/schematics/ng-add/index.js +609 -0
  17. package/schematics/ng-add/init-script.js +108 -0
  18. package/schematics/ng-add/schema.js +2 -0
  19. package/schematics/ng-add/schema.json +34 -0
  20. package/schematics/package.json +3 -0
  21. package/styles/_daten.css +138 -0
  22. package/styles/_formulare.css +97 -0
  23. package/styles/_grundlage.css +424 -0
  24. package/styles/_konfigurator.css +216 -0
  25. package/styles/_navigation.css +179 -0
  26. package/styles/_overlays.css +86 -0
  27. package/styles/_rueckmeldung.css +100 -0
  28. package/styles/_werkzeuge.css +119 -0
  29. package/styles/themes/accents.css +91 -0
  30. package/styles/themes/base.css +26 -0
  31. package/styles/themes/contrast.css +67 -0
  32. package/styles/themes/light.css +74 -0
  33. package/styles/themes.css +21 -0
  34. package/styles/tokens.css +76 -0
  35. package/styles/zenit-ui.css +12 -0
  36. package/types/zenit-hosting-zenit-ui.d.ts +5957 -0
@@ -0,0 +1,463 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.migrateMaterial = migrateMaterial;
7
+ exports.mapOffset = mapOffset;
8
+ /**
9
+ * `ng generate zenit-ui:migrate-material --path src/app/billing`.
10
+ *
11
+ * Rewrites the mechanical Angular Material usages below `--path` to zenit-ui
12
+ * (`mat-icon`, the button attributes, `matTooltip`, `mat-spinner`, static
13
+ * `mat-chip`, and the `imports` of the components) and reports everything it
14
+ * left alone. See `docs/migrate-material.md`.
15
+ *
16
+ * - Templates are parsed with `@angular/compiler`, component files with
17
+ * TypeScript, each file once. Edits are made by source span against the
18
+ * original text and applied in one pass per file, so indentation, attribute
19
+ * order, quotes and line endings stay as they are.
20
+ * - A conversion that is not safe is skipped and reported, the rest of the file
21
+ * is still converted. A template that does not parse is not touched at all.
22
+ * - Only files below `--path` are written. Stylesheets are counted, never edited.
23
+ * - A second run changes nothing.
24
+ */
25
+ const schematics_1 = require("@angular-devkit/schematics");
26
+ const utility_1 = require("@schematics/angular/utility");
27
+ const typescript_1 = __importDefault(require("typescript"));
28
+ const html_1 = require("../ng-add/html");
29
+ const component_1 = require("./component");
30
+ const report_1 = require("./report");
31
+ const tables_1 = require("./tables");
32
+ const template_1 = require("./template");
33
+ const SKIPPED_FOLDERS = /^(node_modules|dist|\..*)$/;
34
+ const STYLESHEET = /\.(css|scss|sass|less)$/;
35
+ function migrateMaterial(options) {
36
+ return async (tree, context) => {
37
+ const rules = parseRules(options.rules);
38
+ const scope = await resolveScope(tree, options);
39
+ const dryRun = isDryRun(context);
40
+ const run = new Run(tree, scope, new Set(rules), dryRun);
41
+ const files = (scope.isFile ? [scope.path] : filesBelow(tree, scope.path)).filter((file) => /\.(ts|html)$/.test(file) || STYLESHEET.test(file));
42
+ for (const path of files.filter((file) => file.endsWith('.ts') && !file.endsWith('.d.ts'))) {
43
+ run.typescript(path);
44
+ }
45
+ for (const path of files.filter((file) => file.endsWith('.html'))) {
46
+ run.orphanTemplate(path);
47
+ }
48
+ for (const path of files.filter((file) => STYLESHEET.test(file))) {
49
+ run.stylesheet(path);
50
+ }
51
+ const report = (0, report_1.buildReport)(scope.path || '.', rules, dryRun, files.length, run.reports);
52
+ const markdown = (0, report_1.renderMarkdown)(report);
53
+ const target = normalize(options.report || 'zenit-migration-report.md');
54
+ const twin = target.replace(/(\.md)?$/, '.json');
55
+ // A run that changed nothing must not overwrite the report of the run that did:
56
+ // that one holds the review items, which cannot be found again afterwards.
57
+ const keep = report.totals.filesChanged === 0 && tree.exists(target);
58
+ // In a dry run the CLI discards the tree, and lists what would have been written.
59
+ if (!keep) {
60
+ write(tree, target, markdown);
61
+ write(tree, twin, (0, report_1.renderJson)(report));
62
+ }
63
+ const { totals, summary } = report;
64
+ const count = (kind) => Object.values(summary).reduce((sum, entry) => sum + entry[kind], 0);
65
+ context.logger.info(`zenit-ui: ${totals.filesScanned} files scanned below ${report.path}, ` +
66
+ `${totals.filesChanged} ${dryRun ? 'would change' : 'changed'}, ` +
67
+ `${totals.converted} spots converted, ${count('manual')} left for manual migration, ` +
68
+ `${count('review')} to review.\n` +
69
+ (dryRun
70
+ ? ' Dry run: nothing is written to disk, so the report follows here.'
71
+ : keep
72
+ ? ` Nothing changed, so the existing report ${target} was kept. Use --print-report or --report <other file> to see the current state.`
73
+ : ` Report: ${target} and ${twin}`));
74
+ if (options.printReport ?? dryRun) {
75
+ context.logger.info(markdown);
76
+ }
77
+ };
78
+ }
79
+ function parseRules(input) {
80
+ const names = (input || tables_1.RULES.join(','))
81
+ .split(',')
82
+ .map((name) => name.trim())
83
+ .filter(Boolean);
84
+ const unknown = names.filter((name) => !tables_1.RULES.includes(name));
85
+ if (unknown.length > 0 || names.length === 0) {
86
+ throw new schematics_1.SchematicsException(`Unknown rule(s) in --rules: ${unknown.join(', ') || '(none given)'}. Known rules: ${tables_1.RULES.join(', ')}.`);
87
+ }
88
+ return tables_1.RULES.filter((rule) => names.includes(rule));
89
+ }
90
+ /**
91
+ * ponytail: the dry-run flag is a CLI option that never reaches a schematic, so
92
+ * this reads the private `_dryRun` of the workflow and falls back to the command
93
+ * line. If a future devkit hides both, `--print-report` still prints the report.
94
+ */
95
+ function isDryRun(context) {
96
+ const workflow = context.engine.workflow;
97
+ return (workflow?._dryRun === true ||
98
+ process.argv.some((argument) => /^(--dry-run|--dryRun|-d)(=true)?$/.test(argument)));
99
+ }
100
+ /** Workspace-relative, forward slashes, no leading or trailing slash; `''` is the workspace root. */
101
+ function normalize(input) {
102
+ let path = input.trim().replace(/\\/g, '/');
103
+ const root = process.cwd().replace(/\\/g, '/').replace(/\/$/, '');
104
+ // An absolute path, as a shell completes it: with a drive letter on Windows, or below the
105
+ // working directory on Linux and macOS. A leading slash alone stays workspace-relative.
106
+ const absolute = /^[a-zA-Z]:\//.test(path) || path === root || path.startsWith(`${root}/`);
107
+ if (absolute) {
108
+ if (!path.toLowerCase().startsWith(`${root.toLowerCase()}/`) &&
109
+ path.toLowerCase() !== root.toLowerCase()) {
110
+ throw new schematics_1.SchematicsException(`"${input}" lies outside the workspace ${root}.`);
111
+ }
112
+ path = path.slice(root.length);
113
+ }
114
+ const parts = [];
115
+ for (const part of path.split('/')) {
116
+ if (part === '..') {
117
+ if (parts.length === 0) {
118
+ throw new schematics_1.SchematicsException(`"${input}" lies outside the workspace.`);
119
+ }
120
+ parts.pop();
121
+ }
122
+ else if (part !== '' && part !== '.') {
123
+ parts.push(part);
124
+ }
125
+ }
126
+ return parts.join('/');
127
+ }
128
+ async function resolveScope(tree, options) {
129
+ if (!options.path?.trim()) {
130
+ throw new schematics_1.SchematicsException('--path is required: the folder or file to migrate.');
131
+ }
132
+ const candidates = [normalize(options.path)];
133
+ let projectRoot;
134
+ if (options.project) {
135
+ const workspace = await (0, utility_1.readWorkspace)(tree);
136
+ const project = workspace.projects.get(options.project);
137
+ if (!project) {
138
+ throw new schematics_1.SchematicsException(`Project "${options.project}" was not found in the workspace. Known projects: ` +
139
+ `${[...workspace.projects.keys()].join(', ') || '(none)'}.`);
140
+ }
141
+ projectRoot = normalize(project.root);
142
+ const sourceRoot = normalize(project.sourceRoot ?? `${project.root}/src`);
143
+ candidates.push(normalize(`${sourceRoot}/${options.path}`));
144
+ }
145
+ for (const path of candidates) {
146
+ const isFile = tree.exists(`/${path}`);
147
+ if (!isFile) {
148
+ // getDir() throws for a file, and an empty directory is what a missing one looks like.
149
+ const directory = tree.getDir(`/${path}`);
150
+ if (directory.subfiles.length === 0 && directory.subdirs.length === 0) {
151
+ continue;
152
+ }
153
+ }
154
+ if (path.split('/').some((part) => SKIPPED_FOLDERS.test(part))) {
155
+ throw new schematics_1.SchematicsException(`"${path}" is not source code of the workspace.`);
156
+ }
157
+ if (projectRoot && path !== projectRoot && !path.startsWith(`${projectRoot}/`)) {
158
+ // (a project at the workspace root has the root '' and contains everything)
159
+ throw new schematics_1.SchematicsException(`"${path}" lies outside the project "${options.project}" (${projectRoot || '.'}).`);
160
+ }
161
+ return { path, isFile };
162
+ }
163
+ throw new schematics_1.SchematicsException(`--path "${options.path}" was not found (looked for ${candidates.map((path) => `"${path}"`).join(' and ')}).`);
164
+ }
165
+ /** All files below a folder, sorted, without `node_modules`, `dist` and dot folders. */
166
+ function filesBelow(tree, folder) {
167
+ const out = [];
168
+ const walk = (path) => {
169
+ const directory = tree.getDir(`/${path}`);
170
+ for (const file of directory.subfiles) {
171
+ out.push(path ? `${path}/${file}` : file);
172
+ }
173
+ for (const sub of directory.subdirs) {
174
+ if (!SKIPPED_FOLDERS.test(sub)) {
175
+ walk(path ? `${path}/${sub}` : sub);
176
+ }
177
+ }
178
+ };
179
+ walk(folder);
180
+ return out.sort();
181
+ }
182
+ function write(tree, path, content) {
183
+ if (tree.exists(path)) {
184
+ tree.overwrite(path, content);
185
+ }
186
+ else {
187
+ tree.create(path, content);
188
+ }
189
+ }
190
+ /** One run over the scope. Every file is read, parsed and written at most once. */
191
+ class Run {
192
+ tree;
193
+ scope;
194
+ rules;
195
+ dryRun;
196
+ reports = [];
197
+ templates = new Map();
198
+ constructor(tree, scope, rules, dryRun) {
199
+ this.tree = tree;
200
+ this.scope = scope;
201
+ this.rules = rules;
202
+ this.dryRun = dryRun;
203
+ }
204
+ inScope(path) {
205
+ return this.scope.isFile
206
+ ? path === this.scope.path
207
+ : this.scope.path === '' || path.startsWith(`${this.scope.path}/`);
208
+ }
209
+ /** The text with its BOM, `undefined` for a file that is not valid UTF-8. */
210
+ read(path) {
211
+ const buffer = this.tree.read(`/${path}`);
212
+ try {
213
+ return buffer
214
+ ? new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(buffer)
215
+ : undefined;
216
+ }
217
+ catch {
218
+ return undefined;
219
+ }
220
+ }
221
+ /** Applies the edits of one file in one pass and files the report entry. */
222
+ finish(path, text, edits, findings, entry) {
223
+ let sorted = (0, template_1.sortEdits)(edits);
224
+ if (sorted.some((edit, index) => index > 0 && edit.start < sorted[index - 1].end)) {
225
+ // Cannot happen with the planners as they are; if it does, the file stays as it is.
226
+ sorted = [];
227
+ findings = [
228
+ ...findings.filter((finding) => finding.kind === 'manual'),
229
+ {
230
+ offset: 0,
231
+ rule: 'imports',
232
+ kind: 'manual',
233
+ code: 'unsafe-edit',
234
+ reason: 'The planned edits of this file overlap, so none of them was applied.',
235
+ fix: 'Migrate this file by hand.',
236
+ },
237
+ ];
238
+ entry = { ...entry, converted: {} };
239
+ }
240
+ const output = (0, html_1.applyEdits)(text, sorted);
241
+ if (output !== text) {
242
+ this.tree.overwrite(`/${path}`, output);
243
+ }
244
+ // Lines refer to what is on disk after the run: the new text, or the old one in a dry run.
245
+ const shown = this.dryRun ? text : output;
246
+ const starts = lineStarts(shown);
247
+ this.reports.push({
248
+ path,
249
+ status: entry.unparsed ? 'unparsed' : output !== text ? 'changed' : 'unchanged',
250
+ converted: entry.converted,
251
+ remaining: entry.remaining,
252
+ findings: findings.map(({ offset, rule, kind, code, reason, fix }) => ({
253
+ ...position(starts, this.dryRun ? offset : mapOffset(offset, sorted)),
254
+ rule,
255
+ kind,
256
+ code,
257
+ reason,
258
+ fix,
259
+ })),
260
+ });
261
+ }
262
+ unparsable(what, error, offset = 0) {
263
+ return {
264
+ offset,
265
+ rule: 'parse',
266
+ kind: 'manual',
267
+ code: 'template-unparsable',
268
+ reason: `${what} does not parse (${error}), so nothing in it was touched.`,
269
+ fix: 'Fix the template, then run the schematic again.',
270
+ };
271
+ }
272
+ /** An external template: migrated once, whoever asks first. */
273
+ template(path) {
274
+ if (this.templates.has(path)) {
275
+ return this.templates.get(path);
276
+ }
277
+ const text = this.read(path);
278
+ if (text === undefined) {
279
+ return undefined;
280
+ }
281
+ const result = (0, template_1.migrateTemplate)(text, { rules: this.rules });
282
+ this.templates.set(path, result);
283
+ this.finish(path, text, result.edits, result.error ? [this.unparsable('The template', result.error)] : result.findings, { converted: result.converted, remaining: result.remaining, unparsed: !!result.error });
284
+ return result;
285
+ }
286
+ /** A template below `--path` that no component below `--path` names. */
287
+ orphanTemplate(path) {
288
+ if (this.templates.has(path)) {
289
+ return;
290
+ }
291
+ const result = this.template(path);
292
+ if (result && !result.error && this.rules.has('imports') && result.zenit.length > 0) {
293
+ this.reports.at(-1)?.findings.push({
294
+ line: 1,
295
+ column: 1,
296
+ rule: 'imports',
297
+ kind: 'manual',
298
+ code: 'imports-owner-outside-path',
299
+ reason: 'No component below --path names this template in templateUrl, so no "imports" were updated for it.',
300
+ fix: `In the component that owns this template: add ${result.zenit.join(', ')} from 'zenit-ui' and remove the Material modules whose selectors are gone.`,
301
+ });
302
+ }
303
+ }
304
+ typescript(path) {
305
+ const text = this.read(path);
306
+ if (text === undefined) {
307
+ return;
308
+ }
309
+ if (path.endsWith('.spec.ts')) {
310
+ const named = [...new Set(text.match(/\bMat[A-Z]\w*(Module|Harness)\b/g) ?? [])].sort();
311
+ if (named.length > 0 && this.rules.has('imports')) {
312
+ this.finish(path, text, [], [
313
+ {
314
+ offset: text.search(/\bMat[A-Z]\w*(Module|Harness)\b/),
315
+ rule: 'imports',
316
+ kind: 'manual',
317
+ code: 'imports-spec',
318
+ reason: `The spec names ${named.join(', ')}. Spec files are never edited.`,
319
+ fix: 'Update the TestBed imports and the harnesses once the component is migrated.',
320
+ },
321
+ ], { converted: {}, remaining: {} });
322
+ }
323
+ return;
324
+ }
325
+ if (!/@Component\s*\(/.test(text)) {
326
+ return;
327
+ }
328
+ const source = typescript_1.default.createSourceFile(path, text, typescript_1.default.ScriptTarget.Latest, true);
329
+ const edits = [];
330
+ const findings = [];
331
+ const converted = {};
332
+ const remaining = {};
333
+ const usages = [];
334
+ for (const component of (0, component_1.findComponents)(source)) {
335
+ const manual = (code, reason, fix) => findings.push({
336
+ offset: component.offset,
337
+ rule: 'parse',
338
+ kind: 'manual',
339
+ code,
340
+ reason,
341
+ fix,
342
+ });
343
+ if (component.inline) {
344
+ const { start, end, delimiter } = component.inline;
345
+ const result = (0, template_1.migrateTemplate)(text.slice(start, end), {
346
+ rules: this.rules,
347
+ quote: delimiter === '"' ? "'" : '"',
348
+ plainString: delimiter !== '`',
349
+ });
350
+ if (result.error) {
351
+ findings.push(this.unparsable(`The inline template of ${component.name}`, result.error, start));
352
+ continue;
353
+ }
354
+ edits.push(...result.edits.map((edit) => ({
355
+ ...edit,
356
+ start: edit.start + start,
357
+ end: edit.end + start,
358
+ })));
359
+ findings.push(...result.findings.map((finding) => ({ ...finding, offset: finding.offset + start })));
360
+ for (const [rule, count] of Object.entries(result.converted)) {
361
+ converted[rule] = (converted[rule] ?? 0) + count;
362
+ }
363
+ for (const [name, count] of Object.entries(result.remaining)) {
364
+ remaining[name] = (remaining[name] ?? 0) + count;
365
+ }
366
+ usages.push([component, result]);
367
+ }
368
+ else if (component.inlineProblem) {
369
+ manual('template-not-literal', `${component.name}: ${component.inlineProblem} It was not read, and the "imports" of the component were left alone.`, 'Migrate this template by hand, or move it into an .html file and run the schematic again.');
370
+ }
371
+ else if (component.templateUrl !== undefined) {
372
+ const folder = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
373
+ let url;
374
+ try {
375
+ url = normalize(`${folder}/${component.templateUrl}`);
376
+ }
377
+ catch {
378
+ url = undefined;
379
+ }
380
+ if (url === undefined || !this.tree.exists(`/${url}`)) {
381
+ manual('template-missing', `${component.name}: the template ${component.templateUrl} was not found.`, 'Check the templateUrl.');
382
+ }
383
+ else if (!this.inScope(url)) {
384
+ manual('template-outside-path', `${component.name}: the template ${url} lies outside --path and was not touched, nor were the "imports" of the component.`, 'Run the schematic with a --path that contains both files.');
385
+ }
386
+ else {
387
+ const result = this.template(url);
388
+ if (result && !result.error) {
389
+ usages.push([component, result]);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ if (this.rules.has('imports') && usages.length > 0) {
395
+ const planned = (0, component_1.planImports)(source, usages);
396
+ edits.push(...planned.edits);
397
+ findings.push(...planned.findings);
398
+ if (planned.changed > 0) {
399
+ converted.imports = planned.changed;
400
+ }
401
+ }
402
+ this.finish(path, text, edits, findings, { converted, remaining });
403
+ }
404
+ stylesheet(path) {
405
+ const text = this.read(path);
406
+ if (text === undefined) {
407
+ return;
408
+ }
409
+ // ponytail: plain pattern counts, comments included. The number is a burn-down, not a lint.
410
+ const styleDebt = {};
411
+ for (const [label, pattern] of Object.entries(tables_1.STYLE_DEBT)) {
412
+ styleDebt[label] = text.match(pattern)?.length ?? 0;
413
+ }
414
+ if (Object.values(styleDebt).some((count) => count > 0)) {
415
+ this.reports.push({
416
+ path,
417
+ status: 'unchanged',
418
+ converted: {},
419
+ findings: [],
420
+ remaining: {},
421
+ styleDebt,
422
+ });
423
+ }
424
+ }
425
+ }
426
+ function lineStarts(text) {
427
+ const starts = [0];
428
+ for (let index = text.indexOf('\n'); index !== -1; index = text.indexOf('\n', index + 1)) {
429
+ starts.push(index + 1);
430
+ }
431
+ return starts;
432
+ }
433
+ /** 1-based line and column of an offset. */
434
+ function position(starts, offset) {
435
+ let low = 0;
436
+ let high = starts.length - 1;
437
+ while (low < high) {
438
+ const middle = (low + high + 1) >> 1;
439
+ if (starts[middle] <= offset) {
440
+ low = middle;
441
+ }
442
+ else {
443
+ high = middle - 1;
444
+ }
445
+ }
446
+ return { line: low + 1, column: offset - starts[low] + 1 };
447
+ }
448
+ /** Where an offset of the original text ends up after the (sorted) edits. */
449
+ function mapOffset(offset, edits) {
450
+ let delta = 0;
451
+ for (const edit of edits) {
452
+ if (edit.end <= offset && edit.start < offset) {
453
+ delta += edit.text.length - (edit.end - edit.start);
454
+ }
455
+ else if (edit.start < offset) {
456
+ return edit.start + delta; // inside a replaced span
457
+ }
458
+ else {
459
+ break;
460
+ }
461
+ }
462
+ return offset + delta;
463
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildReport = buildReport;
4
+ exports.renderJson = renderJson;
5
+ exports.renderMarkdown = renderMarkdown;
6
+ /**
7
+ * The report of `migrate-material`, as data, as JSON and as Markdown. Both
8
+ * renderings are deterministic: no timestamps, files and findings sorted, paths
9
+ * workspace-relative with forward slashes.
10
+ */
11
+ const tables_1 = require("./tables");
12
+ function buildReport(path, rules, dryRun, filesScanned, files) {
13
+ const listed = files
14
+ .filter((file) => file.status !== 'unchanged' ||
15
+ file.findings.length > 0 ||
16
+ Object.keys(file.remaining).length > 0 ||
17
+ file.styleDebt)
18
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
19
+ const summary = {};
20
+ const row = (rule) => (summary[rule] ??= { converted: 0, manual: 0, review: 0, info: 0 });
21
+ tables_1.RULES.forEach(row);
22
+ const remaining = {};
23
+ for (const file of listed) {
24
+ for (const [rule, count] of Object.entries(file.converted)) {
25
+ row(rule).converted += count;
26
+ }
27
+ for (const finding of file.findings) {
28
+ row(finding.rule)[finding.kind] += 1;
29
+ }
30
+ for (const [name, count] of Object.entries(file.remaining)) {
31
+ remaining[name] = (remaining[name] ?? 0) + count;
32
+ }
33
+ file.findings.sort((a, b) => a.line - b.line || a.column - b.column || a.code.localeCompare(b.code));
34
+ }
35
+ return {
36
+ schema: 1,
37
+ path,
38
+ rules,
39
+ dryRun,
40
+ totals: {
41
+ filesScanned,
42
+ filesChanged: listed.filter((file) => file.status === 'changed').length,
43
+ converted: Object.values(summary).reduce((sum, entry) => sum + entry.converted, 0),
44
+ findings: listed.reduce((sum, file) => sum + file.findings.length, 0),
45
+ },
46
+ summary,
47
+ remaining: sortedCounts(remaining),
48
+ files: listed,
49
+ };
50
+ }
51
+ function sortedCounts(counts) {
52
+ return Object.keys(counts)
53
+ .sort()
54
+ .map((name) => ({ name, count: counts[name], replacement: (0, tables_1.replacementFor)(name) }));
55
+ }
56
+ function renderJson(report) {
57
+ return `${JSON.stringify(report, null, 2)}\n`;
58
+ }
59
+ /** A table cell: one line, no pipe that would end the cell. */
60
+ function cell(text) {
61
+ return text.replace(/\s+/g, ' ').replace(/\|/g, '\\|');
62
+ }
63
+ function renderMarkdown(report) {
64
+ const out = [
65
+ '# zenit-ui migration report',
66
+ '',
67
+ `Path \`${report.path}\`, rules ${report.rules.join(', ')}. ` +
68
+ (report.dryRun
69
+ ? 'Dry run: nothing was written, lines refer to the files as they are.'
70
+ : 'Applied: lines refer to the files after the run.'),
71
+ '',
72
+ `${report.totals.filesScanned} files scanned, ${report.totals.filesChanged} changed, ` +
73
+ `${report.totals.converted} spots converted, ${report.totals.findings} findings.`,
74
+ '',
75
+ '- **manual**: not converted, a human has to do it.',
76
+ '- **review**: converted, but something changed that needs a look.',
77
+ '- **info**: converted, what was dropped has no effect in zenit-ui.',
78
+ '',
79
+ '## Summary',
80
+ '',
81
+ '| Rule | Converted | Manual | Review | Info |',
82
+ '| --- | ---: | ---: | ---: | ---: |',
83
+ ...Object.keys(report.summary).map((rule) => {
84
+ const entry = report.summary[rule];
85
+ return `| ${rule} | ${entry.converted} | ${entry.manual} | ${entry.review} | ${entry.info} |`;
86
+ }),
87
+ '',
88
+ ];
89
+ if (report.remaining.length > 0) {
90
+ out.push('## Left for manual migration', '', 'Material tags and attributes still in the templates after this run.', '', '| Selector | Count | zenit-ui replacement |', '| --- | ---: | --- |', ...report.remaining.map(({ name, count, replacement }) => `| \`${name}\` | ${count} | ${cell(replacement)} |`), '');
91
+ }
92
+ const styles = report.files.filter((file) => file.styleDebt);
93
+ if (styles.length > 0) {
94
+ const columns = Object.keys(styles[0].styleDebt ?? {});
95
+ out.push('## Style debt', '', 'Stylesheets are never edited. Every count is a place where application CSS reaches into Material.', '', `| File | ${columns.map(cell).join(' | ')} |`, `| --- | ${columns.map(() => '---:').join(' | ')} |`, ...styles.map((file) => `| \`${file.path}\` | ${columns.map((column) => file.styleDebt?.[column] ?? 0).join(' | ')} |`), '');
96
+ }
97
+ const files = report.files.filter((file) => !file.styleDebt);
98
+ if (files.length > 0) {
99
+ out.push('## Files', '');
100
+ }
101
+ for (const file of files) {
102
+ const converted = Object.entries(file.converted)
103
+ .filter(([, count]) => count > 0)
104
+ .map(([rule, count]) => `${rule} ${count}`);
105
+ out.push(`### \`${file.path}\``, '', `${file.status}${converted.length ? `, converted: ${converted.join(', ')}` : ''}`, '');
106
+ const rows = file.findings.filter((finding) => finding.kind !== 'info');
107
+ if (rows.length > 0) {
108
+ out.push('| Line:Col | Rule | Kind | Code | Reason | Suggested fix |', '| --- | --- | --- | --- | --- | --- |', ...rows.map((f) => `| ${f.line}:${f.column} | ${f.rule} | ${f.kind} | ${f.code} | ${cell(f.reason)} | ${cell(f.fix)} |`), '');
109
+ }
110
+ // Harmless drops are many and alike: one line per code instead of one row each.
111
+ const infos = file.findings.filter((finding) => finding.kind === 'info');
112
+ for (const code of [...new Set(infos.map((finding) => finding.code))].sort()) {
113
+ const lines = infos.filter((finding) => finding.code === code).map((finding) => finding.line);
114
+ out.push(`- info \`${code}\` x${lines.length}, lines ${lines.join(', ')}`);
115
+ }
116
+ if (infos.length > 0) {
117
+ out.push('');
118
+ }
119
+ const remaining = sortedCounts(file.remaining);
120
+ if (remaining.length > 0) {
121
+ out.push(`Left: ${remaining.map(({ name, count }) => `\`${name}\` x${count}`).join(', ')}`, '');
122
+ }
123
+ }
124
+ return `${out.join('\n').replace(/\n+$/, '')}\n`;
125
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "SchematicsZenitUiMigrateMaterial",
4
+ "title": "zenit-ui migrate-material schema",
5
+ "type": "object",
6
+ "properties": {
7
+ "path": {
8
+ "type": "string",
9
+ "description": "Workspace-relative folder or file to migrate. Only files under it are written.",
10
+ "x-prompt": "Which folder or file should be migrated (workspace-relative, for example src/app/billing)?"
11
+ },
12
+ "project": {
13
+ "type": "string",
14
+ "description": "Project the path belongs to. The path may then also be given relative to the project's sourceRoot, and a path outside the project is refused."
15
+ },
16
+ "report": {
17
+ "type": "string",
18
+ "default": "zenit-migration-report.md",
19
+ "description": "Workspace-relative path of the Markdown report. A JSON twin with the same name and the extension .json is written next to it."
20
+ },
21
+ "rules": {
22
+ "type": "string",
23
+ "default": "icon,button,tooltip,spinner,chip,imports",
24
+ "description": "Comma-separated subset of the rules to run: icon, button, tooltip, spinner, chip, imports."
25
+ },
26
+ "printReport": {
27
+ "type": "boolean",
28
+ "description": "Print the full Markdown report to the console. Default: only in a dry run, where no file is written."
29
+ }
30
+ },
31
+ "required": ["path"]
32
+ }