@trineui/cli 0.2.0 → 0.4.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.
package/dist/project.js CHANGED
@@ -1,17 +1,35 @@
1
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import ts from 'typescript';
5
- const STYLE_SOURCE_FILES = ['styles/tokens.css', 'styles/trine-consumer.css'];
6
- const STYLE_IMPORT_LINE = "@import './styles/trine-consumer.css';";
5
+ const TRINE_BASELINE_START_LINE = '/* --- Trine styling baseline: start --- */';
6
+ const TRINE_BASELINE_END_LINE = '/* --- Trine styling baseline: end --- */';
7
+ const TRINE_BASELINE_TEMPLATE_FILE = 'styles/trine-baseline.css';
8
+ const LEGACY_STYLE_IMPORT_LINES = [
9
+ "@import './styles/trine-consumer.css';",
10
+ '@import "./styles/trine-consumer.css";',
11
+ ];
12
+ const AUTHORING_STYLE_IMPORT_LINES = [
13
+ "@import '@trine/ui/styles/trine.css';",
14
+ '@import "@trine/ui/styles/trine.css";',
15
+ ];
16
+ const LEGACY_STYLE_FILES = ['src/styles/tokens.css', 'src/styles/trine-consumer.css'];
17
+ const POSTCSS_CONFIG_FILE_NAMES = [
18
+ '.postcssrc.json',
19
+ '.postcssrc',
20
+ 'postcss.config.js',
21
+ 'postcss.config.cjs',
22
+ 'postcss.config.mjs',
23
+ ];
7
24
  const CONVENTIONAL_STYLE_ENTRY_PATHS = ['src/styles.scss', 'src/styles.css', 'src/global.scss'];
8
25
  export const TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
9
26
  const LOCAL_REPO_ROOT = path.resolve(TEMPLATE_ROOT, '../../..');
10
27
  export const LOCAL_REPO_DEMO_ROOT = path.resolve(TEMPLATE_ROOT, '../../../apps/demo');
11
- export function inspectProjectTarget(targetRoot) {
28
+ export function inspectProjectTarget(targetRoot, options = {}) {
12
29
  const targetAppDir = path.join(targetRoot, 'src', 'app');
13
30
  const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
14
- const targetStylesEntry = resolveStylesEntry(targetRoot);
31
+ const targetStylesEntryCandidates = resolveStylesEntryCandidates(targetRoot);
32
+ const { targetStylesEntry, targetStylesEntryResolution } = resolveStylesEntrySelection(targetRoot, targetStylesEntryCandidates, options.preferredStylesEntry);
15
33
  const missingPaths = [];
16
34
  if (!existsSync(targetRoot)) {
17
35
  missingPaths.push(targetRoot);
@@ -30,13 +48,15 @@ export function inspectProjectTarget(targetRoot) {
30
48
  targetRoot,
31
49
  targetAppDir,
32
50
  targetTsconfig,
51
+ targetStylesEntryCandidates,
33
52
  targetStylesEntry,
53
+ targetStylesEntryResolution,
34
54
  framework,
35
55
  missingPaths,
36
56
  };
37
57
  }
38
- export function resolveSupportedProjectTarget(commandLabel, targetRoot) {
39
- const inspection = inspectProjectTarget(targetRoot);
58
+ export function resolveSupportedProjectTarget(commandLabel, targetRoot, options = {}) {
59
+ const inspection = inspectProjectTarget(targetRoot, options);
40
60
  if (inspection.framework === 'unsupported' || !inspection.targetStylesEntry) {
41
61
  throw new Error([
42
62
  `${commandLabel} requires a supported Angular or Ionic Angular app target with src/app, a global stylesheet entry, and tsconfig.app.json.`,
@@ -59,45 +79,20 @@ export function findAngularAppTargets(root) {
59
79
  export function looksLikeAngularAppRoot(root) {
60
80
  return inspectProjectTarget(root).framework !== 'unsupported';
61
81
  }
62
- export function ensureSharedStyleBaseline(targetRoot, componentLabel) {
63
- const stylesDestDir = path.join(targetRoot, 'src', 'styles');
64
- const createdFiles = [];
65
- const reusedFiles = [];
66
- const warnings = [];
67
- mkdirSync(stylesDestDir, { recursive: true });
68
- for (const sourceFile of STYLE_SOURCE_FILES) {
69
- const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
70
- const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
71
- const relativeDestination = toTargetRelativePath(targetRoot, destinationPath);
72
- if (!existsSync(destinationPath)) {
73
- copyFileSync(templatePath, destinationPath);
74
- createdFiles.push(relativeDestination);
75
- continue;
76
- }
77
- if (readFileSync(destinationPath, 'utf8') !== readFileSync(templatePath, 'utf8')) {
78
- warnings.push(`${relativeDestination} already exists and was preserved. Review it manually if the delivered ${componentLabel} expects newer shared styling baseline content.`);
79
- continue;
80
- }
81
- reusedFiles.push(relativeDestination);
82
- }
83
- return {
84
- createdFiles,
85
- reusedFiles,
86
- warnings,
87
- };
88
- }
89
82
  export function ensureUiRootBarrel(targetRoot) {
90
83
  const uiRootPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
91
- if (existsSync(uiRootPath)) {
92
- return {
93
- created: false,
94
- file: toTargetRelativePath(targetRoot, uiRootPath),
95
- };
84
+ const inspection = inspectUiRootBarrel(targetRoot);
85
+ if (!inspection.created) {
86
+ return inspection;
96
87
  }
97
88
  mkdirSync(path.dirname(uiRootPath), { recursive: true });
98
89
  writeFileSync(uiRootPath, '\n');
90
+ return inspection;
91
+ }
92
+ export function inspectUiRootBarrel(targetRoot) {
93
+ const uiRootPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
99
94
  return {
100
- created: true,
95
+ created: !existsSync(uiRootPath),
101
96
  file: toTargetRelativePath(targetRoot, uiRootPath),
102
97
  };
103
98
  }
@@ -122,6 +117,10 @@ export function ensureLinesFile(filePath, lines) {
122
117
  return changed;
123
118
  }
124
119
  export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
120
+ const inspection = inspectTsconfigAlias(tsconfigPath, targetRoot, cwd);
121
+ if (!inspection.needsUpdate) {
122
+ return false;
123
+ }
125
124
  const currentText = readFileSync(tsconfigPath, 'utf8');
126
125
  const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
127
126
  if (parsed.error) {
@@ -130,20 +129,7 @@ export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
130
129
  const config = (parsed.config ?? {});
131
130
  config.compilerOptions ??= {};
132
131
  config.compilerOptions.paths ??= {};
133
- const aliasTarget = isDemoTarget(targetRoot)
134
- ? toPosixPath(path.relative(LOCAL_REPO_ROOT, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
135
- : toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
136
- const wildcardTarget = 'packages/ui/*';
137
- const currentAlias = config.compilerOptions.paths['@trine/ui'];
138
- const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
139
- const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
140
- const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
141
- (Array.isArray(currentWildcardAlias) &&
142
- currentWildcardAlias.length === 1 &&
143
- currentWildcardAlias[0] === wildcardTarget);
144
- if (aliasIsCurrent && wildcardIsCurrent) {
145
- return false;
146
- }
132
+ const { aliasTarget, wildcardTarget } = buildAliasTargets(tsconfigPath, targetRoot);
147
133
  config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
148
134
  if (isDemoTarget(targetRoot)) {
149
135
  config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
@@ -154,39 +140,75 @@ export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
154
140
  writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
155
141
  return true;
156
142
  }
157
- export function ensureStylesImport(stylesPath) {
158
- const current = readFileSync(stylesPath, 'utf8');
159
- if (current.includes(STYLE_IMPORT_LINE)) {
143
+ export function inspectTsconfigAlias(tsconfigPath, targetRoot, cwd) {
144
+ const currentText = readFileSync(tsconfigPath, 'utf8');
145
+ const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
146
+ if (parsed.error) {
147
+ throw new Error(`Unable to parse ${path.relative(cwd, tsconfigPath)} as JSONC.`);
148
+ }
149
+ const config = (parsed.config ?? {});
150
+ config.compilerOptions ??= {};
151
+ config.compilerOptions.paths ??= {};
152
+ const { aliasTarget, wildcardTarget } = buildAliasTargets(tsconfigPath, targetRoot);
153
+ const currentAlias = config.compilerOptions.paths['@trine/ui'];
154
+ const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
155
+ const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
156
+ const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
157
+ (Array.isArray(currentWildcardAlias) &&
158
+ currentWildcardAlias.length === 1 &&
159
+ currentWildcardAlias[0] === wildcardTarget);
160
+ return {
161
+ needsUpdate: !(aliasIsCurrent && wildcardIsCurrent),
162
+ };
163
+ }
164
+ export function ensureTrineStylesheetBaseline(targetRoot, stylesPath) {
165
+ const inspection = inspectTrineStylesheetBaseline(targetRoot, stylesPath);
166
+ if (!inspection.needsUpdate) {
160
167
  return {
161
168
  updated: false,
162
- authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
169
+ authoringImportStillPresent: inspection.authoringImportStillPresent,
170
+ warnings: inspection.warnings,
163
171
  };
164
172
  }
165
- const lines = current.split('\n');
166
- let insertAt = -1;
167
- for (let index = 0; index < lines.length; index += 1) {
168
- const trimmed = lines[index].trim();
169
- if (trimmed.startsWith('@use') || trimmed.startsWith('@import')) {
170
- insertAt = index;
171
- }
172
- }
173
- if (insertAt === -1) {
174
- lines.unshift(STYLE_IMPORT_LINE, '');
175
- }
176
- else {
177
- lines.splice(insertAt + 1, 0, STYLE_IMPORT_LINE);
178
- }
179
- writeFileSync(stylesPath, lines.join('\n'));
173
+ const current = readFileSync(stylesPath, 'utf8');
174
+ const next = isDemoTarget(targetRoot)
175
+ ? buildDemoAuthoringStylesheet(current)
176
+ : buildConsumerInjectedStylesheet(current);
177
+ writeFileSync(stylesPath, next);
180
178
  return {
181
179
  updated: true,
182
- authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
180
+ authoringImportStillPresent: isDemoTarget(targetRoot),
181
+ warnings: inspection.warnings,
182
+ };
183
+ }
184
+ export function inspectTrineStylesheetBaseline(targetRoot, stylesPath) {
185
+ const current = readFileSync(stylesPath, 'utf8');
186
+ const warnings = buildLegacyStyleWarnings(targetRoot);
187
+ const authoringImportStillPresent = containsExactImportLine(current, AUTHORING_STYLE_IMPORT_LINES);
188
+ if (isDemoTarget(targetRoot)) {
189
+ return {
190
+ needsUpdate: containsManagedBlock(current) ||
191
+ containsExactImportLine(current, LEGACY_STYLE_IMPORT_LINES) ||
192
+ !authoringImportStillPresent,
193
+ authoringImportStillPresent,
194
+ warnings,
195
+ };
196
+ }
197
+ const managedBlock = extractManagedBlock(current);
198
+ const expectedManagedBlock = buildManagedStylesheetBlock();
199
+ const hasLegacyImports = containsExactImportLine(current, LEGACY_STYLE_IMPORT_LINES) || authoringImportStillPresent;
200
+ const hasDuplicateTailwindImport = hasTailwindImportOutsideManagedBlock(current);
201
+ return {
202
+ needsUpdate: managedBlock !== expectedManagedBlock || hasLegacyImports || hasDuplicateTailwindImport,
203
+ authoringImportStillPresent,
204
+ warnings,
183
205
  };
184
206
  }
185
207
  export function readTargetDependencyWarnings(targetRoot, componentLabel) {
186
208
  const packageJsonPath = findNearestFileUpward(targetRoot, 'package.json');
187
209
  if (!packageJsonPath) {
188
210
  return [
189
- 'No package.json was found at or above the target root, so Tailwind CSS v4 and class-variance-authority prerequisites could not be checked automatically.',
211
+ 'No package.json was found at or above the target root, so Tailwind CSS v4, PostCSS, and class-variance-authority prerequisites could not be checked automatically.',
190
212
  ];
191
213
  }
192
214
  try {
@@ -206,29 +228,42 @@ export function readTargetDependencyWarnings(targetRoot, componentLabel) {
206
228
  else if (!looksLikeTailwindV4(tailwindRange)) {
207
229
  warnings.push(`The target repo declares tailwindcss@${tailwindRange}. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
208
230
  }
231
+ if (!deps['@tailwindcss/postcss']) {
232
+ warnings.push(`@tailwindcss/postcss is missing from the target repo. The current proven ${componentLabel} baseline expects it for Tailwind CSS v4 processing.`);
233
+ }
234
+ if (!deps['postcss']) {
235
+ warnings.push(`postcss is missing from the target repo. The current proven ${componentLabel} baseline expects it for Tailwind CSS v4 processing.`);
236
+ }
237
+ const postcssConfigPath = findNearestPostcssConfigPath(targetRoot);
238
+ if (!postcssConfigPath) {
239
+ warnings.push('No PostCSS config was found at or above the target root. The current proven baseline expects a PostCSS config that enables @tailwindcss/postcss.');
240
+ }
209
241
  return warnings;
210
242
  }
211
243
  catch {
212
244
  return [
213
- `package.json could not be parsed for dependency checks. Verify Tailwind CSS v4 and class-variance-authority manually before building the delivered ${componentLabel}.`,
245
+ `package.json could not be parsed for dependency checks. Verify Tailwind CSS v4, PostCSS, and class-variance-authority manually before building the delivered ${componentLabel}.`,
214
246
  ];
215
247
  }
216
248
  }
217
249
  export function resolveStylesEntry(targetRoot) {
250
+ return inspectProjectTarget(targetRoot).targetStylesEntry;
251
+ }
252
+ export function resolveStylesEntryCandidates(targetRoot) {
253
+ const candidates = new Set();
218
254
  for (const relativePath of CONVENTIONAL_STYLE_ENTRY_PATHS) {
219
255
  const absolutePath = path.join(targetRoot, relativePath);
220
256
  if (existsSync(absolutePath)) {
221
- return absolutePath;
257
+ candidates.add(absolutePath);
222
258
  }
223
259
  }
224
260
  const angularJsonPaths = findFilesUpward(targetRoot, 'angular.json');
225
261
  for (const angularJsonPath of angularJsonPaths) {
226
- const resolvedFromWorkspace = resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot);
227
- if (resolvedFromWorkspace) {
228
- return resolvedFromWorkspace;
262
+ for (const resolvedFromWorkspace of resolveStylesEntriesFromAngularWorkspace(angularJsonPath, targetRoot)) {
263
+ candidates.add(resolvedFromWorkspace);
229
264
  }
230
265
  }
231
- return undefined;
266
+ return [...candidates];
232
267
  }
233
268
  export function isDemoTarget(targetRoot) {
234
269
  return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
@@ -287,7 +322,7 @@ function packageJsonUsesIonicAngular(packageJsonPath) {
287
322
  return false;
288
323
  }
289
324
  }
290
- function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
325
+ function resolveStylesEntriesFromAngularWorkspace(angularJsonPath, targetRoot) {
291
326
  try {
292
327
  const angularJson = JSON.parse(readFileSync(angularJsonPath, 'utf8'));
293
328
  const workspaceRoot = path.dirname(angularJsonPath);
@@ -298,6 +333,7 @@ function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
298
333
  : path.resolve(targetRoot) === path.resolve(workspaceRoot)
299
334
  ? projects
300
335
  : [];
336
+ const matches = [];
301
337
  for (const project of candidateProjects) {
302
338
  const styles = project.architect?.build?.options?.styles ?? project.targets?.build?.options?.styles ?? [];
303
339
  for (const style of styles) {
@@ -307,15 +343,15 @@ function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
307
343
  }
308
344
  const absolutePath = path.resolve(workspaceRoot, input);
309
345
  if (existsSync(absolutePath)) {
310
- return absolutePath;
346
+ matches.push(absolutePath);
311
347
  }
312
348
  }
313
349
  }
350
+ return matches;
314
351
  }
315
352
  catch {
316
- return undefined;
353
+ return [];
317
354
  }
318
- return undefined;
319
355
  }
320
356
  function projectMatchesTargetRoot(project, workspaceRoot, targetRoot) {
321
357
  const resolvedTargetRoot = path.resolve(targetRoot);
@@ -355,7 +391,198 @@ function findFilesUpward(startDir, fileName) {
355
391
  function findNearestFileUpward(startDir, fileName) {
356
392
  return findFilesUpward(startDir, fileName)[0];
357
393
  }
394
+ function findNearestPostcssConfigPath(startDir) {
395
+ for (const fileName of POSTCSS_CONFIG_FILE_NAMES) {
396
+ const match = findNearestFileUpward(startDir, fileName);
397
+ if (match) {
398
+ return match;
399
+ }
400
+ }
401
+ return undefined;
402
+ }
358
403
  function looksLikeTailwindV4(range) {
359
404
  const versionTokenPattern = /(?:^|[<>=~^|\s:])v?(\d+)(?:(?:\.\d+){0,2})/g;
360
405
  return [...range.matchAll(versionTokenPattern)].some((match) => Number(match[1]) === 4);
361
406
  }
407
+ function buildAliasTargets(tsconfigPath, targetRoot) {
408
+ const aliasTarget = isDemoTarget(targetRoot)
409
+ ? toPosixPath(path.relative(LOCAL_REPO_ROOT, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
410
+ : toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
411
+ const wildcardTarget = 'packages/ui/*';
412
+ return {
413
+ aliasTarget,
414
+ wildcardTarget,
415
+ };
416
+ }
417
+ function resolveStylesEntrySelection(targetRoot, candidates, preferredStylesEntry) {
418
+ if (preferredStylesEntry) {
419
+ const preferredAbsolutePath = path.resolve(targetRoot, preferredStylesEntry);
420
+ if (candidates.includes(preferredAbsolutePath)) {
421
+ return {
422
+ targetStylesEntry: preferredAbsolutePath,
423
+ targetStylesEntryResolution: 'preferred',
424
+ };
425
+ }
426
+ }
427
+ const managedBlockCandidates = candidates.filter((candidate) => {
428
+ try {
429
+ return containsManagedBlock(readFileSync(candidate, 'utf8'));
430
+ }
431
+ catch {
432
+ return false;
433
+ }
434
+ });
435
+ if (managedBlockCandidates.length === 1) {
436
+ return {
437
+ targetStylesEntry: managedBlockCandidates[0],
438
+ targetStylesEntryResolution: 'managed-block',
439
+ };
440
+ }
441
+ const legacyImportCandidates = candidates.filter((candidate) => {
442
+ try {
443
+ return containsExactImportLine(readFileSync(candidate, 'utf8'), LEGACY_STYLE_IMPORT_LINES);
444
+ }
445
+ catch {
446
+ return false;
447
+ }
448
+ });
449
+ if (legacyImportCandidates.length === 1) {
450
+ return {
451
+ targetStylesEntry: legacyImportCandidates[0],
452
+ targetStylesEntryResolution: 'legacy-import',
453
+ };
454
+ }
455
+ const authoringImportCandidates = candidates.filter((candidate) => {
456
+ try {
457
+ return containsExactImportLine(readFileSync(candidate, 'utf8'), AUTHORING_STYLE_IMPORT_LINES);
458
+ }
459
+ catch {
460
+ return false;
461
+ }
462
+ });
463
+ if (authoringImportCandidates.length === 1) {
464
+ return {
465
+ targetStylesEntry: authoringImportCandidates[0],
466
+ targetStylesEntryResolution: 'authoring-import',
467
+ };
468
+ }
469
+ if (candidates.length > 0) {
470
+ return {
471
+ targetStylesEntry: candidates[0],
472
+ targetStylesEntryResolution: 'default',
473
+ };
474
+ }
475
+ return {
476
+ targetStylesEntry: undefined,
477
+ targetStylesEntryResolution: 'none',
478
+ };
479
+ }
480
+ function buildManagedStylesheetBlock() {
481
+ const templatePath = path.join(TEMPLATE_ROOT, TRINE_BASELINE_TEMPLATE_FILE);
482
+ const templateContent = readFileSync(templatePath, 'utf8').trimEnd();
483
+ return `${TRINE_BASELINE_START_LINE}\n${templateContent}\n${TRINE_BASELINE_END_LINE}`;
484
+ }
485
+ function containsManagedBlock(current) {
486
+ return current.includes(TRINE_BASELINE_START_LINE) && current.includes(TRINE_BASELINE_END_LINE);
487
+ }
488
+ function extractManagedBlock(current) {
489
+ const match = current.match(new RegExp(`${escapeRegExp(TRINE_BASELINE_START_LINE)}[\\s\\S]*?${escapeRegExp(TRINE_BASELINE_END_LINE)}`));
490
+ return match?.[0];
491
+ }
492
+ function stripManagedBlock(current) {
493
+ return current.replace(new RegExp(`\\n?${escapeRegExp(TRINE_BASELINE_START_LINE)}[\\s\\S]*?${escapeRegExp(TRINE_BASELINE_END_LINE)}\\n?`, 'g'), '\n');
494
+ }
495
+ function buildConsumerInjectedStylesheet(current) {
496
+ const withoutManagedBlock = stripManagedBlock(current);
497
+ const withoutLegacyImports = removeExactImportLines(removeExactImportLines(withoutManagedBlock, LEGACY_STYLE_IMPORT_LINES), AUTHORING_STYLE_IMPORT_LINES);
498
+ const withoutTailwindImports = removeTailwindImports(withoutLegacyImports);
499
+ return insertManagedBlock(withoutTailwindImports, buildManagedStylesheetBlock());
500
+ }
501
+ function buildDemoAuthoringStylesheet(current) {
502
+ const withoutManagedBlock = stripManagedBlock(current);
503
+ const withoutLegacyImports = removeExactImportLines(withoutManagedBlock, LEGACY_STYLE_IMPORT_LINES);
504
+ if (containsExactImportLine(withoutLegacyImports, AUTHORING_STYLE_IMPORT_LINES)) {
505
+ return normalizeStylesheetText(withoutLegacyImports);
506
+ }
507
+ return insertManagedBlock(withoutLegacyImports, AUTHORING_STYLE_IMPORT_LINES[0]);
508
+ }
509
+ function removeExactImportLines(current, importsToRemove) {
510
+ return current
511
+ .split('\n')
512
+ .filter((line) => !importsToRemove.includes(line.trim()))
513
+ .join('\n');
514
+ }
515
+ function removeTailwindImports(current) {
516
+ return current
517
+ .split('\n')
518
+ .filter((line) => !isTailwindImportLine(line.trim()))
519
+ .join('\n');
520
+ }
521
+ function containsExactImportLine(current, candidates) {
522
+ return current.split('\n').some((line) => candidates.includes(line.trim()));
523
+ }
524
+ function hasTailwindImportOutsideManagedBlock(current) {
525
+ return stripManagedBlock(current)
526
+ .split('\n')
527
+ .some((line) => isTailwindImportLine(line.trim()));
528
+ }
529
+ function isTailwindImportLine(line) {
530
+ return (line === "@import 'tailwindcss';" ||
531
+ line === '@import "tailwindcss";' ||
532
+ line === "@import url('tailwindcss');" ||
533
+ line === '@import url("tailwindcss");');
534
+ }
535
+ function insertManagedBlock(current, block) {
536
+ const normalized = normalizeStylesheetText(current);
537
+ if (normalized.trim() === '') {
538
+ return `${block}\n`;
539
+ }
540
+ const lines = normalized.split('\n');
541
+ let insertAt = 0;
542
+ for (let index = 0; index < lines.length; index += 1) {
543
+ const trimmed = lines[index].trim();
544
+ if (isPreludeLine(trimmed)) {
545
+ insertAt = index + 1;
546
+ continue;
547
+ }
548
+ break;
549
+ }
550
+ const before = lines.slice(0, insertAt).join('\n').trimEnd();
551
+ const after = lines.slice(insertAt).join('\n').trimStart();
552
+ return joinSections(before, block, after);
553
+ }
554
+ function joinSections(...sections) {
555
+ const populatedSections = sections.filter((section) => section.trim() !== '');
556
+ if (populatedSections.length === 0) {
557
+ return '\n';
558
+ }
559
+ return `${populatedSections.join('\n\n').trimEnd()}\n`;
560
+ }
561
+ function isPreludeLine(trimmedLine) {
562
+ return (trimmedLine === '' ||
563
+ trimmedLine.startsWith('//') ||
564
+ trimmedLine.startsWith('/*') ||
565
+ trimmedLine.startsWith('*') ||
566
+ trimmedLine.startsWith('*/') ||
567
+ trimmedLine.startsWith('@charset') ||
568
+ trimmedLine.startsWith('@use') ||
569
+ trimmedLine.startsWith('@forward') ||
570
+ trimmedLine.startsWith('@import'));
571
+ }
572
+ function normalizeStylesheetText(current) {
573
+ return current
574
+ .replace(/\r\n/g, '\n')
575
+ .replace(/\n{3,}/g, '\n\n')
576
+ .trim();
577
+ }
578
+ function buildLegacyStyleWarnings(targetRoot) {
579
+ return listLegacyStyleFiles(targetRoot).map((file) => `${file} still exists from the older separate-file Trine baseline. The injected global stylesheet model no longer requires it, so remove it manually when you no longer need it.`);
580
+ }
581
+ function listLegacyStyleFiles(targetRoot) {
582
+ return LEGACY_STYLE_FILES.map((relativePath) => path.join(targetRoot, relativePath))
583
+ .filter((filePath) => existsSync(filePath))
584
+ .map((filePath) => toTargetRelativePath(targetRoot, filePath));
585
+ }
586
+ function escapeRegExp(value) {
587
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
588
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,59 @@
1
+ import process from 'node:process';
2
+ import { createInterface } from 'node:readline/promises';
3
+ export async function chooseFromList(message, items, options = {}) {
4
+ assertInteractiveTerminal();
5
+ if (items.length === 0) {
6
+ throw new Error('Cannot choose from an empty list.');
7
+ }
8
+ if (items.length === 1) {
9
+ return items[0];
10
+ }
11
+ const renderItem = options.renderItem ?? ((item, index) => `${String(index + 1)}. ${item}`);
12
+ console.log(message);
13
+ for (const [index, item] of items.entries()) {
14
+ console.log(renderItem(item, index));
15
+ }
16
+ const prompt = createPrompt();
17
+ try {
18
+ for (;;) {
19
+ const answer = (await prompt.question(`Choose an option [1-${String(items.length)}]: `)).trim();
20
+ const selectedIndex = Number(answer);
21
+ if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= items.length) {
22
+ return items[selectedIndex - 1];
23
+ }
24
+ console.log(`Enter a number between 1 and ${String(items.length)}.`);
25
+ }
26
+ }
27
+ finally {
28
+ prompt.close();
29
+ }
30
+ }
31
+ export async function confirmAction(message) {
32
+ assertInteractiveTerminal();
33
+ const prompt = createPrompt();
34
+ try {
35
+ const answer = (await prompt.question(`${message} `)).trim().toLowerCase();
36
+ if (answer === '' || answer === 'y' || answer === 'yes') {
37
+ return true;
38
+ }
39
+ if (answer === 'n' || answer === 'no') {
40
+ return false;
41
+ }
42
+ console.log('Enter Y, yes, N, or no.');
43
+ return await confirmAction(message);
44
+ }
45
+ finally {
46
+ prompt.close();
47
+ }
48
+ }
49
+ export function assertInteractiveTerminal() {
50
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
51
+ throw new Error('trine init default mode is guided and needs an interactive terminal. Re-run with --yes for non-interactive mode.');
52
+ }
53
+ }
54
+ function createPrompt() {
55
+ return createInterface({
56
+ input: process.stdin,
57
+ output: process.stdout,
58
+ });
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trineui/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Copy-paste ownership CLI for Trine UI components.",
6
6
  "main": "./dist/index.js",