@blotch/angular-lynx 22.2.1 → 22.2.3

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 (34) hide show
  1. package/README.md +3 -1
  2. package/fesm2022/blotch-angular-lynx-web-glass-8MFrhPz0.mjs +951 -0
  3. package/fesm2022/blotch-angular-lynx-web-glass-8MFrhPz0.mjs.map +1 -0
  4. package/fesm2022/blotch-angular-lynx.mjs +8084 -0
  5. package/fesm2022/blotch-angular-lynx.mjs.map +1 -0
  6. package/package.json +11 -6
  7. package/schematics/add-i18n/index.js +168 -0
  8. package/schematics/add-i18n/schema.js +2 -0
  9. package/schematics/add-tailwind/index.js +100 -0
  10. package/schematics/add-tailwind/schema.js +2 -0
  11. package/schematics/add-testing/index.js +80 -0
  12. package/schematics/add-testing/schema.js +2 -0
  13. package/schematics/collection.json +7 -2
  14. package/schematics/component/index.js +110 -0
  15. package/schematics/component/schema.js +2 -0
  16. package/schematics/component/schema.json +102 -25
  17. package/schematics/eject/index.js +87 -0
  18. package/schematics/eject/schema.js +2 -0
  19. package/schematics/eject/schema.json +24 -0
  20. package/schematics/files/src/rspeedy-env.d.ts.template +1 -1
  21. package/schematics/migrations/accessibility-traits/index.js +56 -0
  22. package/schematics/migrations/dev-logs-gitignore/index.js +9 -0
  23. package/schematics/migrations/extract-i18n-target/index.js +31 -0
  24. package/schematics/migrations/tailwind-html-templates/index.js +44 -0
  25. package/schematics/migrations.json +25 -0
  26. package/schematics/ng-add/index.js +480 -0
  27. package/schematics/ng-add/schema.js +2 -0
  28. package/schematics/ng-add/schema.json +6 -2
  29. package/schematics/ng-add/versions.js +57 -0
  30. package/schematics/package.json +3 -0
  31. package/schematics/utility/dev-logs-gitignore.js +25 -0
  32. package/schematics/utility/extract-i18n-target.js +58 -0
  33. package/types/blotch-angular-lynx.d.ts +4789 -0
  34. package/schematics/files/lynx.config.ts.template +0 -17
@@ -0,0 +1,480 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const schematics_1 = require("@angular-devkit/schematics");
4
+ const dependency_1 = require("@schematics/angular/utility/dependency");
5
+ const json_file_1 = require("@schematics/angular/utility/json-file");
6
+ const workspace_1 = require("@schematics/angular/utility/workspace");
7
+ const dev_logs_gitignore_1 = require("../utility/dev-logs-gitignore");
8
+ const extract_i18n_target_1 = require("../utility/extract-i18n-target");
9
+ const versions_1 = require("./versions");
10
+ /**
11
+ * Transforms a standard `ng new` Angular project into a Lynx-native application.
12
+ * The schematic replaces browser-specific bootstrap (main.ts, app.config.ts,
13
+ * app.component.ts) with Lynx equivalents, points `ng build`, `ng serve`,
14
+ * `ng test` and `ng extract-i18n` at the AngularLynx builders and
15
+ * `ng generate` at its schematics, git-ignores the `.dev-logs` file that
16
+ * `ng serve` writes `LynxLogger`'s output to, and optionally sets up Tailwind
17
+ * with the Lynx preset. Everything else stays as `ng new` left it: the app
18
+ * needs no build config besides angular.json.
19
+ * This is a one-shot destructive transform — the old browser-Angular files
20
+ * are deleted because they reference DOM APIs and HTML elements that don't
21
+ * exist on the Lynx runtime.
22
+ */
23
+ exports.default = (options) => async (tree) => {
24
+ const workspace = await (0, workspace_1.getWorkspace)(tree);
25
+ const projectName = options.project ??
26
+ workspace.extensions['defaultProject'] ??
27
+ [...workspace.projects.keys()][0];
28
+ if (!projectName) {
29
+ throw new schematics_1.SchematicsException('No project found. Run this schematic inside an Angular workspace.');
30
+ }
31
+ const project = workspace.projects.get(projectName);
32
+ if (!project) {
33
+ throw new schematics_1.SchematicsException(`Project "${projectName}" not found in angular.json.`);
34
+ }
35
+ const sourceRoot = (project.sourceRoot ?? `${project.root}/src`);
36
+ const projectRoot = project.root;
37
+ const prefix = project.extensions['prefix'] ?? 'app';
38
+ // The tsconfig the app is built with. `ng new` points the build target's
39
+ // `tsConfig` at tsconfig.app.json.
40
+ const buildTsConfig = project.targets.get('build')?.options?.['tsConfig'];
41
+ const appTsconfig = typeof buildTsConfig === 'string'
42
+ ? buildTsConfig
43
+ : `${projectRoot ? projectRoot + '/' : ''}tsconfig.app.json`;
44
+ const rules = [
45
+ addTemplateFiles(projectRoot, sourceRoot),
46
+ replaceMainTs(sourceRoot),
47
+ replaceAppConfig(sourceRoot),
48
+ replaceApp(sourceRoot, prefix, projectName),
49
+ deleteOldAppFiles(sourceRoot),
50
+ excludeTestFilesFromAppBuild(appTsconfig),
51
+ includeTestFilesInSpecTsconfig(`${projectRoot ? projectRoot + '/' : ''}tsconfig.spec.json`),
52
+ useLynxBuilders(projectName),
53
+ useLynxSchematics(),
54
+ updatePackageScripts(),
55
+ (0, dev_logs_gitignore_1.ignoreDevLogs)(),
56
+ addCoreDependencies(),
57
+ ];
58
+ // `"false"` is how `ng add ... --tailwind=false` arrives (see schema.ts).
59
+ if (options.tailwind !== false && options.tailwind !== 'false') {
60
+ rules.push(addTailwindConfig(projectRoot), updateStylesCss(sourceRoot), addTailwindDependencies());
61
+ }
62
+ return (0, schematics_1.chain)(rules);
63
+ };
64
+ const addTemplateFiles = (projectRoot, _sourceRoot) => (0, schematics_1.mergeWith)(
65
+ // MergeStrategy.Overwrite is deliberate — `ng add` is meant to be
66
+ // idempotent so re-running it should refresh the template files
67
+ // (src/rspeedy-env.d.ts) to the package's current version. Default merge
68
+ // strategy would error on file collisions.
69
+ (0, schematics_1.apply)((0, schematics_1.url)('../files'), [(0, schematics_1.applyTemplates)({}), (0, schematics_1.move)(projectRoot || '/')]), schematics_1.MergeStrategy.Overwrite);
70
+ const replaceMainTs = (sourceRoot) => {
71
+ return (tree) => {
72
+ // `ng new` generates a main.ts that calls bootstrapApplication from
73
+ // @angular/platform-browser. That platform implementation reaches into
74
+ // browser globals (document, history, navigator) which don't exist on
75
+ // Lynx, so we replace the bootstrap entrypoint with the AngularLynx
76
+ // equivalent that wires the Lynx renderer instead.
77
+ const mainPath = `${sourceRoot}/main.ts`;
78
+ const content = `import { bootstrapApplication } from '@blotch/angular-lynx';
79
+ import { App } from './app/app';
80
+ import { appConfig } from './app/app.config';
81
+
82
+ bootstrapApplication(App, appConfig);
83
+ `;
84
+ tree.overwrite(mainPath, content);
85
+ };
86
+ };
87
+ const replaceAppConfig = (sourceRoot) => {
88
+ return (tree) => {
89
+ // Two providers are required for any AngularLynx app:
90
+ // 1. provideZonelessChangeDetection() — Lynx doesn't carry zone.js, so
91
+ // Angular must run in zoneless mode (signals + manual change detection
92
+ // triggers are the only reactivity path).
93
+ // 2. provideRenderer() — registers the Lynx RendererFactory2 over
94
+ // Angular's default DOM renderer so element creation calls go to
95
+ // __CreateElement/__SetAttribute instead of document.createElement.
96
+ // provideRouter is included by default so the generated App has a working
97
+ // <router-outlet> for the user to hang their routes off.
98
+ const configPath = `${sourceRoot}/app/app.config.ts`;
99
+ const content = `import {
100
+ type ApplicationConfig,
101
+ provideZonelessChangeDetection,
102
+ } from '@angular/core';
103
+ import { provideRenderer, provideRouter } from '@blotch/angular-lynx';
104
+
105
+ import { routes } from './app.routes';
106
+
107
+ export const appConfig: ApplicationConfig = {
108
+ providers: [
109
+ provideZonelessChangeDetection(),
110
+ provideRenderer(),
111
+ provideRouter(routes),
112
+ ],
113
+ };
114
+ `;
115
+ tree.overwrite(configPath, content);
116
+ };
117
+ };
118
+ const replaceApp = (sourceRoot, prefix, projectName) => {
119
+ return (tree) => {
120
+ // `ng new` before Angular 20 generated app.component.ts — delete it and
121
+ // create app.ts. The Angular 2025 style guide drops the `.component`
122
+ // suffix, and AngularLynx's component schematic follows the same
123
+ // convention so the bootstrap component should too.
124
+ const oldPath = `${sourceRoot}/app/app.component.ts`;
125
+ if (tree.exists(oldPath)) {
126
+ tree.delete(oldPath);
127
+ }
128
+ // LYNX_ELEMENTS is the directive bundle that registers <view>, <text>,
129
+ // <scroll-view>, etc. as Angular elements. Without it, the template would
130
+ // fail compilation because those tags aren't known HTML elements and the
131
+ // user hasn't yet added CUSTOM_ELEMENTS_SCHEMA (the build plugin injects
132
+ // it automatically but the type-checker still surfaces template errors).
133
+ const newPath = `${sourceRoot}/app/app.ts`;
134
+ const content = `import { Component, signal } from '@angular/core';
135
+ import { RouterOutlet } from '@angular/router';
136
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
137
+
138
+ @Component({
139
+ selector: '${prefix}-root',
140
+ imports: [RouterOutlet, LYNX_ELEMENTS],
141
+ template: \`
142
+ <scroll-view scroll-orientation="vertical">
143
+ <view style="padding: 24px; align-items: center;">
144
+ <text style="font-size: 24px; font-weight: bold;">{{ title() }}</text>
145
+ <text style="font-size: 14px; margin-top: 8px; color: #666;">
146
+ Edit src/app/app.ts to get started
147
+ </text>
148
+ <router-outlet />
149
+ </view>
150
+ </scroll-view>
151
+ \`,
152
+ })
153
+ export class App {
154
+ title = signal('${projectName}');
155
+ }
156
+ `;
157
+ if (tree.exists(newPath)) {
158
+ tree.overwrite(newPath, content);
159
+ }
160
+ else {
161
+ tree.create(newPath, content);
162
+ }
163
+ };
164
+ };
165
+ const deleteOldAppFiles = (sourceRoot) => {
166
+ return (tree) => {
167
+ // Remove files generated by `ng new` that we don't need.
168
+ // The root component's template and styles are dead once we inline the
169
+ // template above, and its spec tests the browser template we replaced, so
170
+ // it would fail if left behind. Angular 20+ names them app.html, app.css
171
+ // and app.spec.ts; earlier versions used an `app.component.` prefix. We
172
+ // don't try to migrate the user's existing template/styles because this
173
+ // schematic is intended for fresh `ng new` projects — running it on an
174
+ // established app is unsupported.
175
+ for (const file of [
176
+ `${sourceRoot}/app/app.html`,
177
+ `${sourceRoot}/app/app.spec.ts`,
178
+ `${sourceRoot}/app/app.css`,
179
+ `${sourceRoot}/app/app.scss`,
180
+ `${sourceRoot}/app/app.component.html`,
181
+ `${sourceRoot}/app/app.component.spec.ts`,
182
+ `${sourceRoot}/app/app.component.css`,
183
+ `${sourceRoot}/app/app.component.scss`,
184
+ ]) {
185
+ if (tree.exists(file)) {
186
+ tree.delete(file);
187
+ }
188
+ }
189
+ };
190
+ };
191
+ const excludeTestFilesFromAppBuild = (tsconfigPath) => {
192
+ return (tree) => {
193
+ // `ng new` excludes `*.spec.ts` from the app build, but AngularLynx's
194
+ // component and add-testing schematics name tests `*.test.ts`. Left
195
+ // in, the Rsbuild plugin compiles every test file into the app, and a
196
+ // test importing the testing library (a devDependency the app bundle
197
+ // can't resolve) produces build errors.
198
+ if (!tree.exists(tsconfigPath)) {
199
+ return;
200
+ }
201
+ const tsconfig = new json_file_1.JSONFile(tree, tsconfigPath);
202
+ const exclude = tsconfig.get(['exclude']);
203
+ const patterns = Array.isArray(exclude) ? exclude : [];
204
+ if (!patterns.includes('src/**/*.test.ts')) {
205
+ tsconfig.modify(['exclude'], [...patterns, 'src/**/*.test.ts']);
206
+ }
207
+ };
208
+ };
209
+ const includeTestFilesInSpecTsconfig = (tsconfigPath) => {
210
+ return (tree) => {
211
+ // `ng test` compiles `*.test.ts` files alongside `ng new`'s `*.spec.ts`
212
+ // ones, and AngularLynx's schematics name tests `*.test.ts`. Listed in
213
+ // tsconfig.spec.json, editors type-check them with the test types
214
+ // (vitest/globals) instead of treating them as files of no project.
215
+ if (!tree.exists(tsconfigPath)) {
216
+ return;
217
+ }
218
+ const tsconfig = new json_file_1.JSONFile(tree, tsconfigPath);
219
+ const include = tsconfig.get(['include']);
220
+ if (Array.isArray(include) && !include.includes('src/**/*.test.ts')) {
221
+ tsconfig.modify(['include'], [...include, 'src/**/*.test.ts']);
222
+ }
223
+ };
224
+ };
225
+ /**
226
+ * Angular compiler options from `ng new`'s build target that the Lynx builder
227
+ * accepts (see @blotch/angular-lynx-build's builders/application/schema.json).
228
+ * @blotch/rsbuild-plugin-angular-lynx and `ng test` read them from
229
+ * angular.json; anything else, such as options only the browser builder
230
+ * knows, would fail the builder's option validation. `optimization` also picks
231
+ * the bundle's production or development mode.
232
+ */
233
+ const ANGULAR_BUILD_OPTIONS = new Set([
234
+ 'browser',
235
+ 'tsConfig',
236
+ 'polyfills',
237
+ 'styles',
238
+ 'assets',
239
+ 'scripts',
240
+ 'inlineStyleLanguage',
241
+ 'fileReplacements',
242
+ 'aot',
243
+ 'optimization',
244
+ 'sourceMap',
245
+ 'outputHashing',
246
+ 'i18nMissingTranslation',
247
+ 'budgets',
248
+ 'extractLicenses',
249
+ 'outputPath',
250
+ 'index',
251
+ ]);
252
+ const keepAngularOptions = (options) => Object.fromEntries(Object.entries(options ?? {}).filter(([key]) => ANGULAR_BUILD_OPTIONS.has(key)));
253
+ /**
254
+ * Options of Angular's dev server that the Lynx dev server accepts too (see
255
+ * @blotch/angular-lynx-build's builders/dev-server/schema.json).
256
+ */
257
+ const DEV_SERVER_OPTIONS = new Set(['buildTarget', 'port', 'host']);
258
+ const keepDevServerOptions = (options) => Object.fromEntries(Object.entries(options ?? {}).filter(([key]) => DEV_SERVER_OPTIONS.has(key)));
259
+ /**
260
+ * `ng new`'s initial-bundle budget, sized for a web app's first download.
261
+ */
262
+ const NG_NEW_INITIAL_BUDGET = { maximumWarning: '500kB', maximumError: '1MB' };
263
+ /**
264
+ * The initial-bundle budget for a Lynx bundle. It carries more than a web
265
+ * app's first download: Angular runs on both of Lynx's threads, so the
266
+ * framework is in it twice, and a new app's bundle is already about 1 MB.
267
+ * Kept at roughly the same multiples of a new app as Angular's budget.
268
+ */
269
+ const LYNX_INITIAL_BUDGET = { maximumWarning: '2MB', maximumError: '5MB' };
270
+ /**
271
+ * A configuration's options with `ng new`'s initial-bundle budget resized for
272
+ * a Lynx bundle. Budgets the user set differently stay as they are.
273
+ */
274
+ const withLynxBudgets = (options) => {
275
+ if (!Array.isArray(options['budgets'])) {
276
+ return options;
277
+ }
278
+ return {
279
+ ...options,
280
+ budgets: options['budgets'].map((budget) => budget['type'] === 'initial' &&
281
+ budget['maximumWarning'] === NG_NEW_INITIAL_BUDGET.maximumWarning &&
282
+ budget['maximumError'] === NG_NEW_INITIAL_BUDGET.maximumError
283
+ ? { ...budget, ...LYNX_INITIAL_BUDGET }
284
+ : budget),
285
+ };
286
+ };
287
+ /**
288
+ * Points `ng build`, `ng serve`, `ng test` and `ng extract-i18n` at the
289
+ * AngularLynx builders. The first two run rspeedy with
290
+ * @blotch/rsbuild-plugin-angular-lynx added, and
291
+ * `ng build --platform ios|android` also produces the native app; the other
292
+ * two run Angular's unit-test and extract-i18n builders set up for Lynx. The
293
+ * build target keeps `ng new`'s Angular compiler options, which the build
294
+ * plugin, `ng test` and `ng extract-i18n` read, and gains the app's identity
295
+ * for native builds.
296
+ */
297
+ const useLynxBuilders = (projectName) => (0, workspace_1.updateWorkspace)((workspace) => {
298
+ const project = workspace.projects.get(projectName);
299
+ const build = project.targets.get('build');
300
+ const configurations = Object.fromEntries(Object.entries(build?.configurations ?? {}).map(([name, options]) => [
301
+ name,
302
+ withLynxBudgets(keepAngularOptions(options)),
303
+ ]));
304
+ const buildDefinition = {
305
+ builder: '@blotch/angular-lynx-build:application',
306
+ options: {
307
+ ...keepAngularOptions(build?.options),
308
+ appName: projectName,
309
+ // A placeholder the user replaces before publishing: the stores
310
+ // identify the app by it, and it can't change after the first upload.
311
+ appId: `com.example.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '') || 'app'}`,
312
+ },
313
+ configurations,
314
+ defaultConfiguration: build?.defaultConfiguration ?? 'production',
315
+ };
316
+ if (build) {
317
+ project.targets.delete('build');
318
+ }
319
+ project.targets.add({ name: 'build', ...buildDefinition });
320
+ // The Lynx dev server takes `buildTarget` like Angular's, so `ng new`'s
321
+ // configurations (production and development, each naming its build
322
+ // configuration) carry over, and `ng serve -c production` keeps working.
323
+ // Options only the browser dev server knows (ssl, proxyConfig, ...) are
324
+ // dropped: they would fail the builder's option validation.
325
+ // Without a serve target to keep, write the one `ng new` would have, for
326
+ // the build configurations that exist. Without a development one, serve
327
+ // the build target's default configuration rather than the dev server's
328
+ // default (`::development`), which would point at nothing.
329
+ const serve = project.targets.get('serve');
330
+ const serveConfigurations = Object.fromEntries(Object.entries(serve?.configurations ??
331
+ Object.fromEntries(['production', 'development']
332
+ .filter((name) => name in configurations)
333
+ .map((name) => [
334
+ name,
335
+ { buildTarget: `${projectName}:build:${name}` },
336
+ ]))).map(([name, options]) => [name, keepDevServerOptions(options)]));
337
+ const serveDefinition = serve
338
+ ? {
339
+ builder: '@blotch/angular-lynx-build:dev-server',
340
+ options: keepDevServerOptions(serve.options),
341
+ configurations: serveConfigurations,
342
+ defaultConfiguration: serve.defaultConfiguration,
343
+ }
344
+ : {
345
+ builder: '@blotch/angular-lynx-build:dev-server',
346
+ options: 'development' in serveConfigurations
347
+ ? {}
348
+ : { buildTarget: `${projectName}:build` },
349
+ configurations: serveConfigurations,
350
+ defaultConfiguration: 'development' in serveConfigurations ? 'development' : undefined,
351
+ };
352
+ if (serve) {
353
+ project.targets.delete('serve');
354
+ }
355
+ project.targets.add({ name: 'serve', ...serveDefinition });
356
+ // The Lynx unit-test builder is Angular's, set up for Lynx, so it takes
357
+ // the same options and `ng new`'s carry over (as do its own, when `ng add`
358
+ // runs again). Any other test builder (Karma, from older `ng new`
359
+ // versions) can't run Lynx code; its options don't apply.
360
+ const test = project.targets.get('test');
361
+ const keepTest = test?.builder === '@angular/build:unit-test' ||
362
+ test?.builder === '@blotch/angular-lynx-build:unit-test';
363
+ if (test) {
364
+ project.targets.delete('test');
365
+ }
366
+ project.targets.add({
367
+ name: 'test',
368
+ builder: '@blotch/angular-lynx-build:unit-test',
369
+ ...(keepTest && {
370
+ options: test.options,
371
+ configurations: test.configurations,
372
+ defaultConfiguration: test.defaultConfiguration,
373
+ }),
374
+ });
375
+ (0, extract_i18n_target_1.useLynxExtractI18n)(project);
376
+ });
377
+ /**
378
+ * Makes `ng generate` reach AngularLynx's schematics first, so
379
+ * `ng generate component` makes a component with Lynx elements. Angular's own
380
+ * collection stays after it: AngularLynx only has the schematics that differ
381
+ * on Lynx, and `ng generate service`, `pipe`, `guard` and the rest come from
382
+ * Angular's as before.
383
+ */
384
+ const useLynxSchematics = () => (0, workspace_1.updateWorkspace)((workspace) => {
385
+ const cli = (workspace.extensions['cli'] ?? {});
386
+ const collections = Array.isArray(cli['schematicCollections'])
387
+ ? cli['schematicCollections']
388
+ : // What Angular CLI uses when the list isn't set.
389
+ ['@schematics/angular'];
390
+ workspace.extensions['cli'] = {
391
+ ...cli,
392
+ schematicCollections: [
393
+ '@blotch/angular-lynx',
394
+ ...collections.filter((collection) => collection !== '@blotch/angular-lynx'),
395
+ ],
396
+ };
397
+ });
398
+ const updatePackageScripts = () => {
399
+ return (tree) => {
400
+ // `npm start` and `npm run build` go through the Angular CLI, which runs
401
+ // the AngularLynx builders set up by useLynxBuilders. `ng new` already
402
+ // writes these (and `npm test` as `ng test`); this keeps them for
403
+ // projects whose scripts called rspeedy directly.
404
+ const pkg = new json_file_1.JSONFile(tree, '/package.json');
405
+ pkg.modify(['scripts', 'start'], 'ng serve');
406
+ pkg.modify(['scripts', 'build'], 'ng build');
407
+ };
408
+ };
409
+ const addCoreDependencies = () => {
410
+ return (0, schematics_1.chain)([
411
+ // The `ng build`, `ng serve` and `ng test` builders, the one build
412
+ // dependency an AngularLynx app needs, as `@angular/build` is for a web
413
+ // app. They bring rspeedy, the rsbuild plugin that compiles Angular, the
414
+ // QR code plugin and the iOS and Android app builder with them. Taken at
415
+ // this package's own release, which they were tested with.
416
+ (0, dependency_1.addDependency)('@blotch/angular-lynx-build', (0, versions_1.ownVersionRange)(), {
417
+ type: dependency_1.DependencyType.Dev,
418
+ install: dependency_1.InstallBehavior.Auto,
419
+ }),
420
+ (0, dependency_1.addDependency)('@lynx-js/types', versions_1.VERSIONS.lynxTypes, {
421
+ type: dependency_1.DependencyType.Dev,
422
+ install: dependency_1.InstallBehavior.Auto,
423
+ }),
424
+ ]);
425
+ };
426
+ const addTailwindConfig = (projectRoot) => {
427
+ return (tree) => {
428
+ const configPath = `${projectRoot ? projectRoot + '/' : ''}tailwind.config.ts`;
429
+ const content = `import type { Config } from 'tailwindcss';
430
+ import preset from '@lynx-js/tailwind-preset';
431
+
432
+ const config: Config = {
433
+ content: ['./src/**/*.{html,ts}'],
434
+ presets: [preset],
435
+ };
436
+
437
+ export default config;
438
+ `;
439
+ if (tree.exists(configPath)) {
440
+ tree.overwrite(configPath, content);
441
+ }
442
+ else {
443
+ tree.create(configPath, content);
444
+ }
445
+ };
446
+ };
447
+ const updateStylesCss = (sourceRoot) => {
448
+ return (tree) => {
449
+ const cssPath = `${sourceRoot}/styles.css`;
450
+ const scssPath = `${sourceRoot}/styles.scss`;
451
+ const targetPath = tree.exists(scssPath) ? scssPath : cssPath;
452
+ // Only @tailwind base + utilities — no @tailwind components. Lynx doesn't
453
+ // support all of Tailwind's component-layer reset rules (e.g. button
454
+ // resets that reference focus-visible, form input pseudo-classes), and
455
+ // bundling them in would generate CSS the runtime can't parse, producing
456
+ // diagnostic noise. The base layer ships with @lynx-js/tailwind-preset's
457
+ // own Lynx-compatible normalize rules.
458
+ const content = `@tailwind base;
459
+ @tailwind utilities;
460
+ `;
461
+ if (tree.exists(targetPath)) {
462
+ tree.overwrite(targetPath, content);
463
+ }
464
+ else {
465
+ tree.create(cssPath, content);
466
+ }
467
+ };
468
+ };
469
+ const addTailwindDependencies = () => {
470
+ return (0, schematics_1.chain)([
471
+ (0, dependency_1.addDependency)('tailwindcss', versions_1.VERSIONS.tailwindcss, {
472
+ type: dependency_1.DependencyType.Dev,
473
+ install: dependency_1.InstallBehavior.Auto,
474
+ }),
475
+ (0, dependency_1.addDependency)('@lynx-js/tailwind-preset', versions_1.VERSIONS.tailwindPreset, {
476
+ type: dependency_1.DependencyType.Dev,
477
+ install: dependency_1.InstallBehavior.Auto,
478
+ }),
479
+ ]);
480
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -14,10 +14,14 @@
14
14
  }
15
15
  },
16
16
  "tailwind": {
17
- "type": "boolean",
17
+ "type": ["boolean", "string"],
18
+ "pattern": "^(true|false)$",
18
19
  "description": "Whether to add Tailwind CSS with @lynx-js/tailwind-preset.",
19
20
  "default": true,
20
- "x-prompt": "Would you like to add Tailwind CSS with Lynx preset?"
21
+ "x-prompt": {
22
+ "message": "Would you like to add Tailwind CSS with Lynx preset?",
23
+ "type": "confirmation"
24
+ }
21
25
  }
22
26
  },
23
27
  "required": []
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ownVersionRange = exports.VERSIONS = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
7
+ /**
8
+ * Versions of the third-party packages the `ng add @blotch/angular-lynx`
9
+ * schematics install: the ones AngularLynx is tested with.
10
+ *
11
+ * Keep this map in sync when upgrading any Lynx or build-tooling dependency.
12
+ * It lists none of our own packages or Angular's: those come from
13
+ * {@link ownVersionRange} and the app's own `@angular/core`, so they can't go
14
+ * stale.
15
+ */
16
+ exports.VERSIONS = {
17
+ /**
18
+ * @lynx-js/types — TypeScript type definitions for the Lynx runtime APIs
19
+ */
20
+ lynxTypes: '^4.3.0',
21
+ /**
22
+ * tailwindcss — utility-first CSS framework (optional, controlled by the `tailwind` schema option)
23
+ */
24
+ tailwindcss: '^3.4.19',
25
+ /**
26
+ * @lynx-js/tailwind-preset — Lynx-specific Tailwind preset with native design tokens
27
+ */
28
+ tailwindPreset: '^0.6.0',
29
+ /**
30
+ * vitest — fast unit test runner powered by Vite. Only added when the
31
+ * project has none; `ng new` already adds it.
32
+ */
33
+ vitest: '^5.0.0',
34
+ /**
35
+ * jsdom — DOM environment for running component tests. Only added when the
36
+ * project has none; `ng new` already adds it.
37
+ */
38
+ jsdom: '^30.0.0',
39
+ };
40
+ /**
41
+ * The range the schematics install our other packages at
42
+ * (@blotch/angular-lynx-build, the testing library): the version of
43
+ * @blotch/angular-lynx they shipped in. Every package is released at one
44
+ * version (see packages/release), so a range written out here would go stale
45
+ * at each release unless someone remembered to move it, as `^22.2.1` nearly
46
+ * shipped in 22.2.2. The E2E tests check that a new app gets this range.
47
+ *
48
+ * `../../package.json` is the runtime's package.json both in the repository
49
+ * and in the published package, where ng-packagr copies it into dist/ beside
50
+ * the schematics. (`../package.json` is schematics/package.json, which only
51
+ * marks the schematics as CommonJS.)
52
+ */
53
+ const ownVersionRange = () => {
54
+ const { version } = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(__dirname, '../../package.json'), 'utf8'));
55
+ return `^${version}`;
56
+ };
57
+ exports.ownVersionRange = ownVersionRange;
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ignoreDevLogs = void 0;
4
+ /** An entry that already ignores `.dev-logs`, anchored or not. */
5
+ const DEV_LOGS_ENTRY = /^\/?\.dev-logs\/?\s*$/m;
6
+ /**
7
+ * Adds `.dev-logs` to the workspace's `.gitignore`. `ng serve` writes
8
+ * `LynxLogger`'s output to that file, in the folder it runs from, and it
9
+ * shouldn't be committed. The entry has no leading slash, so it matches
10
+ * wherever the file is.
11
+ *
12
+ * A workspace without a `.gitignore` (`ng new --skip-git`) is left alone,
13
+ * and so is one that ignores the file already.
14
+ */
15
+ const ignoreDevLogs = () => (tree) => {
16
+ const file = '/.gitignore';
17
+ if (!tree.exists(file))
18
+ return;
19
+ const text = tree.readText(file);
20
+ if (DEV_LOGS_ENTRY.test(text))
21
+ return;
22
+ const separator = text === '' || text.endsWith('\n') ? '' : '\n';
23
+ tree.overwrite(file, `${text}${separator}\n# LynxLogger's output while \`ng serve\` runs\n.dev-logs\n`);
24
+ };
25
+ exports.ignoreDevLogs = ignoreDevLogs;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jsonMessagesOptions = exports.useLynxExtractI18n = void 0;
4
+ const LYNX_EXTRACT_I18N_BUILDER = '@blotch/angular-lynx-build:extract-i18n';
5
+ /**
6
+ * Angular's extract-i18n builders. The Lynx builder hands its options to
7
+ * `@angular/build:extract-i18n`, and `@angular-devkit/build-angular`'s takes
8
+ * the same ones, so a target of either works with it unchanged.
9
+ */
10
+ const ANGULAR_EXTRACT_I18N_BUILDERS = [
11
+ '@angular/build:extract-i18n',
12
+ '@angular-devkit/build-angular:extract-i18n',
13
+ ];
14
+ /**
15
+ * Points a project's `extract-i18n` target at the AngularLynx builder, so
16
+ * `ng extract-i18n` works. Angular CLI picks an extractor by itself only for
17
+ * a project built with one of Angular's own builders; for the Lynx build
18
+ * builder it fails with "Cannot determine project or target for command."
19
+ * Angular's extractor would fail anyway on the first imported image, which
20
+ * the Lynx builder sets it up for.
21
+ *
22
+ * `options` are set where the target doesn't set them already, so a format
23
+ * or path the app chose stays. A target of Angular's switches builder; a
24
+ * target with any other builder is the app's own choice, and stays as it is.
25
+ */
26
+ const useLynxExtractI18n = (project, options = {}) => {
27
+ const target = project.targets.get('extract-i18n');
28
+ if (!target) {
29
+ project.targets.add({
30
+ name: 'extract-i18n',
31
+ builder: LYNX_EXTRACT_I18N_BUILDER,
32
+ ...(Object.keys(options).length > 0 && { options }),
33
+ });
34
+ return;
35
+ }
36
+ if (target.builder !== LYNX_EXTRACT_I18N_BUILDER &&
37
+ !ANGULAR_EXTRACT_I18N_BUILDERS.includes(target.builder)) {
38
+ return;
39
+ }
40
+ target.builder = LYNX_EXTRACT_I18N_BUILDER;
41
+ const missing = Object.entries(options).filter(([name]) => !(name in (target.options ?? {})));
42
+ if (missing.length > 0) {
43
+ target.options = { ...target.options, ...Object.fromEntries(missing) };
44
+ }
45
+ };
46
+ exports.useLynxExtractI18n = useLynxExtractI18n;
47
+ /**
48
+ * The options `add-i18n` gives the `extract-i18n` target: the messages go as
49
+ * JSON to `<sourceRoot>/locale/messages.json`, where Angular's i18n guide
50
+ * keeps translation files. Angular's default format is XLIFF, but an app on
51
+ * Lynx loads its translations at runtime with `loadTranslations()`, which
52
+ * takes the JSON file's `translations` object as it is.
53
+ */
54
+ const jsonMessagesOptions = (sourceRoot) => ({
55
+ format: 'json',
56
+ outputPath: `${sourceRoot}/locale`,
57
+ });
58
+ exports.jsonMessagesOptions = jsonMessagesOptions;