@trineui/cli 0.1.2 → 0.2.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 +14 -2
- package/dist/add-component.js +4 -231
- package/dist/index.js +93 -73
- package/dist/init.js +54 -0
- package/dist/project.js +361 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,25 +6,36 @@ Canonical public package:
|
|
|
6
6
|
npx @trineui/cli@latest add button
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Registry status note:
|
|
10
|
+
|
|
11
|
+
- live npm `@latest` is currently `0.1.2`
|
|
12
|
+
- repo-local next publish candidate is `0.2.0`
|
|
13
|
+
- the live registry package currently exposes `add button`
|
|
14
|
+
- `init` is implemented in this repo and included in the next publish candidate, but it is not available from the live npm package until publish succeeds
|
|
15
|
+
|
|
16
|
+
Repository-local current command surface:
|
|
10
17
|
|
|
11
18
|
```bash
|
|
19
|
+
trine init --target <app-root>
|
|
12
20
|
trine add button --target <app-root>
|
|
13
21
|
```
|
|
14
22
|
|
|
15
23
|
Notes:
|
|
16
24
|
|
|
25
|
+
- `init` is now the preferred first step for project readiness; `add button` still works without it for backward compatibility
|
|
17
26
|
- `button` is the only supported component in this public-style baseline
|
|
18
27
|
- omitting `--target` uses the current directory when it already matches the supported Angular app shape
|
|
19
28
|
- when the current directory is not a supported Angular app target, the CLI auto-detects a single Angular app target under the current directory and proceeds automatically
|
|
20
29
|
- when multiple Angular app targets are found, the CLI stops and asks for `--target <app-root>`
|
|
21
30
|
- external targets can run `trine add button` from the app root or pass an explicit app root such as `--target /absolute/path/to/angular-app`
|
|
31
|
+
- `init` detects `angular` and `ionic-angular` targets conservatively; unsupported targets fail clearly
|
|
22
32
|
- the canonical public package name is `@trineui/cli`
|
|
23
33
|
- the CLI command exposed through the package bin is still `trine`
|
|
24
34
|
- `apps/consumer-fixture` is the first separate-target proof and does not use the demo-only `@trine/ui/*` bridge
|
|
25
35
|
- `/tmp/trine-button-publish-proof` is the latest truly external packaged-proof repo outside the monorepo
|
|
26
36
|
- packaged/public-style proof uses a packed local tarball to simulate `npx @trineui/cli@latest add button`
|
|
27
37
|
- the packaged CLI ships compiled runtime files plus Button templates so it can execute from `node_modules` in a real `npx`-style flow
|
|
38
|
+
- `init` ensures `src/styles/tokens.css`, `src/styles/trine-consumer.css`, `src/app/components/ui/index.ts`, the local `@trine/ui` alias, and local stylesheet wiring
|
|
28
39
|
- consumer-owned component destination files cause a clear failure
|
|
29
40
|
- existing shared baseline files (`tokens.css` and `trine-consumer.css`) are preserved so a second component can be added into the same target repo
|
|
30
41
|
- the command copies consumer-owned source instead of wiring runtime back to `packages/ui`
|
|
@@ -38,5 +49,6 @@ Notes:
|
|
|
38
49
|
Local package proof equivalent:
|
|
39
50
|
|
|
40
51
|
```bash
|
|
41
|
-
npx --yes --package /absolute/path/to/trineui-cli
|
|
52
|
+
npx --yes --package /absolute/path/to/trineui-cli-<version>.tgz trine init
|
|
53
|
+
npx --yes --package /absolute/path/to/trineui-cli-<version>.tgz trine add button
|
|
42
54
|
```
|
package/dist/add-component.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import ts from 'typescript';
|
|
5
|
-
const STYLE_SOURCE_FILES = ['styles/tokens.css', 'styles/trine-consumer.css'];
|
|
6
|
-
const TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
7
|
-
const LOCAL_REPO_DEMO_ROOT = path.resolve(TEMPLATE_ROOT, '../../../apps/demo');
|
|
8
|
-
const STYLE_IMPORT_LINE = "@import './styles/trine-consumer.css';";
|
|
3
|
+
import { TEMPLATE_ROOT, ensureLinesFile, ensureSharedStyleBaseline, ensureStylesImport, ensureTsconfigAlias, isDemoTarget, readTargetDependencyWarnings, resolveSupportedProjectTarget, toTargetRelativePath, } from "./project.js";
|
|
9
4
|
const DEMO_BRIDGE_COMMENT = '// Temporary demo verification bridge: delivered local components resolve locally; other components still re-export from the authoring source.';
|
|
10
5
|
const DEMO_LOCAL_COMPONENTS = [
|
|
11
6
|
{
|
|
@@ -61,9 +56,8 @@ const DEMO_PROXY_EXPORT_LINES = [
|
|
|
61
56
|
];
|
|
62
57
|
export function addComponent(manifest, options) {
|
|
63
58
|
const targetRoot = path.resolve(options.cwd, options.target);
|
|
64
|
-
const { targetStylesEntry, targetTsconfig } =
|
|
59
|
+
const { targetStylesEntry, targetTsconfig } = resolveSupportedProjectTarget(`trine add ${manifest.componentName}`, targetRoot);
|
|
65
60
|
const componentDestDir = path.join(targetRoot, 'src', 'app', 'components', 'ui', manifest.componentName);
|
|
66
|
-
const stylesDestDir = path.join(targetRoot, 'src', 'styles');
|
|
67
61
|
const componentCopyTargets = manifest.sourceFiles.map((source) => ({
|
|
68
62
|
source: path.join(TEMPLATE_ROOT, source),
|
|
69
63
|
destination: path.join(componentDestDir, path.basename(source)),
|
|
@@ -78,13 +72,12 @@ export function addComponent(manifest, options) {
|
|
|
78
72
|
].join('\n'));
|
|
79
73
|
}
|
|
80
74
|
mkdirSync(componentDestDir, { recursive: true });
|
|
81
|
-
mkdirSync(stylesDestDir, { recursive: true });
|
|
82
75
|
for (const { source, destination } of componentCopyTargets) {
|
|
83
76
|
copyFileSync(source, destination);
|
|
84
77
|
}
|
|
85
78
|
const componentCopiedFiles = componentCopyTargets.map(({ destination }) => toTargetRelativePath(targetRoot, destination));
|
|
86
|
-
const sharedStylesResult = ensureSharedStyleBaseline(targetRoot,
|
|
87
|
-
const copiedFiles = [...componentCopiedFiles, ...sharedStylesResult.
|
|
79
|
+
const sharedStylesResult = ensureSharedStyleBaseline(targetRoot, manifest.componentLabel);
|
|
80
|
+
const copiedFiles = [...componentCopiedFiles, ...sharedStylesResult.createdFiles];
|
|
88
81
|
const updatedFiles = [];
|
|
89
82
|
const warnings = [...sharedStylesResult.warnings];
|
|
90
83
|
const componentBarrelPath = path.join(componentDestDir, 'index.ts');
|
|
@@ -122,78 +115,6 @@ export function addComponent(manifest, options) {
|
|
|
122
115
|
targetRoot,
|
|
123
116
|
};
|
|
124
117
|
}
|
|
125
|
-
function ensureSharedStyleBaseline(targetRoot, stylesDestDir, componentLabel) {
|
|
126
|
-
const copiedFiles = [];
|
|
127
|
-
const warnings = [];
|
|
128
|
-
for (const sourceFile of STYLE_SOURCE_FILES) {
|
|
129
|
-
const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
|
|
130
|
-
const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
|
|
131
|
-
const relativeDestination = toTargetRelativePath(targetRoot, destinationPath);
|
|
132
|
-
if (!existsSync(destinationPath)) {
|
|
133
|
-
copyFileSync(templatePath, destinationPath);
|
|
134
|
-
copiedFiles.push(relativeDestination);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (readFileSync(destinationPath, 'utf8') !== readFileSync(templatePath, 'utf8')) {
|
|
138
|
-
warnings.push(`${relativeDestination} already exists and was preserved. Review it manually if the delivered ${componentLabel} expects newer shared styling baseline content.`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return {
|
|
142
|
-
copiedFiles,
|
|
143
|
-
warnings,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function resolveTargetShape(componentName, targetRoot) {
|
|
147
|
-
const targetAppDir = path.join(targetRoot, 'src', 'app');
|
|
148
|
-
const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
|
|
149
|
-
const targetStylesEntry = resolveStylesEntry(targetRoot);
|
|
150
|
-
const missing = [];
|
|
151
|
-
if (!existsSync(targetRoot)) {
|
|
152
|
-
missing.push(targetRoot);
|
|
153
|
-
}
|
|
154
|
-
if (!existsSync(targetAppDir)) {
|
|
155
|
-
missing.push(targetAppDir);
|
|
156
|
-
}
|
|
157
|
-
if (!targetStylesEntry) {
|
|
158
|
-
missing.push(`${path.join(targetRoot, 'src', 'styles.scss')} or ${path.join(targetRoot, 'src', 'styles.css')} or the first resolvable build styles entry in ${path.join(targetRoot, 'angular.json')}`);
|
|
159
|
-
}
|
|
160
|
-
if (!existsSync(targetTsconfig)) {
|
|
161
|
-
missing.push(targetTsconfig);
|
|
162
|
-
}
|
|
163
|
-
if (missing.length > 0) {
|
|
164
|
-
throw new Error([
|
|
165
|
-
`trine add ${componentName} requires an Angular app target with src/app, a global stylesheet entry, and tsconfig.app.json.`,
|
|
166
|
-
...missing.map((file) => `- ${file}`),
|
|
167
|
-
].join('\n'));
|
|
168
|
-
}
|
|
169
|
-
if (!targetStylesEntry) {
|
|
170
|
-
throw new Error(`trine add ${componentName} could not resolve a global stylesheet entry for ${targetRoot}.`);
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
targetStylesEntry,
|
|
174
|
-
targetTsconfig,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
function ensureLinesFile(filePath, lines) {
|
|
178
|
-
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
179
|
-
const normalizedExisting = existing.trimEnd();
|
|
180
|
-
const currentLines = normalizedExisting === '' ? [] : normalizedExisting.split('\n');
|
|
181
|
-
let changed = false;
|
|
182
|
-
for (const line of lines) {
|
|
183
|
-
if (!currentLines.includes(line)) {
|
|
184
|
-
currentLines.push(line);
|
|
185
|
-
changed = true;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (!existsSync(filePath)) {
|
|
189
|
-
changed = true;
|
|
190
|
-
}
|
|
191
|
-
if (changed) {
|
|
192
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
193
|
-
writeFileSync(filePath, `${currentLines.join('\n')}\n`);
|
|
194
|
-
}
|
|
195
|
-
return changed;
|
|
196
|
-
}
|
|
197
118
|
function rewriteDemoUiRootBarrel(filePath) {
|
|
198
119
|
const uiRootDir = path.dirname(filePath);
|
|
199
120
|
const localComponentLines = DEMO_LOCAL_COMPONENTS.filter(({ key }) => existsSync(path.join(uiRootDir, key, 'index.ts')));
|
|
@@ -212,151 +133,3 @@ function rewriteDemoUiRootBarrel(filePath) {
|
|
|
212
133
|
writeFileSync(filePath, nextContent);
|
|
213
134
|
return true;
|
|
214
135
|
}
|
|
215
|
-
function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
216
|
-
const currentText = readFileSync(tsconfigPath, 'utf8');
|
|
217
|
-
const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
|
|
218
|
-
if (parsed.error) {
|
|
219
|
-
throw new Error(`Unable to parse ${path.relative(cwd, tsconfigPath)} as JSONC.`);
|
|
220
|
-
}
|
|
221
|
-
const config = (parsed.config ?? {});
|
|
222
|
-
config.compilerOptions ??= {};
|
|
223
|
-
config.compilerOptions.paths ??= {};
|
|
224
|
-
const aliasTarget = isDemoTarget(targetRoot)
|
|
225
|
-
? toPosixPath(path.relative(process.cwd(), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
|
|
226
|
-
: toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
|
|
227
|
-
const wildcardTarget = 'packages/ui/*';
|
|
228
|
-
const currentAlias = config.compilerOptions.paths['@trine/ui'];
|
|
229
|
-
const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
|
|
230
|
-
const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
|
|
231
|
-
const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
|
|
232
|
-
(Array.isArray(currentWildcardAlias) &&
|
|
233
|
-
currentWildcardAlias.length === 1 &&
|
|
234
|
-
currentWildcardAlias[0] === wildcardTarget);
|
|
235
|
-
if (aliasIsCurrent && wildcardIsCurrent) {
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
|
|
239
|
-
if (isDemoTarget(targetRoot)) {
|
|
240
|
-
config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
|
|
241
|
-
}
|
|
242
|
-
else if ('@trine/ui/*' in config.compilerOptions.paths) {
|
|
243
|
-
delete config.compilerOptions.paths['@trine/ui/*'];
|
|
244
|
-
}
|
|
245
|
-
writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
function ensureStylesImport(stylesPath) {
|
|
249
|
-
const current = readFileSync(stylesPath, 'utf8');
|
|
250
|
-
if (current.includes(STYLE_IMPORT_LINE)) {
|
|
251
|
-
return {
|
|
252
|
-
updated: false,
|
|
253
|
-
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
const lines = current.split('\n');
|
|
257
|
-
let insertAt = -1;
|
|
258
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
259
|
-
const trimmed = lines[index].trim();
|
|
260
|
-
if (trimmed.startsWith('@use') || trimmed.startsWith('@import')) {
|
|
261
|
-
insertAt = index;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (insertAt === -1) {
|
|
265
|
-
lines.unshift(STYLE_IMPORT_LINE, '');
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
lines.splice(insertAt + 1, 0, STYLE_IMPORT_LINE);
|
|
269
|
-
}
|
|
270
|
-
writeFileSync(stylesPath, lines.join('\n'));
|
|
271
|
-
return {
|
|
272
|
-
updated: true,
|
|
273
|
-
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
function isDemoTarget(targetRoot) {
|
|
277
|
-
return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
|
|
278
|
-
path.resolve(targetRoot) === path.resolve(LOCAL_REPO_DEMO_ROOT));
|
|
279
|
-
}
|
|
280
|
-
function toPosixPath(filePath) {
|
|
281
|
-
return filePath.split(path.sep).join(path.posix.sep);
|
|
282
|
-
}
|
|
283
|
-
function toConfigRelativePath(filePath) {
|
|
284
|
-
const posixPath = toPosixPath(filePath);
|
|
285
|
-
return posixPath.startsWith('.') ? posixPath : `./${posixPath}`;
|
|
286
|
-
}
|
|
287
|
-
function toTargetRelativePath(targetRoot, filePath) {
|
|
288
|
-
return toPosixPath(path.relative(targetRoot, filePath));
|
|
289
|
-
}
|
|
290
|
-
function readTargetDependencyWarnings(targetRoot, componentLabel) {
|
|
291
|
-
const packageJsonPath = path.join(targetRoot, 'package.json');
|
|
292
|
-
if (!existsSync(packageJsonPath)) {
|
|
293
|
-
return [
|
|
294
|
-
'No package.json was found in the target root, so Tailwind CSS v4 and class-variance-authority prerequisites could not be checked automatically.',
|
|
295
|
-
];
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
299
|
-
const deps = {
|
|
300
|
-
...(packageJson.dependencies ?? {}),
|
|
301
|
-
...(packageJson.devDependencies ?? {}),
|
|
302
|
-
};
|
|
303
|
-
const warnings = [];
|
|
304
|
-
if (!deps['class-variance-authority']) {
|
|
305
|
-
warnings.push(`class-variance-authority is missing from the target repo. Install it before building the delivered ${componentLabel}.`);
|
|
306
|
-
}
|
|
307
|
-
const tailwindRange = deps['tailwindcss'];
|
|
308
|
-
if (!tailwindRange) {
|
|
309
|
-
warnings.push(`tailwindcss is missing from the target repo. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
|
|
310
|
-
}
|
|
311
|
-
else if (!looksLikeTailwindV4(tailwindRange)) {
|
|
312
|
-
warnings.push(`The target repo declares tailwindcss@${tailwindRange}. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
|
|
313
|
-
}
|
|
314
|
-
return warnings;
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
return [
|
|
318
|
-
`package.json could not be parsed for dependency checks. Verify Tailwind CSS v4 and class-variance-authority manually before building the delivered ${componentLabel}.`,
|
|
319
|
-
];
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
function looksLikeTailwindV4(range) {
|
|
323
|
-
return /(^|[^\d])4(\D|$)/.test(range);
|
|
324
|
-
}
|
|
325
|
-
export function looksLikeAngularAppRoot(root) {
|
|
326
|
-
return (existsSync(path.join(root, 'src', 'app')) &&
|
|
327
|
-
existsSync(path.join(root, 'tsconfig.app.json')) &&
|
|
328
|
-
resolveStylesEntry(root) !== undefined);
|
|
329
|
-
}
|
|
330
|
-
function resolveStylesEntry(targetRoot) {
|
|
331
|
-
const conventionalStyles = ['src/styles.scss', 'src/styles.css'];
|
|
332
|
-
for (const relativePath of conventionalStyles) {
|
|
333
|
-
const absolutePath = path.join(targetRoot, relativePath);
|
|
334
|
-
if (existsSync(absolutePath)) {
|
|
335
|
-
return absolutePath;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const angularJsonPath = path.join(targetRoot, 'angular.json');
|
|
339
|
-
if (!existsSync(angularJsonPath)) {
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
try {
|
|
343
|
-
const angularJson = JSON.parse(readFileSync(angularJsonPath, 'utf8'));
|
|
344
|
-
for (const project of Object.values(angularJson.projects ?? {})) {
|
|
345
|
-
const styles = project.architect?.build?.options?.styles ?? project.targets?.build?.options?.styles ?? [];
|
|
346
|
-
for (const styleEntry of styles) {
|
|
347
|
-
const relativePath = typeof styleEntry === 'string' ? styleEntry : (styleEntry.input ?? undefined);
|
|
348
|
-
if (!relativePath) {
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
const absolutePath = path.join(targetRoot, relativePath);
|
|
352
|
-
if (existsSync(absolutePath)) {
|
|
353
|
-
return absolutePath;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
return undefined;
|
|
360
|
-
}
|
|
361
|
-
return undefined;
|
|
362
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readdirSync } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import { looksLikeAngularAppRoot } from "./add-component.js";
|
|
5
3
|
import { addButton } from "./add-button.js";
|
|
4
|
+
import { initProject } from "./init.js";
|
|
5
|
+
import { findAngularAppTargets, looksLikeAngularAppRoot } from "./project.js";
|
|
6
6
|
const HELP_TEXT = `Usage:
|
|
7
|
+
npx @trineui/cli@latest init [--target <app-root>]
|
|
7
8
|
npx @trineui/cli@latest add button [--target <app-root>]
|
|
9
|
+
trine init [--target <app-root>]
|
|
8
10
|
trine add button [--target <app-root>]
|
|
9
11
|
|
|
10
12
|
Defaults:
|
|
11
|
-
- current directory when it matches the supported
|
|
12
|
-
- otherwise auto-detect a single Angular app target under the current directory
|
|
13
|
-
- when multiple
|
|
13
|
+
- current directory when it already matches the supported Trine app target shape
|
|
14
|
+
- otherwise auto-detect a single supported Angular app target under the current directory
|
|
15
|
+
- when multiple supported app targets are found, re-run with --target <app-root>
|
|
14
16
|
|
|
15
17
|
Notes:
|
|
16
|
-
- v0 supports
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- v0 supports init plus add button only
|
|
19
|
+
- init owns target detection, framework detection, stylesheet resolution, baseline files, local @trine/ui alias setup, and local stylesheet wiring
|
|
20
|
+
- add button still works without init for backward compatibility, but init is now the preferred first step
|
|
21
|
+
- the current proven target model is Angular 21 + src/app + tsconfig.app.json + a global stylesheet entry such as src/styles.scss, src/styles.css, or src/global.scss resolved directly or from angular.json
|
|
22
|
+
- v0 distinguishes angular and ionic-angular targets; unsupported frameworks fail clearly
|
|
20
23
|
- the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
|
|
21
24
|
- consumer-owned component files fail clearly if they already exist
|
|
22
25
|
- shared styling baseline files are copied when missing and preserved when they already exist
|
|
@@ -24,19 +27,28 @@ Notes:
|
|
|
24
27
|
- apps/demo keeps a temporary @trine/ui/* bridge for non-localized components during local repo verification`;
|
|
25
28
|
const SUPPORTED_COMPONENTS = ['button'];
|
|
26
29
|
function main(argv) {
|
|
27
|
-
const [command,
|
|
30
|
+
const [command, secondArg, ...rest] = argv;
|
|
31
|
+
if (command === 'init') {
|
|
32
|
+
const selection = resolveTargetSelection(process.cwd(), argv.slice(1), 'trine init');
|
|
33
|
+
const result = initProject({
|
|
34
|
+
target: selection.target,
|
|
35
|
+
cwd: process.cwd(),
|
|
36
|
+
});
|
|
37
|
+
printInitSuccess(result, selection);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
28
40
|
if (command !== 'add') {
|
|
29
41
|
throw new Error(command ? `Unsupported command: ${command}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
30
42
|
}
|
|
31
|
-
if (!isSupportedComponent(
|
|
32
|
-
throw new Error(
|
|
43
|
+
if (!isSupportedComponent(secondArg)) {
|
|
44
|
+
throw new Error(secondArg ? `Unsupported component: ${secondArg}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
33
45
|
}
|
|
34
|
-
const
|
|
46
|
+
const selection = resolveTargetSelection(process.cwd(), rest, `trine add ${secondArg}`);
|
|
35
47
|
const result = addButton({
|
|
36
|
-
target,
|
|
48
|
+
target: selection.target,
|
|
37
49
|
cwd: process.cwd(),
|
|
38
50
|
});
|
|
39
|
-
|
|
51
|
+
printAddSuccess(secondArg, result, selection);
|
|
40
52
|
}
|
|
41
53
|
function readTarget(argv) {
|
|
42
54
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -46,82 +58,90 @@ function readTarget(argv) {
|
|
|
46
58
|
}
|
|
47
59
|
return undefined;
|
|
48
60
|
}
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
function resolveTargetSelection(cwd, argv, commandLabel) {
|
|
62
|
+
const explicitTarget = readTarget(argv);
|
|
63
|
+
if (explicitTarget) {
|
|
64
|
+
return {
|
|
65
|
+
target: explicitTarget,
|
|
66
|
+
mode: 'explicit',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (looksLikeAngularAppRoot(cwd)) {
|
|
70
|
+
return {
|
|
71
|
+
target: '.',
|
|
72
|
+
mode: 'cwd',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const matches = findAngularAppTargets(cwd);
|
|
76
|
+
if (matches.length === 1) {
|
|
77
|
+
return {
|
|
78
|
+
target: matches[0],
|
|
79
|
+
mode: 'auto-detected',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (matches.length > 1) {
|
|
83
|
+
throw new Error([
|
|
84
|
+
`Multiple supported Angular app targets were found under the current directory. Re-run with ${commandLabel} --target <app-root>:`,
|
|
85
|
+
...matches.map((match) => `- ${match}`),
|
|
86
|
+
].join('\n'));
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
target: '.',
|
|
90
|
+
mode: 'cwd',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function printInitSuccess(result, selection) {
|
|
94
|
+
const displayTarget = toDisplayTarget(process.cwd(), result.targetRoot);
|
|
95
|
+
const stylesheetDisplay = toDisplayFilePath(result.targetRoot, result.targetStylesEntry);
|
|
54
96
|
const lines = [
|
|
55
|
-
|
|
97
|
+
'trine init completed.',
|
|
56
98
|
`Target: ${displayTarget}`,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
...result.copiedFiles.map((file) => `- ${file}`),
|
|
60
|
-
'',
|
|
61
|
-
'Created or updated:',
|
|
62
|
-
...result.updatedFiles.map((file) => `- ${file}`),
|
|
99
|
+
`Framework: ${result.framework}`,
|
|
100
|
+
`Resolved stylesheet entry: ${stylesheetDisplay}`,
|
|
63
101
|
];
|
|
102
|
+
if (selection.mode === 'auto-detected') {
|
|
103
|
+
lines.push(`Selected app: ${displayTarget}`);
|
|
104
|
+
}
|
|
105
|
+
lines.push('', 'Created:', ...renderFileGroup(result.createdFiles, '- nothing created'), '', 'Reused:', ...renderFileGroup(result.reusedFiles, '- nothing reused'), '', 'Updated:', ...renderFileGroup(result.updatedFiles, '- nothing updated'));
|
|
106
|
+
if (result.warnings.length > 0) {
|
|
107
|
+
lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
|
|
108
|
+
}
|
|
109
|
+
lines.push('', 'Manual next steps:', '- Review prerequisite warnings and install anything still missing before building the target app.', '- Run trine add button when you want to deliver the first Trine component into this project.', '- Build the target app and confirm @trine/ui resolves through the local consumer-owned source tree.');
|
|
110
|
+
console.log(lines.join('\n'));
|
|
111
|
+
}
|
|
112
|
+
function printAddSuccess(component, result, selection) {
|
|
113
|
+
const displayTarget = toDisplayTarget(process.cwd(), result.targetRoot);
|
|
114
|
+
const componentLabel = capitalize(component);
|
|
115
|
+
const isRepoDemoVerification = result.warnings.some((warning) => warning.includes('temporary @trine/ui/* bridge'));
|
|
116
|
+
const lines = [`trine add ${component} completed.`, `Target: ${displayTarget}`];
|
|
117
|
+
if (selection.mode === 'auto-detected') {
|
|
118
|
+
lines.push(`Selected app: ${displayTarget}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push('', 'Copied files:', ...renderFileGroup(result.copiedFiles, '- nothing copied'), '', 'Created or updated:', ...renderFileGroup(result.updatedFiles, '- nothing created or updated'));
|
|
64
121
|
if (result.warnings.length > 0) {
|
|
65
122
|
lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
|
|
66
123
|
}
|
|
67
|
-
lines.push('', 'Manual next steps:', `- Ensure the target repo has Tailwind CSS v4 and class-variance-authority installed before building the delivered ${componentLabel}.`, `- Build the target app and confirm ${componentLabel} imports resolve through the local @trine/ui alias.`, `- Review the copied ${component}.skin.ts and tokens.css if you want local consumer customization
|
|
124
|
+
lines.push('', 'Manual next steps:', `- Ensure the target repo has Tailwind CSS v4 and class-variance-authority installed before building the delivered ${componentLabel}.`, `- Build the target app and confirm ${componentLabel} imports resolve through the local @trine/ui alias.`, `- Review the copied ${component}.skin.ts and tokens.css if you want local consumer customization.`, '- For future setup in this repo, prefer running trine init first so project-level Trine baseline files are already in place.');
|
|
68
125
|
if (isRepoDemoVerification) {
|
|
69
126
|
lines.push('- Open /validation-shell and review the CLI delivery proof section for the temporary demo verification path.');
|
|
70
127
|
}
|
|
71
128
|
console.log(lines.join('\n'));
|
|
72
129
|
}
|
|
130
|
+
function renderFileGroup(files, emptyMessage) {
|
|
131
|
+
return files.length > 0 ? files.map((file) => `- ${file}`) : [emptyMessage];
|
|
132
|
+
}
|
|
73
133
|
function isSupportedComponent(value) {
|
|
74
134
|
return SUPPORTED_COMPONENTS.some((component) => component === value);
|
|
75
135
|
}
|
|
76
136
|
function capitalize(value) {
|
|
77
137
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
78
138
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
const matches = findAngularAppTargets(cwd);
|
|
84
|
-
if (matches.length === 1) {
|
|
85
|
-
return matches[0];
|
|
86
|
-
}
|
|
87
|
-
if (matches.length > 1) {
|
|
88
|
-
throw new Error([
|
|
89
|
-
'Multiple Angular app targets were found under the current directory. Re-run with --target <app-root>:',
|
|
90
|
-
...matches.map((match) => `- ${match}`),
|
|
91
|
-
].join('\n'));
|
|
92
|
-
}
|
|
93
|
-
return '.';
|
|
94
|
-
}
|
|
95
|
-
function findAngularAppTargets(root) {
|
|
96
|
-
const matches = new Set();
|
|
97
|
-
walkForAngularApps(root, root, matches);
|
|
98
|
-
return [...matches].sort((left, right) => left.localeCompare(right));
|
|
99
|
-
}
|
|
100
|
-
function walkForAngularApps(root, currentDir, matches) {
|
|
101
|
-
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
102
|
-
if (entry.isDirectory()) {
|
|
103
|
-
if (shouldSkipDirectory(entry.name)) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
walkForAngularApps(root, path.join(currentDir, entry.name), matches);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (!entry.isFile() || entry.name !== 'tsconfig.app.json') {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const candidateRoot = currentDir;
|
|
113
|
-
if (!looksLikeAngularAppRoot(candidateRoot)) {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
const relativeRoot = path.relative(root, candidateRoot) || '.';
|
|
117
|
-
matches.add(toPosixPath(relativeRoot));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function shouldSkipDirectory(name) {
|
|
121
|
-
return ['.angular', '.git', '.playwright-cli', 'dist', 'node_modules', 'output'].includes(name);
|
|
139
|
+
function toDisplayTarget(cwd, targetRoot) {
|
|
140
|
+
const relativeTarget = path.relative(cwd, targetRoot) || '.';
|
|
141
|
+
return relativeTarget.startsWith('..') ? targetRoot : relativeTarget;
|
|
122
142
|
}
|
|
123
|
-
function
|
|
124
|
-
return
|
|
143
|
+
function toDisplayFilePath(targetRoot, filePath) {
|
|
144
|
+
return path.relative(targetRoot, filePath) || '.';
|
|
125
145
|
}
|
|
126
146
|
try {
|
|
127
147
|
main(process.argv.slice(2));
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureSharedStyleBaseline, ensureStylesImport, ensureTsconfigAlias, ensureUiRootBarrel, isDemoTarget, readTargetDependencyWarnings, resolveSupportedProjectTarget, toTargetRelativePath, } from "./project.js";
|
|
3
|
+
export function initProject(options) {
|
|
4
|
+
const targetRoot = path.resolve(options.cwd, options.target);
|
|
5
|
+
const { framework, targetStylesEntry, targetTsconfig } = resolveSupportedProjectTarget('trine init', targetRoot);
|
|
6
|
+
const createdFiles = [];
|
|
7
|
+
const updatedFiles = [];
|
|
8
|
+
const reusedFiles = [];
|
|
9
|
+
const warnings = [];
|
|
10
|
+
const sharedStylesResult = ensureSharedStyleBaseline(targetRoot, 'Trine components');
|
|
11
|
+
createdFiles.push(...sharedStylesResult.createdFiles);
|
|
12
|
+
reusedFiles.push(...sharedStylesResult.reusedFiles);
|
|
13
|
+
warnings.push(...sharedStylesResult.warnings);
|
|
14
|
+
const uiRootResult = ensureUiRootBarrel(targetRoot);
|
|
15
|
+
if (uiRootResult.created) {
|
|
16
|
+
createdFiles.push(uiRootResult.file);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
reusedFiles.push(uiRootResult.file);
|
|
20
|
+
}
|
|
21
|
+
const tsconfigDisplayPath = toTargetRelativePath(targetRoot, targetTsconfig);
|
|
22
|
+
if (ensureTsconfigAlias(targetTsconfig, targetRoot, options.cwd)) {
|
|
23
|
+
updatedFiles.push(tsconfigDisplayPath);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
reusedFiles.push(tsconfigDisplayPath);
|
|
27
|
+
}
|
|
28
|
+
const stylesDisplayPath = toTargetRelativePath(targetRoot, targetStylesEntry);
|
|
29
|
+
const stylesResult = ensureStylesImport(targetStylesEntry);
|
|
30
|
+
if (stylesResult.updated) {
|
|
31
|
+
updatedFiles.push(stylesDisplayPath);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
reusedFiles.push(stylesDisplayPath);
|
|
35
|
+
}
|
|
36
|
+
if (stylesResult.authoringImportStillPresent) {
|
|
37
|
+
warnings.push(`${stylesDisplayPath} still imports @trine/ui/styles/trine.css for a broader authoring baseline outside the local Trine consumer setup.`);
|
|
38
|
+
}
|
|
39
|
+
if (isDemoTarget(targetRoot)) {
|
|
40
|
+
warnings.push('apps/demo keeps a temporary @trine/ui/* bridge for non-localized components so the internal demo app can still build while local Trine source is verified.');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
warnings.push(...readTargetDependencyWarnings(targetRoot, 'Trine components'));
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
targetRoot,
|
|
47
|
+
framework,
|
|
48
|
+
targetStylesEntry,
|
|
49
|
+
createdFiles,
|
|
50
|
+
updatedFiles,
|
|
51
|
+
reusedFiles,
|
|
52
|
+
warnings,
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
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';";
|
|
7
|
+
const CONVENTIONAL_STYLE_ENTRY_PATHS = ['src/styles.scss', 'src/styles.css', 'src/global.scss'];
|
|
8
|
+
export const TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
9
|
+
const LOCAL_REPO_ROOT = path.resolve(TEMPLATE_ROOT, '../../..');
|
|
10
|
+
export const LOCAL_REPO_DEMO_ROOT = path.resolve(TEMPLATE_ROOT, '../../../apps/demo');
|
|
11
|
+
export function inspectProjectTarget(targetRoot) {
|
|
12
|
+
const targetAppDir = path.join(targetRoot, 'src', 'app');
|
|
13
|
+
const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
|
|
14
|
+
const targetStylesEntry = resolveStylesEntry(targetRoot);
|
|
15
|
+
const missingPaths = [];
|
|
16
|
+
if (!existsSync(targetRoot)) {
|
|
17
|
+
missingPaths.push(targetRoot);
|
|
18
|
+
}
|
|
19
|
+
if (!existsSync(targetAppDir)) {
|
|
20
|
+
missingPaths.push(targetAppDir);
|
|
21
|
+
}
|
|
22
|
+
if (!targetStylesEntry) {
|
|
23
|
+
missingPaths.push(`${path.join(targetRoot, 'src', 'styles.scss')} or ${path.join(targetRoot, 'src', 'styles.css')} or ${path.join(targetRoot, 'src', 'global.scss')} or the first resolvable build styles entry in ${findNearestAngularJsonDisplayPath(targetRoot)}`);
|
|
24
|
+
}
|
|
25
|
+
if (!existsSync(targetTsconfig)) {
|
|
26
|
+
missingPaths.push(targetTsconfig);
|
|
27
|
+
}
|
|
28
|
+
const framework = missingPaths.length === 0 ? detectProjectFramework(targetRoot) : 'unsupported';
|
|
29
|
+
return {
|
|
30
|
+
targetRoot,
|
|
31
|
+
targetAppDir,
|
|
32
|
+
targetTsconfig,
|
|
33
|
+
targetStylesEntry,
|
|
34
|
+
framework,
|
|
35
|
+
missingPaths,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function resolveSupportedProjectTarget(commandLabel, targetRoot) {
|
|
39
|
+
const inspection = inspectProjectTarget(targetRoot);
|
|
40
|
+
if (inspection.framework === 'unsupported' || !inspection.targetStylesEntry) {
|
|
41
|
+
throw new Error([
|
|
42
|
+
`${commandLabel} requires a supported Angular or Ionic Angular app target with src/app, a global stylesheet entry, and tsconfig.app.json.`,
|
|
43
|
+
...inspection.missingPaths.map((file) => `- ${file}`),
|
|
44
|
+
].join('\n'));
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
targetRoot: inspection.targetRoot,
|
|
48
|
+
targetAppDir: inspection.targetAppDir,
|
|
49
|
+
targetTsconfig: inspection.targetTsconfig,
|
|
50
|
+
targetStylesEntry: inspection.targetStylesEntry,
|
|
51
|
+
framework: inspection.framework,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function findAngularAppTargets(root) {
|
|
55
|
+
const matches = new Set();
|
|
56
|
+
walkForAngularApps(root, root, matches);
|
|
57
|
+
return [...matches].sort((left, right) => left.localeCompare(right));
|
|
58
|
+
}
|
|
59
|
+
export function looksLikeAngularAppRoot(root) {
|
|
60
|
+
return inspectProjectTarget(root).framework !== 'unsupported';
|
|
61
|
+
}
|
|
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
|
+
export function ensureUiRootBarrel(targetRoot) {
|
|
90
|
+
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
|
+
};
|
|
96
|
+
}
|
|
97
|
+
mkdirSync(path.dirname(uiRootPath), { recursive: true });
|
|
98
|
+
writeFileSync(uiRootPath, '\n');
|
|
99
|
+
return {
|
|
100
|
+
created: true,
|
|
101
|
+
file: toTargetRelativePath(targetRoot, uiRootPath),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function ensureLinesFile(filePath, lines) {
|
|
105
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
106
|
+
const normalizedExisting = existing.trimEnd();
|
|
107
|
+
const currentLines = normalizedExisting === '' ? [] : normalizedExisting.split('\n');
|
|
108
|
+
let changed = false;
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (!currentLines.includes(line)) {
|
|
111
|
+
currentLines.push(line);
|
|
112
|
+
changed = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!existsSync(filePath)) {
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
if (changed) {
|
|
119
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
120
|
+
writeFileSync(filePath, `${currentLines.join('\n')}\n`);
|
|
121
|
+
}
|
|
122
|
+
return changed;
|
|
123
|
+
}
|
|
124
|
+
export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
125
|
+
const currentText = readFileSync(tsconfigPath, 'utf8');
|
|
126
|
+
const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
|
|
127
|
+
if (parsed.error) {
|
|
128
|
+
throw new Error(`Unable to parse ${path.relative(cwd, tsconfigPath)} as JSONC.`);
|
|
129
|
+
}
|
|
130
|
+
const config = (parsed.config ?? {});
|
|
131
|
+
config.compilerOptions ??= {};
|
|
132
|
+
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
|
+
}
|
|
147
|
+
config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
|
|
148
|
+
if (isDemoTarget(targetRoot)) {
|
|
149
|
+
config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
|
|
150
|
+
}
|
|
151
|
+
else if ('@trine/ui/*' in config.compilerOptions.paths) {
|
|
152
|
+
delete config.compilerOptions.paths['@trine/ui/*'];
|
|
153
|
+
}
|
|
154
|
+
writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
export function ensureStylesImport(stylesPath) {
|
|
158
|
+
const current = readFileSync(stylesPath, 'utf8');
|
|
159
|
+
if (current.includes(STYLE_IMPORT_LINE)) {
|
|
160
|
+
return {
|
|
161
|
+
updated: false,
|
|
162
|
+
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
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'));
|
|
180
|
+
return {
|
|
181
|
+
updated: true,
|
|
182
|
+
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function readTargetDependencyWarnings(targetRoot, componentLabel) {
|
|
186
|
+
const packageJsonPath = findNearestFileUpward(targetRoot, 'package.json');
|
|
187
|
+
if (!packageJsonPath) {
|
|
188
|
+
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.',
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
194
|
+
const deps = {
|
|
195
|
+
...(packageJson.dependencies ?? {}),
|
|
196
|
+
...(packageJson.devDependencies ?? {}),
|
|
197
|
+
};
|
|
198
|
+
const warnings = [];
|
|
199
|
+
if (!deps['class-variance-authority']) {
|
|
200
|
+
warnings.push(`class-variance-authority is missing from the target repo. Install it before building the delivered ${componentLabel}.`);
|
|
201
|
+
}
|
|
202
|
+
const tailwindRange = deps['tailwindcss'];
|
|
203
|
+
if (!tailwindRange) {
|
|
204
|
+
warnings.push(`tailwindcss is missing from the target repo. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
|
|
205
|
+
}
|
|
206
|
+
else if (!looksLikeTailwindV4(tailwindRange)) {
|
|
207
|
+
warnings.push(`The target repo declares tailwindcss@${tailwindRange}. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
|
|
208
|
+
}
|
|
209
|
+
return warnings;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
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}.`,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export function resolveStylesEntry(targetRoot) {
|
|
218
|
+
for (const relativePath of CONVENTIONAL_STYLE_ENTRY_PATHS) {
|
|
219
|
+
const absolutePath = path.join(targetRoot, relativePath);
|
|
220
|
+
if (existsSync(absolutePath)) {
|
|
221
|
+
return absolutePath;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const angularJsonPaths = findFilesUpward(targetRoot, 'angular.json');
|
|
225
|
+
for (const angularJsonPath of angularJsonPaths) {
|
|
226
|
+
const resolvedFromWorkspace = resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot);
|
|
227
|
+
if (resolvedFromWorkspace) {
|
|
228
|
+
return resolvedFromWorkspace;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
export function isDemoTarget(targetRoot) {
|
|
234
|
+
return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
|
|
235
|
+
path.resolve(targetRoot) === path.resolve(LOCAL_REPO_DEMO_ROOT));
|
|
236
|
+
}
|
|
237
|
+
export function toPosixPath(filePath) {
|
|
238
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
239
|
+
}
|
|
240
|
+
export function toConfigRelativePath(filePath) {
|
|
241
|
+
const posixPath = toPosixPath(filePath);
|
|
242
|
+
return posixPath.startsWith('.') ? posixPath : `./${posixPath}`;
|
|
243
|
+
}
|
|
244
|
+
export function toTargetRelativePath(targetRoot, filePath) {
|
|
245
|
+
return toPosixPath(path.relative(targetRoot, filePath));
|
|
246
|
+
}
|
|
247
|
+
function walkForAngularApps(root, currentDir, matches) {
|
|
248
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
if (shouldSkipDirectory(entry.name)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
walkForAngularApps(root, path.join(currentDir, entry.name), matches);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!entry.isFile() || entry.name !== 'tsconfig.app.json') {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const candidateRoot = currentDir;
|
|
260
|
+
if (!looksLikeAngularAppRoot(candidateRoot)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const relativeRoot = path.relative(root, candidateRoot) || '.';
|
|
264
|
+
matches.add(toPosixPath(relativeRoot));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function shouldSkipDirectory(name) {
|
|
268
|
+
return ['.angular', '.git', '.playwright-cli', 'dist', 'node_modules', 'output'].includes(name);
|
|
269
|
+
}
|
|
270
|
+
function detectProjectFramework(targetRoot) {
|
|
271
|
+
if (findNearestFileUpward(targetRoot, 'ionic.config.json')) {
|
|
272
|
+
return 'ionic-angular';
|
|
273
|
+
}
|
|
274
|
+
const packageJsonPath = findNearestFileUpward(targetRoot, 'package.json');
|
|
275
|
+
if (packageJsonPath && packageJsonUsesIonicAngular(packageJsonPath)) {
|
|
276
|
+
return 'ionic-angular';
|
|
277
|
+
}
|
|
278
|
+
return 'angular';
|
|
279
|
+
}
|
|
280
|
+
function packageJsonUsesIonicAngular(packageJsonPath) {
|
|
281
|
+
try {
|
|
282
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
283
|
+
return Boolean(packageJson.dependencies?.['@ionic/angular'] ??
|
|
284
|
+
packageJson.devDependencies?.['@ionic/angular']);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
|
|
291
|
+
try {
|
|
292
|
+
const angularJson = JSON.parse(readFileSync(angularJsonPath, 'utf8'));
|
|
293
|
+
const workspaceRoot = path.dirname(angularJsonPath);
|
|
294
|
+
const projects = Object.values(angularJson.projects ?? {});
|
|
295
|
+
const matchingProjects = projects.filter((project) => projectMatchesTargetRoot(project, workspaceRoot, targetRoot));
|
|
296
|
+
const candidateProjects = matchingProjects.length > 0
|
|
297
|
+
? matchingProjects
|
|
298
|
+
: path.resolve(targetRoot) === path.resolve(workspaceRoot)
|
|
299
|
+
? projects
|
|
300
|
+
: [];
|
|
301
|
+
for (const project of candidateProjects) {
|
|
302
|
+
const styles = project.architect?.build?.options?.styles ?? project.targets?.build?.options?.styles ?? [];
|
|
303
|
+
for (const style of styles) {
|
|
304
|
+
const input = typeof style === 'string' ? style : style.input;
|
|
305
|
+
if (!input) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const absolutePath = path.resolve(workspaceRoot, input);
|
|
309
|
+
if (existsSync(absolutePath)) {
|
|
310
|
+
return absolutePath;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
function projectMatchesTargetRoot(project, workspaceRoot, targetRoot) {
|
|
321
|
+
const resolvedTargetRoot = path.resolve(targetRoot);
|
|
322
|
+
const candidateRoots = new Set([path.resolve(workspaceRoot, project.root ?? '.')]);
|
|
323
|
+
if (project.sourceRoot) {
|
|
324
|
+
candidateRoots.add(path.resolve(workspaceRoot, path.dirname(project.sourceRoot)));
|
|
325
|
+
}
|
|
326
|
+
for (const candidateRoot of candidateRoots) {
|
|
327
|
+
if (path.resolve(candidateRoot) === resolvedTargetRoot) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function findNearestAngularJsonDisplayPath(targetRoot) {
|
|
334
|
+
const nearestAngularJsonPath = findNearestFileUpward(targetRoot, 'angular.json');
|
|
335
|
+
return nearestAngularJsonPath ? nearestAngularJsonPath : path.join(targetRoot, 'angular.json');
|
|
336
|
+
}
|
|
337
|
+
function findFilesUpward(startDir, fileName) {
|
|
338
|
+
const matches = [];
|
|
339
|
+
let currentDir = path.resolve(startDir);
|
|
340
|
+
let reachedRoot = false;
|
|
341
|
+
while (!reachedRoot) {
|
|
342
|
+
const candidatePath = path.join(currentDir, fileName);
|
|
343
|
+
if (existsSync(candidatePath)) {
|
|
344
|
+
matches.push(candidatePath);
|
|
345
|
+
}
|
|
346
|
+
const parentDir = path.dirname(currentDir);
|
|
347
|
+
if (parentDir === currentDir) {
|
|
348
|
+
reachedRoot = true;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
currentDir = parentDir;
|
|
352
|
+
}
|
|
353
|
+
return matches;
|
|
354
|
+
}
|
|
355
|
+
function findNearestFileUpward(startDir, fileName) {
|
|
356
|
+
return findFilesUpward(startDir, fileName)[0];
|
|
357
|
+
}
|
|
358
|
+
function looksLikeTailwindV4(range) {
|
|
359
|
+
const versionTokenPattern = /(?:^|[<>=~^|\s:])v?(\d+)(?:(?:\.\d+){0,2})/g;
|
|
360
|
+
return [...range.matchAll(versionTokenPattern)].some((match) => Number(match[1]) === 4);
|
|
361
|
+
}
|