@trineui/cli 0.1.0-beta.1 → 0.1.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.
Files changed (39) hide show
  1. package/README.md +40 -0
  2. package/bin/trine.js +24 -0
  3. package/dist/add-button.js +15 -0
  4. package/dist/add-component.js +315 -0
  5. package/dist/index.js +73 -646
  6. package/package.json +23 -30
  7. package/templates/button/button.html +11 -0
  8. package/templates/button/button.skin.ts +128 -0
  9. package/templates/button/button.ts +39 -0
  10. package/templates/styles/tokens.css +102 -0
  11. package/templates/styles/trine-consumer.css +58 -0
  12. package/CHANGELOG.md +0 -30
  13. package/src/commands/add.ts +0 -101
  14. package/src/commands/diff.test.ts +0 -55
  15. package/src/commands/diff.ts +0 -104
  16. package/src/commands/eject.ts +0 -95
  17. package/src/commands/init.ts +0 -92
  18. package/src/commands/sync-interactive.ts +0 -108
  19. package/src/commands/sync.test.ts +0 -35
  20. package/src/commands/sync.ts +0 -113
  21. package/src/index.ts +0 -18
  22. package/src/types/manifest.ts +0 -14
  23. package/src/utils/__tests__/hash.test.ts +0 -35
  24. package/src/utils/__tests__/template.test.ts +0 -47
  25. package/src/utils/eject-merger.ts +0 -149
  26. package/src/utils/hash.ts +0 -43
  27. package/src/utils/manifest.ts +0 -43
  28. package/src/utils/template.ts +0 -26
  29. package/templates/button.blueprint.ts.hbs +0 -41
  30. package/templates/button.skin.ts.hbs +0 -35
  31. package/templates/checkbox.blueprint.ts.hbs +0 -57
  32. package/templates/checkbox.skin.ts.hbs +0 -44
  33. package/templates/dialog.blueprint.ts.hbs +0 -61
  34. package/templates/dialog.skin.ts.hbs +0 -27
  35. package/templates/input.blueprint.ts.hbs +0 -83
  36. package/templates/input.skin.ts.hbs +0 -29
  37. package/templates/select.blueprint.ts.hbs +0 -86
  38. package/templates/select.skin.ts.hbs +0 -53
  39. package/tsconfig.json +0 -10
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @trineui/cli
2
+
3
+ Canonical public package:
4
+
5
+ ```bash
6
+ npx @trineui/cli@latest add button
7
+ ```
8
+
9
+ Current v0 command:
10
+
11
+ ```bash
12
+ trine add button --target <app-root>
13
+ ```
14
+
15
+ Notes:
16
+
17
+ - `button` is the only supported component in this public-style baseline
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, omitting `--target` falls back to `apps/demo` for local Trine repo verification only
20
+ - 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
+ - the canonical public package name is `@trineui/cli`
22
+ - the CLI command exposed through the package bin is still `trine`
23
+ - `apps/consumer-fixture` is the first separate-target proof and does not use the demo-only `@trine/ui/*` bridge
24
+ - `/tmp/trine-button-publish-proof` is the latest truly external packaged-proof repo outside the monorepo
25
+ - packaged/public-style proof uses a packed local tarball to simulate `npx @trineui/cli@latest add button`
26
+ - the packaged CLI ships compiled runtime files plus Button templates so it can execute from `node_modules` in a real `npx`-style flow
27
+ - consumer-owned component destination files cause a clear failure
28
+ - existing shared baseline files (`tokens.css` and `trine-consumer.css`) are preserved so a second component can be added into the same target repo
29
+ - the command copies consumer-owned source instead of wiring runtime back to `packages/ui`
30
+ - for `apps/demo`, `@trine/ui` resolves locally for delivered components while `@trine/ui/*` temporarily bridges non-localized components back to the authoring source
31
+ - the delivered shared styling baseline is `tokens.css` + `trine-consumer.css`
32
+ - the current proven target dependency baseline is Angular 21, Tailwind CSS v4, and `class-variance-authority`
33
+ - use a Node LTS line supported by Angular 21 in the target repo; odd-numbered Node releases can build with warnings
34
+ - when `package.json` is present in the target root, the CLI warns if Tailwind CSS v4 or `class-variance-authority` are missing
35
+
36
+ Local package proof equivalent:
37
+
38
+ ```bash
39
+ npx --yes --package /absolute/path/to/trineui-cli-0.1.0.tgz trine add button
40
+ ```
package/bin/trine.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
9
+ const distEntryPath = path.resolve(currentDir, '../dist/index.js');
10
+ const sourceEntryPath = path.resolve(currentDir, '../src/index.ts');
11
+ const entryPath = existsSync(distEntryPath) ? distEntryPath : sourceEntryPath;
12
+ const nodeArgs = entryPath.endsWith('.ts')
13
+ ? ['--experimental-strip-types', entryPath, ...process.argv.slice(2)]
14
+ : [entryPath, ...process.argv.slice(2)];
15
+
16
+ const result = spawnSync(process.execPath, nodeArgs, {
17
+ stdio: 'inherit',
18
+ });
19
+
20
+ if (result.error) {
21
+ throw result.error;
22
+ }
23
+
24
+ process.exit(result.status ?? 1);
@@ -0,0 +1,15 @@
1
+ import { addComponent, } from "./add-component.js";
2
+ const BUTTON_SOURCE_FILES = [
3
+ 'button/button.ts',
4
+ 'button/button.html',
5
+ 'button/button.skin.ts',
6
+ ];
7
+ export function addButton(options) {
8
+ return addComponent({
9
+ componentName: 'button',
10
+ componentLabel: 'Button',
11
+ sourceFiles: BUTTON_SOURCE_FILES,
12
+ barrelLine: "export { ButtonComponent, ButtonComponent as TrineButton } from './button';",
13
+ uiExportLine: "export * from './button';",
14
+ }, options);
15
+ }
@@ -0,0 +1,315 @@
1
+ import { copyFileSync, existsSync, mkdirSync, 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 TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
7
+ const STYLE_IMPORT_LINE = "@import './styles/trine-consumer.css';";
8
+ const DEMO_BRIDGE_COMMENT = '// Temporary demo verification bridge: delivered local components resolve locally; other components still re-export from the authoring source.';
9
+ const DEMO_LOCAL_COMPONENTS = [
10
+ {
11
+ key: 'button',
12
+ exportLine: "export * from './button';",
13
+ },
14
+ ];
15
+ const DEMO_PROXY_EXPORT_LINES = [
16
+ {
17
+ key: 'button-group',
18
+ line: "export * from '@trine/ui/src/components/ui/button-group';",
19
+ },
20
+ {
21
+ key: 'checkbox',
22
+ line: "export * from '@trine/ui/src/components/ui/checkbox';",
23
+ },
24
+ {
25
+ key: 'dialog',
26
+ line: "export * from '@trine/ui/src/components/ui/dialog';",
27
+ },
28
+ {
29
+ key: 'input',
30
+ line: "export * from '@trine/ui/src/components/ui/input/input';",
31
+ },
32
+ {
33
+ key: 'popover',
34
+ line: "export * from '@trine/ui/src/components/ui/popover';",
35
+ },
36
+ {
37
+ key: 'radio',
38
+ line: "export * from '@trine/ui/src/components/ui/radio';",
39
+ },
40
+ {
41
+ key: 'select',
42
+ line: "export * from '@trine/ui/src/components/ui/select';",
43
+ },
44
+ {
45
+ key: 'sprint-zero-placeholder',
46
+ line: "export * from '@trine/ui/src/components/ui/sprint-zero-placeholder/sprint-zero-placeholder';",
47
+ },
48
+ {
49
+ key: 'switch',
50
+ line: "export * from '@trine/ui/src/components/ui/switch';",
51
+ },
52
+ {
53
+ key: 'textarea',
54
+ line: "export * from '@trine/ui/src/components/ui/textarea';",
55
+ },
56
+ {
57
+ key: 'tabs',
58
+ line: "export * from '@trine/ui/src/components/ui/tabs';",
59
+ },
60
+ ];
61
+ export function addComponent(manifest, options) {
62
+ const targetRoot = path.resolve(options.cwd, options.target);
63
+ const targetAppDir = path.join(targetRoot, 'src', 'app');
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);
67
+ const componentDestDir = path.join(targetRoot, 'src', 'app', 'components', 'ui', manifest.componentName);
68
+ const stylesDestDir = path.join(targetRoot, 'src', 'styles');
69
+ const componentCopyTargets = manifest.sourceFiles.map((source) => ({
70
+ source: path.join(TEMPLATE_ROOT, source),
71
+ destination: path.join(componentDestDir, path.basename(source)),
72
+ }));
73
+ const conflicts = componentCopyTargets
74
+ .filter(({ destination }) => existsSync(destination))
75
+ .map(({ destination }) => toTargetRelativePath(targetRoot, destination));
76
+ if (conflicts.length > 0) {
77
+ throw new Error([
78
+ `trine add ${manifest.componentName} aborted because consumer-owned destination files already exist:`,
79
+ ...conflicts.map((file) => `- ${file}`),
80
+ ].join('\n'));
81
+ }
82
+ mkdirSync(componentDestDir, { recursive: true });
83
+ mkdirSync(stylesDestDir, { recursive: true });
84
+ for (const { source, destination } of componentCopyTargets) {
85
+ copyFileSync(source, destination);
86
+ }
87
+ const componentCopiedFiles = componentCopyTargets.map(({ destination }) => toTargetRelativePath(targetRoot, destination));
88
+ const sharedStylesResult = ensureSharedStyleBaseline(targetRoot, stylesDestDir, manifest.componentLabel);
89
+ const copiedFiles = [...componentCopiedFiles, ...sharedStylesResult.copiedFiles];
90
+ const updatedFiles = [];
91
+ const warnings = [...sharedStylesResult.warnings];
92
+ const componentBarrelPath = path.join(componentDestDir, 'index.ts');
93
+ if (ensureLinesFile(componentBarrelPath, [manifest.barrelLine])) {
94
+ updatedFiles.push(toTargetRelativePath(targetRoot, componentBarrelPath));
95
+ }
96
+ const uiRootBarrelPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
97
+ const uiRootUpdated = isDemoTarget(targetRoot, options.cwd)
98
+ ? rewriteDemoUiRootBarrel(uiRootBarrelPath)
99
+ : ensureLinesFile(uiRootBarrelPath, [manifest.uiExportLine]);
100
+ if (uiRootUpdated) {
101
+ updatedFiles.push(toTargetRelativePath(targetRoot, uiRootBarrelPath));
102
+ }
103
+ if (ensureTsconfigAlias(targetTsconfig, targetRoot, options.cwd)) {
104
+ updatedFiles.push(toTargetRelativePath(targetRoot, targetTsconfig));
105
+ }
106
+ const stylesResult = ensureStylesImport(targetStylesEntry);
107
+ if (stylesResult.updated) {
108
+ updatedFiles.push(toTargetRelativePath(targetRoot, targetStylesEntry));
109
+ }
110
+ if (stylesResult.authoringImportStillPresent) {
111
+ warnings.push(`${toTargetRelativePath(targetRoot, targetStylesEntry)} still imports @trine/ui/styles/trine.css for the broader demo authoring baseline.`);
112
+ }
113
+ if (isDemoTarget(targetRoot, options.cwd)) {
114
+ 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
+ }
116
+ else {
117
+ warnings.push(...readTargetDependencyWarnings(targetRoot, manifest.componentLabel));
118
+ }
119
+ return {
120
+ componentName: manifest.componentName,
121
+ copiedFiles,
122
+ updatedFiles,
123
+ warnings,
124
+ targetRoot,
125
+ };
126
+ }
127
+ function ensureSharedStyleBaseline(targetRoot, stylesDestDir, componentLabel) {
128
+ const copiedFiles = [];
129
+ const warnings = [];
130
+ for (const sourceFile of STYLE_SOURCE_FILES) {
131
+ const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
132
+ const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
133
+ const relativeDestination = toTargetRelativePath(targetRoot, destinationPath);
134
+ if (!existsSync(destinationPath)) {
135
+ copyFileSync(templatePath, destinationPath);
136
+ copiedFiles.push(relativeDestination);
137
+ continue;
138
+ }
139
+ if (readFileSync(destinationPath, 'utf8') !== readFileSync(templatePath, 'utf8')) {
140
+ warnings.push(`${relativeDestination} already exists and was preserved. Review it manually if the delivered ${componentLabel} expects newer shared styling baseline content.`);
141
+ }
142
+ }
143
+ return {
144
+ copiedFiles,
145
+ warnings,
146
+ };
147
+ }
148
+ function assertTargetShape(componentName, targetRoot, targetAppDir, targetStylesEntry, targetTsconfig) {
149
+ const missing = [];
150
+ if (!existsSync(targetRoot)) {
151
+ missing.push(targetRoot);
152
+ }
153
+ if (!existsSync(targetAppDir)) {
154
+ missing.push(targetAppDir);
155
+ }
156
+ if (!existsSync(targetStylesEntry)) {
157
+ missing.push(targetStylesEntry);
158
+ }
159
+ if (!existsSync(targetTsconfig)) {
160
+ missing.push(targetTsconfig);
161
+ }
162
+ if (missing.length > 0) {
163
+ throw new Error([
164
+ `trine add ${componentName} requires an Angular app target with src/app, src/styles.scss, and tsconfig.app.json.`,
165
+ ...missing.map((file) => `- ${file}`),
166
+ ].join('\n'));
167
+ }
168
+ }
169
+ function ensureLinesFile(filePath, lines) {
170
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
171
+ const normalizedExisting = existing.trimEnd();
172
+ const currentLines = normalizedExisting === '' ? [] : normalizedExisting.split('\n');
173
+ let changed = false;
174
+ for (const line of lines) {
175
+ if (!currentLines.includes(line)) {
176
+ currentLines.push(line);
177
+ changed = true;
178
+ }
179
+ }
180
+ if (!existsSync(filePath)) {
181
+ changed = true;
182
+ }
183
+ if (changed) {
184
+ mkdirSync(path.dirname(filePath), { recursive: true });
185
+ writeFileSync(filePath, `${currentLines.join('\n')}\n`);
186
+ }
187
+ return changed;
188
+ }
189
+ function rewriteDemoUiRootBarrel(filePath) {
190
+ const uiRootDir = path.dirname(filePath);
191
+ const localComponentLines = DEMO_LOCAL_COMPONENTS.filter(({ key }) => existsSync(path.join(uiRootDir, key, 'index.ts')));
192
+ const localComponentKeys = new Set(localComponentLines.map(({ key }) => key));
193
+ const nextLines = [
194
+ DEMO_BRIDGE_COMMENT,
195
+ ...localComponentLines.map(({ exportLine }) => exportLine),
196
+ ...DEMO_PROXY_EXPORT_LINES.filter(({ key }) => !localComponentKeys.has(key)).map(({ line }) => line),
197
+ ];
198
+ const nextContent = `${nextLines.join('\n')}\n`;
199
+ const currentContent = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
200
+ if (currentContent === nextContent) {
201
+ return false;
202
+ }
203
+ mkdirSync(path.dirname(filePath), { recursive: true });
204
+ writeFileSync(filePath, nextContent);
205
+ return true;
206
+ }
207
+ function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
208
+ const currentText = readFileSync(tsconfigPath, 'utf8');
209
+ const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
210
+ if (parsed.error) {
211
+ throw new Error(`Unable to parse ${path.relative(cwd, tsconfigPath)} as JSONC.`);
212
+ }
213
+ const config = (parsed.config ?? {});
214
+ config.compilerOptions ??= {};
215
+ config.compilerOptions.paths ??= {};
216
+ const aliasTarget = isDemoTarget(targetRoot, cwd)
217
+ ? toPosixPath(path.relative(cwd, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
218
+ : toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
219
+ const wildcardTarget = 'packages/ui/*';
220
+ const currentAlias = config.compilerOptions.paths['@trine/ui'];
221
+ const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
222
+ const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
223
+ const wildcardIsCurrent = !isDemoTarget(targetRoot, cwd) ||
224
+ (Array.isArray(currentWildcardAlias) &&
225
+ currentWildcardAlias.length === 1 &&
226
+ currentWildcardAlias[0] === wildcardTarget);
227
+ if (aliasIsCurrent && wildcardIsCurrent) {
228
+ return false;
229
+ }
230
+ config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
231
+ if (isDemoTarget(targetRoot, cwd)) {
232
+ config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
233
+ }
234
+ else if ('@trine/ui/*' in config.compilerOptions.paths) {
235
+ delete config.compilerOptions.paths['@trine/ui/*'];
236
+ }
237
+ writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
238
+ return true;
239
+ }
240
+ function ensureStylesImport(stylesPath) {
241
+ const current = readFileSync(stylesPath, 'utf8');
242
+ if (current.includes(STYLE_IMPORT_LINE)) {
243
+ return {
244
+ updated: false,
245
+ authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
246
+ };
247
+ }
248
+ const lines = current.split('\n');
249
+ let insertAt = -1;
250
+ for (let index = 0; index < lines.length; index += 1) {
251
+ const trimmed = lines[index].trim();
252
+ if (trimmed.startsWith('@use') || trimmed.startsWith('@import')) {
253
+ insertAt = index;
254
+ }
255
+ }
256
+ if (insertAt === -1) {
257
+ lines.unshift(STYLE_IMPORT_LINE, '');
258
+ }
259
+ else {
260
+ lines.splice(insertAt + 1, 0, STYLE_IMPORT_LINE);
261
+ }
262
+ writeFileSync(stylesPath, lines.join('\n'));
263
+ return {
264
+ updated: true,
265
+ authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
266
+ };
267
+ }
268
+ function isDemoTarget(targetRoot, cwd) {
269
+ return path.resolve(targetRoot) === path.resolve(cwd, 'apps/demo');
270
+ }
271
+ function toPosixPath(filePath) {
272
+ return filePath.split(path.sep).join(path.posix.sep);
273
+ }
274
+ function toConfigRelativePath(filePath) {
275
+ const posixPath = toPosixPath(filePath);
276
+ return posixPath.startsWith('.') ? posixPath : `./${posixPath}`;
277
+ }
278
+ function toTargetRelativePath(targetRoot, filePath) {
279
+ return toPosixPath(path.relative(targetRoot, filePath));
280
+ }
281
+ function readTargetDependencyWarnings(targetRoot, componentLabel) {
282
+ const packageJsonPath = path.join(targetRoot, 'package.json');
283
+ if (!existsSync(packageJsonPath)) {
284
+ return [
285
+ 'No package.json was found in the target root, so Tailwind CSS v4 and class-variance-authority prerequisites could not be checked automatically.',
286
+ ];
287
+ }
288
+ try {
289
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
290
+ const deps = {
291
+ ...(packageJson.dependencies ?? {}),
292
+ ...(packageJson.devDependencies ?? {}),
293
+ };
294
+ const warnings = [];
295
+ if (!deps['class-variance-authority']) {
296
+ warnings.push(`class-variance-authority is missing from the target repo. Install it before building the delivered ${componentLabel}.`);
297
+ }
298
+ const tailwindRange = deps['tailwindcss'];
299
+ if (!tailwindRange) {
300
+ warnings.push(`tailwindcss is missing from the target repo. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
301
+ }
302
+ else if (!looksLikeTailwindV4(tailwindRange)) {
303
+ warnings.push(`The target repo declares tailwindcss@${tailwindRange}. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
304
+ }
305
+ return warnings;
306
+ }
307
+ catch {
308
+ return [
309
+ `package.json could not be parsed for dependency checks. Verify Tailwind CSS v4 and class-variance-authority manually before building the delivered ${componentLabel}.`,
310
+ ];
311
+ }
312
+ }
313
+ function looksLikeTailwindV4(range) {
314
+ return /(^|[^\d])4(\D|$)/.test(range);
315
+ }