@trineui/cli 0.1.0 → 0.1.2
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 +3 -1
- package/dist/add-component.js +63 -16
- package/dist/index.js +53 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,8 @@ Notes:
|
|
|
16
16
|
|
|
17
17
|
- `button` is the only supported component in this public-style baseline
|
|
18
18
|
- omitting `--target` uses the current directory when it already matches the supported Angular app shape
|
|
19
|
-
- when the current directory is not a supported Angular app target,
|
|
19
|
+
- 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
|
+
- when multiple Angular app targets are found, the CLI stops and asks for `--target <app-root>`
|
|
20
21
|
- 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`
|
|
21
22
|
- the canonical public package name is `@trineui/cli`
|
|
22
23
|
- the CLI command exposed through the package bin is still `trine`
|
|
@@ -30,6 +31,7 @@ Notes:
|
|
|
30
31
|
- for `apps/demo`, `@trine/ui` resolves locally for delivered components while `@trine/ui/*` temporarily bridges non-localized components back to the authoring source
|
|
31
32
|
- the delivered shared styling baseline is `tokens.css` + `trine-consumer.css`
|
|
32
33
|
- the current proven target dependency baseline is Angular 21, Tailwind CSS v4, and `class-variance-authority`
|
|
34
|
+
- the current proven target shape accepts a global stylesheet entry such as `src/styles.scss`, `src/styles.css`, or `src/global.scss` when it is resolved from `angular.json`
|
|
33
35
|
- use a Node LTS line supported by Angular 21 in the target repo; odd-numbered Node releases can build with warnings
|
|
34
36
|
- when `package.json` is present in the target root, the CLI warns if Tailwind CSS v4 or `class-variance-authority` are missing
|
|
35
37
|
|
package/dist/add-component.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import ts from 'typescript';
|
|
5
5
|
const STYLE_SOURCE_FILES = ['styles/tokens.css', 'styles/trine-consumer.css'];
|
|
6
6
|
const TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
7
|
+
const LOCAL_REPO_DEMO_ROOT = path.resolve(TEMPLATE_ROOT, '../../../apps/demo');
|
|
7
8
|
const STYLE_IMPORT_LINE = "@import './styles/trine-consumer.css';";
|
|
8
9
|
const DEMO_BRIDGE_COMMENT = '// Temporary demo verification bridge: delivered local components resolve locally; other components still re-export from the authoring source.';
|
|
9
10
|
const DEMO_LOCAL_COMPONENTS = [
|
|
@@ -60,10 +61,7 @@ const DEMO_PROXY_EXPORT_LINES = [
|
|
|
60
61
|
];
|
|
61
62
|
export function addComponent(manifest, options) {
|
|
62
63
|
const targetRoot = path.resolve(options.cwd, options.target);
|
|
63
|
-
const
|
|
64
|
-
const targetStylesEntry = path.join(targetRoot, 'src', 'styles.scss');
|
|
65
|
-
const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
|
|
66
|
-
assertTargetShape(manifest.componentName, targetRoot, targetAppDir, targetStylesEntry, targetTsconfig);
|
|
64
|
+
const { targetStylesEntry, targetTsconfig } = resolveTargetShape(manifest.componentName, targetRoot);
|
|
67
65
|
const componentDestDir = path.join(targetRoot, 'src', 'app', 'components', 'ui', manifest.componentName);
|
|
68
66
|
const stylesDestDir = path.join(targetRoot, 'src', 'styles');
|
|
69
67
|
const componentCopyTargets = manifest.sourceFiles.map((source) => ({
|
|
@@ -94,7 +92,7 @@ export function addComponent(manifest, options) {
|
|
|
94
92
|
updatedFiles.push(toTargetRelativePath(targetRoot, componentBarrelPath));
|
|
95
93
|
}
|
|
96
94
|
const uiRootBarrelPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
|
|
97
|
-
const uiRootUpdated = isDemoTarget(targetRoot
|
|
95
|
+
const uiRootUpdated = isDemoTarget(targetRoot)
|
|
98
96
|
? rewriteDemoUiRootBarrel(uiRootBarrelPath)
|
|
99
97
|
: ensureLinesFile(uiRootBarrelPath, [manifest.uiExportLine]);
|
|
100
98
|
if (uiRootUpdated) {
|
|
@@ -110,7 +108,7 @@ export function addComponent(manifest, options) {
|
|
|
110
108
|
if (stylesResult.authoringImportStillPresent) {
|
|
111
109
|
warnings.push(`${toTargetRelativePath(targetRoot, targetStylesEntry)} still imports @trine/ui/styles/trine.css for the broader demo authoring baseline.`);
|
|
112
110
|
}
|
|
113
|
-
if (isDemoTarget(targetRoot
|
|
111
|
+
if (isDemoTarget(targetRoot)) {
|
|
114
112
|
warnings.push('apps/demo keeps non-localized components on a temporary @trine/ui/* bridge back to packages/ui so the full demo app can still build while delivered components resolve locally.');
|
|
115
113
|
}
|
|
116
114
|
else {
|
|
@@ -145,7 +143,10 @@ function ensureSharedStyleBaseline(targetRoot, stylesDestDir, componentLabel) {
|
|
|
145
143
|
warnings,
|
|
146
144
|
};
|
|
147
145
|
}
|
|
148
|
-
function
|
|
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);
|
|
149
150
|
const missing = [];
|
|
150
151
|
if (!existsSync(targetRoot)) {
|
|
151
152
|
missing.push(targetRoot);
|
|
@@ -153,18 +154,25 @@ function assertTargetShape(componentName, targetRoot, targetAppDir, targetStyles
|
|
|
153
154
|
if (!existsSync(targetAppDir)) {
|
|
154
155
|
missing.push(targetAppDir);
|
|
155
156
|
}
|
|
156
|
-
if (!
|
|
157
|
-
missing.push(
|
|
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')}`);
|
|
158
159
|
}
|
|
159
160
|
if (!existsSync(targetTsconfig)) {
|
|
160
161
|
missing.push(targetTsconfig);
|
|
161
162
|
}
|
|
162
163
|
if (missing.length > 0) {
|
|
163
164
|
throw new Error([
|
|
164
|
-
`trine add ${componentName} requires an Angular app target with src/app,
|
|
165
|
+
`trine add ${componentName} requires an Angular app target with src/app, a global stylesheet entry, and tsconfig.app.json.`,
|
|
165
166
|
...missing.map((file) => `- ${file}`),
|
|
166
167
|
].join('\n'));
|
|
167
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
|
+
};
|
|
168
176
|
}
|
|
169
177
|
function ensureLinesFile(filePath, lines) {
|
|
170
178
|
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
@@ -213,14 +221,14 @@ function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
|
213
221
|
const config = (parsed.config ?? {});
|
|
214
222
|
config.compilerOptions ??= {};
|
|
215
223
|
config.compilerOptions.paths ??= {};
|
|
216
|
-
const aliasTarget = isDemoTarget(targetRoot
|
|
217
|
-
? toPosixPath(path.relative(cwd, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
|
|
224
|
+
const aliasTarget = isDemoTarget(targetRoot)
|
|
225
|
+
? toPosixPath(path.relative(process.cwd(), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
|
|
218
226
|
: toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
|
|
219
227
|
const wildcardTarget = 'packages/ui/*';
|
|
220
228
|
const currentAlias = config.compilerOptions.paths['@trine/ui'];
|
|
221
229
|
const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
|
|
222
230
|
const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
|
|
223
|
-
const wildcardIsCurrent = !isDemoTarget(targetRoot
|
|
231
|
+
const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
|
|
224
232
|
(Array.isArray(currentWildcardAlias) &&
|
|
225
233
|
currentWildcardAlias.length === 1 &&
|
|
226
234
|
currentWildcardAlias[0] === wildcardTarget);
|
|
@@ -228,7 +236,7 @@ function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
|
228
236
|
return false;
|
|
229
237
|
}
|
|
230
238
|
config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
|
|
231
|
-
if (isDemoTarget(targetRoot
|
|
239
|
+
if (isDemoTarget(targetRoot)) {
|
|
232
240
|
config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
|
|
233
241
|
}
|
|
234
242
|
else if ('@trine/ui/*' in config.compilerOptions.paths) {
|
|
@@ -265,8 +273,9 @@ function ensureStylesImport(stylesPath) {
|
|
|
265
273
|
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
266
274
|
};
|
|
267
275
|
}
|
|
268
|
-
function isDemoTarget(targetRoot
|
|
269
|
-
return
|
|
276
|
+
function isDemoTarget(targetRoot) {
|
|
277
|
+
return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
|
|
278
|
+
path.resolve(targetRoot) === path.resolve(LOCAL_REPO_DEMO_ROOT));
|
|
270
279
|
}
|
|
271
280
|
function toPosixPath(filePath) {
|
|
272
281
|
return filePath.split(path.sep).join(path.posix.sep);
|
|
@@ -313,3 +322,41 @@ function readTargetDependencyWarnings(targetRoot, componentLabel) {
|
|
|
313
322
|
function looksLikeTailwindV4(range) {
|
|
314
323
|
return /(^|[^\d])4(\D|$)/.test(range);
|
|
315
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,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { looksLikeAngularAppRoot } from "./add-component.js";
|
|
4
5
|
import { addButton } from "./add-button.js";
|
|
5
6
|
const HELP_TEXT = `Usage:
|
|
6
7
|
npx @trineui/cli@latest add button [--target <app-root>]
|
|
@@ -8,12 +9,13 @@ const HELP_TEXT = `Usage:
|
|
|
8
9
|
|
|
9
10
|
Defaults:
|
|
10
11
|
- current directory when it matches the supported Angular app target shape
|
|
11
|
-
- otherwise
|
|
12
|
+
- otherwise auto-detect a single Angular app target under the current directory
|
|
13
|
+
- when multiple Angular app targets are found, re-run with --target <app-root>
|
|
12
14
|
|
|
13
15
|
Notes:
|
|
14
16
|
- v0 supports Button only
|
|
15
17
|
- external targets can run trine add button from the app root or pass --target /absolute/path/to/angular-app
|
|
16
|
-
- the current proven target model is Angular 21 + src/app + src/styles.scss
|
|
18
|
+
- 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 from angular.json
|
|
17
19
|
- use a Node LTS line supported by Angular 21 in the target repo
|
|
18
20
|
- the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
|
|
19
21
|
- consumer-owned component files fail clearly if they already exist
|
|
@@ -29,7 +31,7 @@ function main(argv) {
|
|
|
29
31
|
if (!isSupportedComponent(component)) {
|
|
30
32
|
throw new Error(component ? `Unsupported component: ${component}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
31
33
|
}
|
|
32
|
-
const target = readTarget(rest) ??
|
|
34
|
+
const target = readTarget(rest) ?? autoDetectTarget(process.cwd());
|
|
33
35
|
const result = addButton({
|
|
34
36
|
target,
|
|
35
37
|
cwd: process.cwd(),
|
|
@@ -48,6 +50,7 @@ function printSuccess(component, result) {
|
|
|
48
50
|
const relativeTarget = path.relative(process.cwd(), result.targetRoot) || '.';
|
|
49
51
|
const displayTarget = relativeTarget.startsWith('..') ? result.targetRoot : relativeTarget;
|
|
50
52
|
const componentLabel = capitalize(component);
|
|
53
|
+
const isRepoDemoVerification = result.warnings.some((warning) => warning.includes('temporary @trine/ui/* bridge'));
|
|
51
54
|
const lines = [
|
|
52
55
|
`trine add ${component} completed.`,
|
|
53
56
|
`Target: ${displayTarget}`,
|
|
@@ -62,7 +65,7 @@ function printSuccess(component, result) {
|
|
|
62
65
|
lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
|
|
63
66
|
}
|
|
64
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.`);
|
|
65
|
-
if (
|
|
68
|
+
if (isRepoDemoVerification) {
|
|
66
69
|
lines.push('- Open /validation-shell and review the CLI delivery proof section for the temporary demo verification path.');
|
|
67
70
|
}
|
|
68
71
|
console.log(lines.join('\n'));
|
|
@@ -73,11 +76,52 @@ function isSupportedComponent(value) {
|
|
|
73
76
|
function capitalize(value) {
|
|
74
77
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
75
78
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
79
|
+
function autoDetectTarget(cwd) {
|
|
80
|
+
if (looksLikeAngularAppRoot(cwd)) {
|
|
81
|
+
return '.';
|
|
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);
|
|
78
122
|
}
|
|
79
|
-
function
|
|
80
|
-
return
|
|
123
|
+
function toPosixPath(filePath) {
|
|
124
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
81
125
|
}
|
|
82
126
|
try {
|
|
83
127
|
main(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trineui/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Copy-paste ownership CLI for Trine UI components.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"trine": "
|
|
8
|
+
"trine": "bin/trine.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc -p tsconfig.build.json",
|