@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/README.md +186 -36
- package/dist/add-component.js +6 -6
- package/dist/index.js +175 -29
- package/dist/init.js +23 -12
- package/dist/project.js +312 -85
- package/dist/prompt.js +59 -0
- package/package.json +1 -1
- package/templates/button/button.skin.ts +41 -41
- package/templates/styles/trine-baseline.css +125 -0
- package/templates/styles/tokens.css +0 -102
- package/templates/styles/trine-consumer.css +0 -58
package/dist/project.js
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
const
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
158
|
-
const
|
|
159
|
-
|
|
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:
|
|
169
|
+
authoringImportStillPresent: inspection.authoringImportStillPresent,
|
|
170
|
+
warnings: inspection.warnings,
|
|
163
171
|
};
|
|
164
172
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
return resolvedFromWorkspace;
|
|
262
|
+
for (const resolvedFromWorkspace of resolveStylesEntriesFromAngularWorkspace(angularJsonPath, targetRoot)) {
|
|
263
|
+
candidates.add(resolvedFromWorkspace);
|
|
229
264
|
}
|
|
230
265
|
}
|
|
231
|
-
return
|
|
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
|
|
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
|
-
|
|
346
|
+
matches.push(absolutePath);
|
|
311
347
|
}
|
|
312
348
|
}
|
|
313
349
|
}
|
|
350
|
+
return matches;
|
|
314
351
|
}
|
|
315
352
|
catch {
|
|
316
|
-
return
|
|
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
|
+
}
|