@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,609 @@
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.ngAdd = ngAdd;
7
+ /**
8
+ * `ng add zenit-ui`.
9
+ *
10
+ * Performs the setup that `projects/zenit-ui/README.md` documents by hand:
11
+ * style entries in `angular.json`, `z-root` in `index.html`, `@angular/cdk`,
12
+ * the four self-hosted fonts and `<z-toast-outlet />` in the root component.
13
+ * With `--themes` it also wires the theme so that the stored scheme is on the
14
+ * page before the first paint: init script in `index.html`, render-blocking
15
+ * stylesheet, `provideZenitTheme()` in the application config.
16
+ *
17
+ * Every step is idempotent. Running the schematic twice leaves the workspace
18
+ * unchanged, existing entries are never duplicated, and a wrong style order is
19
+ * corrected.
20
+ *
21
+ * Every source file is atomic: all changes to it are computed against its
22
+ * original text and applied in one pass, or the file stays untouched and the
23
+ * final log names the manual step. Inserted text uses the file's own line ending.
24
+ */
25
+ const schematics_1 = require("@angular-devkit/schematics");
26
+ const utility_1 = require("@schematics/angular/utility");
27
+ const ast_utils_1 = require("@schematics/angular/utility/ast-utils");
28
+ const change_1 = require("@schematics/angular/utility/change");
29
+ const eol_1 = require("@schematics/angular/utility/eol");
30
+ const latest_versions_1 = require("@schematics/angular/utility/latest-versions");
31
+ const app_component_1 = require("@schematics/angular/utility/standalone/app_component");
32
+ const app_config_1 = require("@schematics/angular/utility/standalone/app_config");
33
+ const util_1 = require("@schematics/angular/utility/standalone/util");
34
+ const core_1 = require("@angular-devkit/core");
35
+ const typescript_1 = __importDefault(require("typescript"));
36
+ const html_1 = require("./html");
37
+ const init_script_1 = require("./init-script");
38
+ /** Style entries the schematic owns, in the order the README prescribes. */
39
+ const TOKENS = 'zenit-ui/styles/tokens.css';
40
+ const THEMES = 'zenit-ui/styles/themes.css';
41
+ const CDK_OVERLAY = '@angular/cdk/overlay-prebuilt.css';
42
+ const LIBRARY = 'zenit-ui/styles/zenit-ui.css';
43
+ const MANAGED = [TOKENS, THEMES, CDK_OVERLAY, LIBRARY];
44
+ /** Font packages, self-hosted so that no request reaches Google. */
45
+ const FONT_PACKAGES = {
46
+ 'material-icons': '^1.13.14',
47
+ '@fontsource/inter': '^5.3.0',
48
+ '@fontsource/space-grotesk': '^5.3.0',
49
+ '@fontsource/jetbrains-mono': '^5.3.0',
50
+ };
51
+ /**
52
+ * The `layer(schriften)` is required: `material-icons` sets its own `font-size`
53
+ * on `.material-icons` and is loaded after `zenit-ui.css`. The layer makes sure
54
+ * `.z-icon` from the library wins. Valid in CSS and in SCSS (Sass >= 1.71),
55
+ * because every URL ends in `.css` and is therefore a plain CSS import.
56
+ */
57
+ const FONT_IMPORTS = `/* Fonts self-hosted, no request to Google. The layer keeps .z-icon from
58
+ zenit-ui.css ahead of the font-size that material-icons sets itself. */
59
+ @import 'material-icons/iconfont/filled.css' layer(schriften);
60
+ @import '@fontsource/inter/400.css';
61
+ @import '@fontsource/inter/500.css';
62
+ @import '@fontsource/inter/600.css';
63
+ @import '@fontsource/space-grotesk/600.css';
64
+ @import '@fontsource/space-grotesk/700.css';
65
+ @import '@fontsource/jetbrains-mono/400.css';
66
+ @import '@fontsource/jetbrains-mono/600.css';
67
+ `;
68
+ const TOAST_OUTLET = 'ZToastOutlet';
69
+ const TOAST_TAG = '<z-toast-outlet />';
70
+ function ngAdd(options) {
71
+ return async (tree, context) => {
72
+ const workspace = await (0, utility_1.readWorkspace)(tree);
73
+ const [name, project] = selectProject(workspace.projects, options.project);
74
+ if (!options.project) {
75
+ context.logger.info(`zenit-ui: no --project given, using the only application "${name}".`);
76
+ }
77
+ const themes = options.themes === true;
78
+ const report = { done: [], manual: [] };
79
+ return (0, schematics_1.chain)([
80
+ styleRule(name, themes, report),
81
+ indexHtmlRule(project, themes, report),
82
+ keepingCrlf('/package.json', (0, utility_1.addDependency)('@angular/cdk', cdkRange(tree), {
83
+ type: utility_1.DependencyType.Default,
84
+ existing: utility_1.ExistingBehavior.Skip,
85
+ })),
86
+ options.fonts === false ? noop : fontRule(project, report),
87
+ options.toastOutlet === false ? noop : toastOutletRule(name, report),
88
+ themes ? (0, schematics_1.chain)([inlineCriticalRule(name, report), themeProviderRule(name, report)]) : noop,
89
+ nextSteps(report),
90
+ ]);
91
+ };
92
+ }
93
+ /** A rule that changes nothing. */
94
+ const noop = (tree) => tree;
95
+ const LONE_LF = /(^|[^\r])\n/;
96
+ /**
97
+ * Wraps a devkit rule that writes LF whatever the file uses (`addDependency`
98
+ * re-serialises `package.json`, `addRootProvider` creates `providers`): a file
99
+ * that was purely CRLF before is purely CRLF afterwards.
100
+ */
101
+ function keepingCrlf(path, rule) {
102
+ return (tree) => {
103
+ const before = tree.exists(path) ? tree.readText(path) : '';
104
+ if (!before.includes('\r\n') || LONE_LF.test(before)) {
105
+ return rule;
106
+ }
107
+ return (0, schematics_1.chain)([
108
+ rule,
109
+ (after) => {
110
+ const text = after.readText(path);
111
+ if (LONE_LF.test(text)) {
112
+ after.overwrite(path, text.replace(/\r?\n/g, '\r\n'));
113
+ }
114
+ },
115
+ ]);
116
+ };
117
+ }
118
+ /** Builders whose `build` target takes `styles`, `index` and a main entry point. */
119
+ const APPLICATION_BUILDER = /:(application|browser|browser-esbuild)$/;
120
+ /**
121
+ * The project to wire up. Only an application with an application build target
122
+ * qualifies: a library's `build` target belongs to ng-packagr, which knows no
123
+ * `styles`. Without `--project` the workspace must have exactly one application.
124
+ */
125
+ function selectProject(projects, requested) {
126
+ const all = new Map(projects);
127
+ const applications = [...all]
128
+ .filter(([, project]) => project.extensions['projectType'] === 'application')
129
+ .map(([name]) => name);
130
+ const listed = applications.join(', ') || '(none)';
131
+ if (!requested && applications.length !== 1) {
132
+ throw new schematics_1.SchematicsException(applications.length === 0
133
+ ? 'The workspace has no application to wire zenit-ui into. Create one with "ng generate application" first.'
134
+ : `The workspace has several applications: ${listed}. Name one with --project.`);
135
+ }
136
+ const name = requested ?? applications[0];
137
+ const project = all.get(name);
138
+ if (!project) {
139
+ const known = [...all.keys()].join(', ') || '(none)';
140
+ throw new schematics_1.SchematicsException(`Project "${name}" was not found in the workspace. Known projects: ${known}.`);
141
+ }
142
+ if (project.extensions['projectType'] !== 'application') {
143
+ throw new schematics_1.SchematicsException(`Project "${name}" is not an application (projectType: ` +
144
+ `${JSON.stringify(project.extensions['projectType'])}). zenit-ui is wired into the ` +
145
+ `application that renders it. Applications of this workspace: ${listed}.`);
146
+ }
147
+ const builder = project.targets.get('build')?.builder;
148
+ if (!builder || !APPLICATION_BUILDER.test(builder)) {
149
+ throw new schematics_1.SchematicsException(`Project "${name}" has no application build target (builder of "build": ` +
150
+ `${builder ?? 'none'}). Expected a builder ending in :application, :browser or ` +
151
+ ':browser-esbuild. Follow the manual setup in the README of zenit-ui instead.');
152
+ }
153
+ return [name, project];
154
+ }
155
+ // --------------------------------------------------------------------------
156
+ // 1. angular.json styles
157
+ // --------------------------------------------------------------------------
158
+ /**
159
+ * Puts the library stylesheets in front of the application's own styles, in the
160
+ * order the README prescribes. Entries already present are moved rather than
161
+ * duplicated, so a wrong order is corrected.
162
+ */
163
+ function styleRule(projectName, themes, report) {
164
+ return (0, utility_1.updateWorkspace)((workspace) => {
165
+ const project = workspace.projects.get(projectName);
166
+ const targets = [
167
+ ['build', project?.targets.get('build')],
168
+ // The test target only gets the styles when it has such an option at all.
169
+ ['test', project?.targets.get('test')],
170
+ ];
171
+ for (const [name, target] of targets) {
172
+ if (!target || (name === 'test' && !(target.options && 'styles' in target.options))) {
173
+ continue;
174
+ }
175
+ setStyles(target, themes);
176
+ report.done.push(`Stylesheets registered in the "options" of the "${name}" target.`);
177
+ // `styles` of a configuration replace the ones from `options`, they are not merged.
178
+ const overriding = Object.entries(target.configurations ?? {})
179
+ .filter(([, configuration]) => configuration && 'styles' in configuration)
180
+ .map(([configuration]) => `"${configuration}"`);
181
+ if (overriding.length > 0) {
182
+ report.manual.push(`angular.json: the configuration(s) ${overriding.join(', ')} of the "${name}" target ` +
183
+ 'set their own "styles", which replace the ones from "options". They were left ' +
184
+ `alone: put ${TOKENS}, ${themes ? `${THEMES}, ` : ''}${CDK_OVERLAY} and ${LIBRARY} ` +
185
+ 'in front of their entries yourself, in this order.');
186
+ }
187
+ }
188
+ });
189
+ }
190
+ /** The managed entry an existing `styles` entry stands for, in either spelling. */
191
+ function managedKey(entry) {
192
+ const input = styleInput(entry)
193
+ ?.replace(/^\.\//, '')
194
+ .replace(/^node_modules\//, '');
195
+ return input && MANAGED.includes(input) ? input : undefined;
196
+ }
197
+ /**
198
+ * Takes every managed entry out of the array and puts the set back in front, in
199
+ * canonical order. An existing entry is reused as it is, so the object form
200
+ * (`{ input, bundleName, inject }`) and the `node_modules/` spelling survive.
201
+ * `themes.css` stays when it was there, also without `--themes`.
202
+ */
203
+ function setStyles(target, themes) {
204
+ const options = (target.options ??= {});
205
+ const current = Array.isArray(options['styles']) ? options['styles'] : [];
206
+ const existing = new Map();
207
+ for (const entry of current) {
208
+ const key = managedKey(entry);
209
+ if (key && !existing.has(key)) {
210
+ existing.set(key, entry);
211
+ }
212
+ }
213
+ const wanted = MANAGED.filter((key) => key !== THEMES || themes || existing.has(THEMES));
214
+ options['styles'] = [
215
+ ...wanted.map((key) => existing.get(key) ?? key),
216
+ ...current.filter((entry) => !managedKey(entry)),
217
+ ];
218
+ }
219
+ /** A style entry is either a path or `{ input, bundleName }`. */
220
+ function styleInput(entry) {
221
+ if (typeof entry === 'string') {
222
+ return entry;
223
+ }
224
+ if (entry && typeof entry === 'object') {
225
+ const input = entry.input;
226
+ if (typeof input === 'string') {
227
+ return input;
228
+ }
229
+ }
230
+ return undefined;
231
+ }
232
+ // --------------------------------------------------------------------------
233
+ // 2. index.html
234
+ // --------------------------------------------------------------------------
235
+ /**
236
+ * Adds `z-root` to `<html>` and `<body>`, a `lang` when none is set and, with
237
+ * `--themes`, the init script. One plan against the original text, one write;
238
+ * when a tag the plan needs is missing, the file stays as it is.
239
+ */
240
+ function indexHtmlRule(project, themes, report) {
241
+ return (tree) => {
242
+ const path = indexPath(project);
243
+ const wanted = 'class="z-root" on <html> and <body>' +
244
+ (themes
245
+ ? ', and the output of zenitThemeInitScript() in a <script> at the top of <head> (docs/theming.md)'
246
+ : '');
247
+ if (!tree.exists(path)) {
248
+ report.manual.push(`${path} was not found. Add ${wanted} yourself.`);
249
+ return;
250
+ }
251
+ const content = tree.readText(path);
252
+ const withScript = themes && !content.includes(THEME_MARKER);
253
+ const plan = (0, html_1.planIndexHtml)(content, withScript ? initScriptLines() : undefined);
254
+ if (plan.missing.length > 0) {
255
+ report.manual.push(`${path} has no ${plan.missing.join(', ')} tag and was left alone. Add ${wanted} yourself.`);
256
+ return;
257
+ }
258
+ if (plan.edits.length > 0) {
259
+ tree.overwrite(path, (0, html_1.applyEdits)(content, plan.edits));
260
+ }
261
+ report.done.push(`z-root set on <html> and <body>${themes ? ', theme init script at the top of <head>' : ''} in ${path}.`);
262
+ // An existing `lang` is the application's decision, `ng new` writes "en".
263
+ if (plan.lang !== undefined && !/^de(-|$)/i.test(plan.lang)) {
264
+ report.manual.push(`${path}: lang left as "${plan.lang}": set it to your UI language ` +
265
+ '(the built-in labels of zenit-ui are German).');
266
+ }
267
+ };
268
+ }
269
+ function indexPath(project) {
270
+ const fromOptions = styleInput(project.targets.get('build')?.options?.['index']);
271
+ return fromOptions ? (0, core_1.normalize)(fromOptions) : (0, core_1.join)(sourceRoot(project), 'index.html');
272
+ }
273
+ function sourceRoot(project) {
274
+ return project.sourceRoot ? (0, core_1.normalize)(project.sourceRoot) : (0, core_1.join)((0, core_1.normalize)(project.root), 'src');
275
+ }
276
+ // --------------------------------------------------------------------------
277
+ // 3. @angular/cdk
278
+ // --------------------------------------------------------------------------
279
+ /** The CDK follows the major of the Angular version already installed. */
280
+ function cdkRange(tree) {
281
+ const manifest = tree.readJson('/package.json');
282
+ const core = manifest.dependencies?.['@angular/core'] ?? manifest.devDependencies?.['@angular/core'];
283
+ const major = core ? /\d+/.exec(core)?.[0] : undefined;
284
+ return major ? `^${major}.0.0` : latest_versions_1.latestVersions['Angular'];
285
+ }
286
+ // --------------------------------------------------------------------------
287
+ // 4. Fonts
288
+ // --------------------------------------------------------------------------
289
+ /** Adds the four font packages and prepends their imports to the global stylesheet. */
290
+ function fontRule(project, report) {
291
+ const dependencies = Object.entries(FONT_PACKAGES).map(([name, version]) => (0, utility_1.addDependency)(name, version, { type: utility_1.DependencyType.Dev, existing: utility_1.ExistingBehavior.Skip }));
292
+ return (0, schematics_1.chain)([
293
+ keepingCrlf('/package.json', (0, schematics_1.chain)(dependencies)),
294
+ (tree) => {
295
+ const path = globalStylesheet(tree, project);
296
+ if (!path) {
297
+ report.manual.push('Fonts: no .css, .scss or .less file inside the project is registered in the "styles" ' +
298
+ 'of the build target (files in node_modules or outside the project are never ' +
299
+ 'written to). Put the font imports from the README section "Self-host the fonts" ' +
300
+ 'at the top of your global stylesheet yourself.');
301
+ return;
302
+ }
303
+ const content = tree.readText(path);
304
+ if (!content.includes('material-icons/iconfont/filled.css')) {
305
+ const eol = (0, eol_1.getEOL)(content);
306
+ tree.overwrite(path, (FONT_IMPORTS + '\n').replace(/\n/g, eol) + content);
307
+ }
308
+ report.done.push(`Font imports at the top of ${path}.`);
309
+ },
310
+ ]);
311
+ }
312
+ /**
313
+ * The application's own stylesheet: the first `styles` entry of the build target
314
+ * that is a stylesheet inside the project. A file of a package
315
+ * (`node_modules/material-icons/...`) or outside the project is not ours to edit.
316
+ * `.sass` is left out, the import block is not valid in the indented syntax.
317
+ */
318
+ function globalStylesheet(tree, project) {
319
+ const styles = project.targets.get('build')?.options?.['styles'];
320
+ const root = (0, core_1.normalize)(project.root);
321
+ for (const entry of Array.isArray(styles) ? styles : []) {
322
+ const input = styleInput(entry);
323
+ if (!input || managedKey(entry) || /^([/\\]|[a-zA-Z]:)/.test(input)) {
324
+ continue;
325
+ }
326
+ const path = (0, core_1.normalize)(input);
327
+ const inside = root === '' ? !path.startsWith('..') : path.startsWith(`${root}/`);
328
+ if (inside &&
329
+ !/(^|\/)node_modules\//.test(path) &&
330
+ /\.(css|scss|less)$/.test(path) &&
331
+ tree.exists(path)) {
332
+ return path;
333
+ }
334
+ }
335
+ return undefined;
336
+ }
337
+ // --------------------------------------------------------------------------
338
+ // 5. Toast outlet
339
+ // --------------------------------------------------------------------------
340
+ /** `<z-toast-outlet` as an element; a comment that mentions it does not count. */
341
+ function hasToastOutlet(template) {
342
+ return /<z-toast-outlet[\s/>]/.test(template.replace(/<!--[\s\S]*?-->/g, ''));
343
+ }
344
+ /**
345
+ * Mounts `<z-toast-outlet />` in the root component and adds `ZToastOutlet` to
346
+ * its `imports`. Everything is checked first and every change to the component
347
+ * file is computed against its original source and applied in one pass, so the
348
+ * component is either fully patched or untouched. When it cannot be patched
349
+ * safely the schematic reports what to do by hand instead of guessing.
350
+ */
351
+ function toastOutletRule(projectName, report) {
352
+ return async (tree) => {
353
+ const manual = (reason) => report.manual.push(`Toast outlet: ${reason} Nothing was changed. Add ${TOAST_TAG} at the end of the root ` +
354
+ `component's template and ${TOAST_OUTLET} (import { ${TOAST_OUTLET} } from ` +
355
+ `'zenit-ui') to the "imports" array of that component yourself.`);
356
+ const mainPath = await mainFile(tree, projectName);
357
+ if (!mainPath) {
358
+ manual('the main entry point of the project could not be resolved.');
359
+ return;
360
+ }
361
+ let bootstrapped = null;
362
+ try {
363
+ bootstrapped = (0, app_component_1.resolveBootstrappedComponentData)(tree, mainPath);
364
+ }
365
+ catch {
366
+ // Neither bootstrapApplication() nor a resolvable bootstrapModule().
367
+ }
368
+ if (bootstrapped?.moduleName || isNgModuleApp(tree, mainPath)) {
369
+ // A non-standalone root component has no `imports`, its NgModule has them.
370
+ report.manual.push(`Toast outlet: ${mainPath} bootstraps an NgModule. Nothing was changed. Add ` +
371
+ `${TOAST_OUTLET} (import { ${TOAST_OUTLET} } from 'zenit-ui') to the "imports" of the ` +
372
+ `NgModule that declares your root component, and ${TOAST_TAG} at the end of that ` +
373
+ "component's template.");
374
+ return;
375
+ }
376
+ if (!bootstrapped) {
377
+ manual(`no bootstrapApplication(Component, ...) call was found in ${mainPath}.`);
378
+ return;
379
+ }
380
+ const { componentName, componentImportPathInSameFile } = bootstrapped;
381
+ const componentPath = (0, core_1.join)((0, core_1.dirname)((0, core_1.normalize)(mainPath)), `${componentImportPathInSameFile}.ts`);
382
+ if (!tree.exists(componentPath)) {
383
+ manual(`the root component file ${componentPath} was not found.`);
384
+ return;
385
+ }
386
+ const source = (0, util_1.getSourceFile)(tree, componentPath);
387
+ const eol = (0, eol_1.getEOL)(source.text);
388
+ const metadata = (0, ast_utils_1.getDecoratorMetadata)(source, 'Component', '@angular/core')
389
+ .filter(typescript_1.default.isObjectLiteralExpression)
390
+ .find((node) => {
391
+ // object literal -> call -> decorator -> class
392
+ const owner = node.parent?.parent?.parent;
393
+ return owner && typescript_1.default.isClassDeclaration(owner) && owner.name?.text === componentName;
394
+ });
395
+ if (!metadata) {
396
+ manual(`${componentPath} has no @Component({...}) on a class ${componentName}.`);
397
+ return;
398
+ }
399
+ const changes = [];
400
+ // 1. `imports`: extend an array literal or add the property, nothing else.
401
+ const named = metadata.properties.filter(({ name }) => name && (typescript_1.default.isIdentifier(name) || typescript_1.default.isStringLiteral(name)) && name.text === 'imports');
402
+ const imports = named[0];
403
+ const list = imports &&
404
+ typescript_1.default.isPropertyAssignment(imports) &&
405
+ typescript_1.default.isArrayLiteralExpression(imports.initializer)
406
+ ? imports.initializer.elements
407
+ : undefined;
408
+ const spreadOnly = list !== undefined && list.length > 0 && list.every(typescript_1.default.isSpreadElement);
409
+ if (imports && (named.length > 1 || !list || spreadOnly)) {
410
+ manual(`the "imports" of ${componentPath} are not a plain array literal ` +
411
+ `(${imports.getText().slice(0, 60)}).`);
412
+ return;
413
+ }
414
+ if (!list) {
415
+ const at = metadata.getStart() + 1;
416
+ changes.push(new change_1.InsertChange(componentPath, at, `${eol} imports: [${TOAST_OUTLET}],`));
417
+ }
418
+ else if (!list.some((element) => element.getText() === TOAST_OUTLET)) {
419
+ changes.push(list.length === 0
420
+ ? new change_1.InsertChange(componentPath, list.pos, TOAST_OUTLET)
421
+ : new change_1.InsertChange(componentPath, list[list.length - 1].getEnd(), `, ${TOAST_OUTLET}`));
422
+ }
423
+ if (changes.length > 0) {
424
+ const importChange = (0, ast_utils_1.insertImport)(source, componentPath, TOAST_OUTLET, 'zenit-ui');
425
+ if (importChange instanceof change_1.NoopChange && !(0, ast_utils_1.isImported)(source, TOAST_OUTLET, 'zenit-ui')) {
426
+ manual(`${componentPath} imports "zenit-ui" as a namespace.`);
427
+ return;
428
+ }
429
+ changes.push(importChange);
430
+ }
431
+ // 2. Template: external file or inline template literal.
432
+ let external;
433
+ const templateUrl = (0, ast_utils_1.getMetadataField)(metadata, 'templateUrl')[0];
434
+ const template = (0, ast_utils_1.getMetadataField)(metadata, 'template')[0];
435
+ if (templateUrl && typescript_1.default.isStringLiteralLike(templateUrl.initializer)) {
436
+ const path = (0, core_1.join)((0, core_1.dirname)(componentPath), templateUrl.initializer.text);
437
+ if (!tree.exists(path)) {
438
+ manual(`the template ${path} of ${componentPath} was not found.`);
439
+ return;
440
+ }
441
+ const content = tree.readText(path);
442
+ if (!hasToastOutlet(content)) {
443
+ const nl = (0, eol_1.getEOL)(content);
444
+ external = { path, content: `${content.replace(/\s*$/, '')}${nl}${nl}${TOAST_TAG}${nl}` };
445
+ }
446
+ }
447
+ else if (template && typescript_1.default.isNoSubstitutionTemplateLiteral(template.initializer)) {
448
+ if (!hasToastOutlet(template.initializer.text)) {
449
+ // A multi-line template gets the tag after its last content line, at that
450
+ // indentation; a one-line template is opened up.
451
+ const inner = template.initializer.getText().slice(1, -1);
452
+ const content = inner.trimEnd();
453
+ const trailing = inner.slice(content.length);
454
+ const at = template.initializer.getStart() + 1 + content.length;
455
+ const indent = `${trailing.slice(trailing.lastIndexOf('\n') + 1)} `;
456
+ changes.push(new change_1.InsertChange(componentPath, at, trailing.includes('\n')
457
+ ? `${eol}${indent}${TOAST_TAG}`
458
+ : `${eol} ${TOAST_TAG}${eol} `));
459
+ }
460
+ }
461
+ else {
462
+ manual(`${componentPath} has neither a "templateUrl" nor a "template" written as a plain ` +
463
+ 'template literal (backticks without ${...}).');
464
+ return;
465
+ }
466
+ // Everything is decided: one recorder for the component, one write for the template.
467
+ (0, util_1.applyChangesToFile)(tree, componentPath, changes);
468
+ if (external) {
469
+ tree.overwrite(external.path, external.content);
470
+ }
471
+ report.done.push(`${TOAST_TAG} mounted in the root component ${componentPath}.`);
472
+ };
473
+ }
474
+ /** The main entry point of the project, `undefined` when it cannot be resolved. */
475
+ async function mainFile(tree, projectName) {
476
+ try {
477
+ const path = await (0, util_1.getMainFilePath)(tree, projectName);
478
+ return path && tree.exists(path) ? path : undefined;
479
+ }
480
+ catch {
481
+ return undefined;
482
+ }
483
+ }
484
+ /** `platformBrowser().bootstrapModule(AppModule)` in the main file. */
485
+ function isNgModuleApp(tree, mainPath) {
486
+ return /\bbootstrapModule\s*\(/.test(tree.readText(mainPath));
487
+ }
488
+ // --------------------------------------------------------------------------
489
+ // 6. Theme without a flash (--themes)
490
+ // --------------------------------------------------------------------------
491
+ /** Marker comment of the init script; its presence makes the step idempotent. */
492
+ const THEME_MARKER = 'zenit-theme-init';
493
+ const THEME_PROVIDER = 'provideZenitTheme';
494
+ /**
495
+ * Three steps that only work together. `ZTheme` applies the stored scheme after
496
+ * bootstrap, which is several frames after the first paint, so:
497
+ *
498
+ * 1. the init script sets `data-theme` and `data-accent` in `<head>`
499
+ * (`indexHtmlRule`, together with `z-root`, so `index.html` is written once),
500
+ * 2. `inlineCritical` is switched off for the production build. The CLI would
501
+ * otherwise inline only the CSS that matches `index.html` and load the full
502
+ * stylesheet without blocking; `[data-theme="light"]` matches nothing there,
503
+ * so the page would still paint in the default scheme until the full
504
+ * stylesheet arrives,
505
+ * 3. `provideZenitTheme()` goes into the application config, so the service
506
+ * takes over from the script with the same defaults.
507
+ *
508
+ * The block goes in front of every stylesheet: right after `<meta charset>`,
509
+ * which has to stay within the first 1024 bytes of the document, or as the
510
+ * first child of `<head>` when there is none.
511
+ */
512
+ function initScriptLines() {
513
+ return [
514
+ `<!-- ${THEME_MARKER}: output of zenitThemeInitScript() from zenit-ui. It applies the stored`,
515
+ ' theme before the first paint. Regenerate it when you pass a config to',
516
+ ' provideZenitTheme(); with a CSP, hash or nonce it (docs/theming.md). -->',
517
+ '<!-- prettier-ignore -->',
518
+ `<script>${(0, init_script_1.zenitThemeInitScript)()}</script>`,
519
+ ];
520
+ }
521
+ /** Switches critical CSS inlining off for the production build, keeping every other setting. */
522
+ function inlineCriticalRule(projectName, report) {
523
+ return (0, utility_1.updateWorkspace)((workspace) => {
524
+ const build = workspace.projects.get(projectName)?.targets.get('build');
525
+ if (!build) {
526
+ return;
527
+ }
528
+ const configurations = (build.configurations ??= {});
529
+ const production = (configurations['production'] ??= {});
530
+ const current = production['optimization'];
531
+ const styles = isJsonObject(current) ? current['styles'] : undefined;
532
+ // `false` means nothing is optimised and nothing is inlined either.
533
+ if (current === false || styles === false) {
534
+ return;
535
+ }
536
+ production['optimization'] = {
537
+ ...(isJsonObject(current) ? current : { scripts: true, fonts: true }),
538
+ styles: {
539
+ minify: true,
540
+ removeSpecialComments: true,
541
+ ...(isJsonObject(styles) ? styles : {}),
542
+ inlineCritical: false,
543
+ },
544
+ };
545
+ report.done.push('optimization.styles.inlineCritical is false for production: the CLI would otherwise ' +
546
+ 'inline only the CSS that matches index.html and load the rest late, and ' +
547
+ '[data-theme="light"] matches nothing there, so a stored light scheme would paint dark first.');
548
+ });
549
+ }
550
+ function isJsonObject(value) {
551
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
552
+ }
553
+ /**
554
+ * Adds `provideZenitTheme()` to the application config. Only where that is
555
+ * safe: a `bootstrapApplication()` call whose config can be resolved. Anything
556
+ * else gets the instruction instead of a guess.
557
+ */
558
+ function themeProviderRule(projectName, report) {
559
+ return async (tree) => {
560
+ const manual = (reason, where = 'the "providers" of your application config') => report.manual.push(`Theme: ${reason} Add ${THEME_PROVIDER}() (import { ${THEME_PROVIDER} } from ` +
561
+ `'zenit-ui') to ${where} yourself.`);
562
+ const mainPath = await mainFile(tree, projectName);
563
+ if (!mainPath) {
564
+ manual('the main entry point of the project could not be resolved.');
565
+ return;
566
+ }
567
+ if (isNgModuleApp(tree, mainPath)) {
568
+ manual(`${mainPath} bootstraps an NgModule.`, 'the "providers" of your root NgModule');
569
+ return;
570
+ }
571
+ let configPath;
572
+ try {
573
+ const bootstrap = (0, util_1.findBootstrapApplicationCall)(tree, mainPath);
574
+ const appConfig = (0, app_config_1.findAppConfig)(bootstrap, tree, mainPath);
575
+ if (!appConfig && bootstrap.arguments.length !== 1) {
576
+ manual(`the config passed to bootstrapApplication() in ${mainPath} could not be resolved.`);
577
+ return;
578
+ }
579
+ // findAppConfig() joins with the platform separator.
580
+ configPath = (appConfig?.filePath ?? mainPath).replace(/\\/g, '/');
581
+ }
582
+ catch {
583
+ manual('no bootstrapApplication() call could be located.');
584
+ return;
585
+ }
586
+ const before = tree.readText(configPath);
587
+ report.done.push(`${THEME_PROVIDER}() in the providers of ${configPath}.`);
588
+ if (before.includes(`${THEME_PROVIDER}(`)) {
589
+ return;
590
+ }
591
+ return keepingCrlf(configPath, (0, utility_1.addRootProvider)(projectName, ({ code, external }) => code `${external(THEME_PROVIDER, 'zenit-ui')}()`));
592
+ };
593
+ }
594
+ // --------------------------------------------------------------------------
595
+ // 7. Final log
596
+ // --------------------------------------------------------------------------
597
+ /** Says what was done and, separately and as a warning, what is left to the user. */
598
+ function nextSteps(report) {
599
+ return (_tree, context) => {
600
+ const list = (items) => items.map((item) => ` - ${item}`).join('\n');
601
+ context.logger.info(`zenit-ui: done\n${list(report.done)}\n` +
602
+ ' Next: import the building blocks you need from "zenit-ui", and copy the Stylelint\n' +
603
+ ' rules from the package README so your own styles stay on tokens. With a config for\n' +
604
+ ' provideZenitTheme(), regenerate the init script with zenitThemeInitScript(config).');
605
+ if (report.manual.length > 0) {
606
+ context.logger.warn(`zenit-ui: left for you\n${list(report.manual)}`);
607
+ }
608
+ };
609
+ }