@trineui/cli 0.1.1 → 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 +15 -2
- package/dist/add-component.js +4 -186
- package/dist/index.js +93 -75
- package/dist/init.js +54 -0
- package/dist/project.js +361 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,36 +6,49 @@ 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`
|
|
31
42
|
- for `apps/demo`, `@trine/ui` resolves locally for delivered components while `@trine/ui/*` temporarily bridges non-localized components back to the authoring source
|
|
32
43
|
- the delivered shared styling baseline is `tokens.css` + `trine-consumer.css`
|
|
33
44
|
- the current proven target dependency baseline is Angular 21, Tailwind CSS v4, and `class-variance-authority`
|
|
45
|
+
- 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`
|
|
34
46
|
- use a Node LTS line supported by Angular 21 in the target repo; odd-numbered Node releases can build with warnings
|
|
35
47
|
- when `package.json` is present in the target root, the CLI warns if Tailwind CSS v4 or `class-variance-authority` are missing
|
|
36
48
|
|
|
37
49
|
Local package proof equivalent:
|
|
38
50
|
|
|
39
51
|
```bash
|
|
40
|
-
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
|
|
41
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,12 +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
|
|
65
|
-
const targetStylesEntry = path.join(targetRoot, 'src', 'styles.scss');
|
|
66
|
-
const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
|
|
67
|
-
assertTargetShape(manifest.componentName, targetRoot, targetAppDir, targetStylesEntry, targetTsconfig);
|
|
59
|
+
const { targetStylesEntry, targetTsconfig } = resolveSupportedProjectTarget(`trine add ${manifest.componentName}`, targetRoot);
|
|
68
60
|
const componentDestDir = path.join(targetRoot, 'src', 'app', 'components', 'ui', manifest.componentName);
|
|
69
|
-
const stylesDestDir = path.join(targetRoot, 'src', 'styles');
|
|
70
61
|
const componentCopyTargets = manifest.sourceFiles.map((source) => ({
|
|
71
62
|
source: path.join(TEMPLATE_ROOT, source),
|
|
72
63
|
destination: path.join(componentDestDir, path.basename(source)),
|
|
@@ -81,13 +72,12 @@ export function addComponent(manifest, options) {
|
|
|
81
72
|
].join('\n'));
|
|
82
73
|
}
|
|
83
74
|
mkdirSync(componentDestDir, { recursive: true });
|
|
84
|
-
mkdirSync(stylesDestDir, { recursive: true });
|
|
85
75
|
for (const { source, destination } of componentCopyTargets) {
|
|
86
76
|
copyFileSync(source, destination);
|
|
87
77
|
}
|
|
88
78
|
const componentCopiedFiles = componentCopyTargets.map(({ destination }) => toTargetRelativePath(targetRoot, destination));
|
|
89
|
-
const sharedStylesResult = ensureSharedStyleBaseline(targetRoot,
|
|
90
|
-
const copiedFiles = [...componentCopiedFiles, ...sharedStylesResult.
|
|
79
|
+
const sharedStylesResult = ensureSharedStyleBaseline(targetRoot, manifest.componentLabel);
|
|
80
|
+
const copiedFiles = [...componentCopiedFiles, ...sharedStylesResult.createdFiles];
|
|
91
81
|
const updatedFiles = [];
|
|
92
82
|
const warnings = [...sharedStylesResult.warnings];
|
|
93
83
|
const componentBarrelPath = path.join(componentDestDir, 'index.ts');
|
|
@@ -125,68 +115,6 @@ export function addComponent(manifest, options) {
|
|
|
125
115
|
targetRoot,
|
|
126
116
|
};
|
|
127
117
|
}
|
|
128
|
-
function ensureSharedStyleBaseline(targetRoot, stylesDestDir, componentLabel) {
|
|
129
|
-
const copiedFiles = [];
|
|
130
|
-
const warnings = [];
|
|
131
|
-
for (const sourceFile of STYLE_SOURCE_FILES) {
|
|
132
|
-
const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
|
|
133
|
-
const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
|
|
134
|
-
const relativeDestination = toTargetRelativePath(targetRoot, destinationPath);
|
|
135
|
-
if (!existsSync(destinationPath)) {
|
|
136
|
-
copyFileSync(templatePath, destinationPath);
|
|
137
|
-
copiedFiles.push(relativeDestination);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (readFileSync(destinationPath, 'utf8') !== readFileSync(templatePath, 'utf8')) {
|
|
141
|
-
warnings.push(`${relativeDestination} already exists and was preserved. Review it manually if the delivered ${componentLabel} expects newer shared styling baseline content.`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
copiedFiles,
|
|
146
|
-
warnings,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function assertTargetShape(componentName, targetRoot, targetAppDir, targetStylesEntry, targetTsconfig) {
|
|
150
|
-
const missing = [];
|
|
151
|
-
if (!existsSync(targetRoot)) {
|
|
152
|
-
missing.push(targetRoot);
|
|
153
|
-
}
|
|
154
|
-
if (!existsSync(targetAppDir)) {
|
|
155
|
-
missing.push(targetAppDir);
|
|
156
|
-
}
|
|
157
|
-
if (!existsSync(targetStylesEntry)) {
|
|
158
|
-
missing.push(targetStylesEntry);
|
|
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, src/styles.scss, and tsconfig.app.json.`,
|
|
166
|
-
...missing.map((file) => `- ${file}`),
|
|
167
|
-
].join('\n'));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function ensureLinesFile(filePath, lines) {
|
|
171
|
-
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
172
|
-
const normalizedExisting = existing.trimEnd();
|
|
173
|
-
const currentLines = normalizedExisting === '' ? [] : normalizedExisting.split('\n');
|
|
174
|
-
let changed = false;
|
|
175
|
-
for (const line of lines) {
|
|
176
|
-
if (!currentLines.includes(line)) {
|
|
177
|
-
currentLines.push(line);
|
|
178
|
-
changed = true;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (!existsSync(filePath)) {
|
|
182
|
-
changed = true;
|
|
183
|
-
}
|
|
184
|
-
if (changed) {
|
|
185
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
186
|
-
writeFileSync(filePath, `${currentLines.join('\n')}\n`);
|
|
187
|
-
}
|
|
188
|
-
return changed;
|
|
189
|
-
}
|
|
190
118
|
function rewriteDemoUiRootBarrel(filePath) {
|
|
191
119
|
const uiRootDir = path.dirname(filePath);
|
|
192
120
|
const localComponentLines = DEMO_LOCAL_COMPONENTS.filter(({ key }) => existsSync(path.join(uiRootDir, key, 'index.ts')));
|
|
@@ -205,113 +133,3 @@ function rewriteDemoUiRootBarrel(filePath) {
|
|
|
205
133
|
writeFileSync(filePath, nextContent);
|
|
206
134
|
return true;
|
|
207
135
|
}
|
|
208
|
-
function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
209
|
-
const currentText = readFileSync(tsconfigPath, 'utf8');
|
|
210
|
-
const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
|
|
211
|
-
if (parsed.error) {
|
|
212
|
-
throw new Error(`Unable to parse ${path.relative(cwd, tsconfigPath)} as JSONC.`);
|
|
213
|
-
}
|
|
214
|
-
const config = (parsed.config ?? {});
|
|
215
|
-
config.compilerOptions ??= {};
|
|
216
|
-
config.compilerOptions.paths ??= {};
|
|
217
|
-
const aliasTarget = isDemoTarget(targetRoot)
|
|
218
|
-
? toPosixPath(path.relative(process.cwd(), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
|
|
219
|
-
: toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
|
|
220
|
-
const wildcardTarget = 'packages/ui/*';
|
|
221
|
-
const currentAlias = config.compilerOptions.paths['@trine/ui'];
|
|
222
|
-
const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
|
|
223
|
-
const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
|
|
224
|
-
const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
|
|
225
|
-
(Array.isArray(currentWildcardAlias) &&
|
|
226
|
-
currentWildcardAlias.length === 1 &&
|
|
227
|
-
currentWildcardAlias[0] === wildcardTarget);
|
|
228
|
-
if (aliasIsCurrent && wildcardIsCurrent) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
|
|
232
|
-
if (isDemoTarget(targetRoot)) {
|
|
233
|
-
config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
|
|
234
|
-
}
|
|
235
|
-
else if ('@trine/ui/*' in config.compilerOptions.paths) {
|
|
236
|
-
delete config.compilerOptions.paths['@trine/ui/*'];
|
|
237
|
-
}
|
|
238
|
-
writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
function ensureStylesImport(stylesPath) {
|
|
242
|
-
const current = readFileSync(stylesPath, 'utf8');
|
|
243
|
-
if (current.includes(STYLE_IMPORT_LINE)) {
|
|
244
|
-
return {
|
|
245
|
-
updated: false,
|
|
246
|
-
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
const lines = current.split('\n');
|
|
250
|
-
let insertAt = -1;
|
|
251
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
252
|
-
const trimmed = lines[index].trim();
|
|
253
|
-
if (trimmed.startsWith('@use') || trimmed.startsWith('@import')) {
|
|
254
|
-
insertAt = index;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (insertAt === -1) {
|
|
258
|
-
lines.unshift(STYLE_IMPORT_LINE, '');
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
lines.splice(insertAt + 1, 0, STYLE_IMPORT_LINE);
|
|
262
|
-
}
|
|
263
|
-
writeFileSync(stylesPath, lines.join('\n'));
|
|
264
|
-
return {
|
|
265
|
-
updated: true,
|
|
266
|
-
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
function isDemoTarget(targetRoot) {
|
|
270
|
-
return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
|
|
271
|
-
path.resolve(targetRoot) === path.resolve(LOCAL_REPO_DEMO_ROOT));
|
|
272
|
-
}
|
|
273
|
-
function toPosixPath(filePath) {
|
|
274
|
-
return filePath.split(path.sep).join(path.posix.sep);
|
|
275
|
-
}
|
|
276
|
-
function toConfigRelativePath(filePath) {
|
|
277
|
-
const posixPath = toPosixPath(filePath);
|
|
278
|
-
return posixPath.startsWith('.') ? posixPath : `./${posixPath}`;
|
|
279
|
-
}
|
|
280
|
-
function toTargetRelativePath(targetRoot, filePath) {
|
|
281
|
-
return toPosixPath(path.relative(targetRoot, filePath));
|
|
282
|
-
}
|
|
283
|
-
function readTargetDependencyWarnings(targetRoot, componentLabel) {
|
|
284
|
-
const packageJsonPath = path.join(targetRoot, 'package.json');
|
|
285
|
-
if (!existsSync(packageJsonPath)) {
|
|
286
|
-
return [
|
|
287
|
-
'No package.json was found in the target root, so Tailwind CSS v4 and class-variance-authority prerequisites could not be checked automatically.',
|
|
288
|
-
];
|
|
289
|
-
}
|
|
290
|
-
try {
|
|
291
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
292
|
-
const deps = {
|
|
293
|
-
...(packageJson.dependencies ?? {}),
|
|
294
|
-
...(packageJson.devDependencies ?? {}),
|
|
295
|
-
};
|
|
296
|
-
const warnings = [];
|
|
297
|
-
if (!deps['class-variance-authority']) {
|
|
298
|
-
warnings.push(`class-variance-authority is missing from the target repo. Install it before building the delivered ${componentLabel}.`);
|
|
299
|
-
}
|
|
300
|
-
const tailwindRange = deps['tailwindcss'];
|
|
301
|
-
if (!tailwindRange) {
|
|
302
|
-
warnings.push(`tailwindcss is missing from the target repo. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
|
|
303
|
-
}
|
|
304
|
-
else if (!looksLikeTailwindV4(tailwindRange)) {
|
|
305
|
-
warnings.push(`The target repo declares tailwindcss@${tailwindRange}. The current proven ${componentLabel} baseline expects Tailwind CSS v4.`);
|
|
306
|
-
}
|
|
307
|
-
return warnings;
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
return [
|
|
311
|
-
`package.json could not be parsed for dependency checks. Verify Tailwind CSS v4 and class-variance-authority manually before building the delivered ${componentLabel}.`,
|
|
312
|
-
];
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function looksLikeTailwindV4(range) {
|
|
316
|
-
return /(^|[^\d])4(\D|$)/.test(range);
|
|
317
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { addButton } from "./add-button.js";
|
|
4
|
+
import { initProject } from "./init.js";
|
|
5
|
+
import { findAngularAppTargets, looksLikeAngularAppRoot } from "./project.js";
|
|
5
6
|
const HELP_TEXT = `Usage:
|
|
7
|
+
npx @trineui/cli@latest init [--target <app-root>]
|
|
6
8
|
npx @trineui/cli@latest add button [--target <app-root>]
|
|
9
|
+
trine init [--target <app-root>]
|
|
7
10
|
trine add button [--target <app-root>]
|
|
8
11
|
|
|
9
12
|
Defaults:
|
|
10
|
-
- current directory when it matches the supported
|
|
11
|
-
- otherwise auto-detect a single Angular app target under the current directory
|
|
12
|
-
- 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>
|
|
13
16
|
|
|
14
17
|
Notes:
|
|
15
|
-
- v0 supports
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
|
19
23
|
- the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
|
|
20
24
|
- consumer-owned component files fail clearly if they already exist
|
|
21
25
|
- shared styling baseline files are copied when missing and preserved when they already exist
|
|
@@ -23,19 +27,28 @@ Notes:
|
|
|
23
27
|
- apps/demo keeps a temporary @trine/ui/* bridge for non-localized components during local repo verification`;
|
|
24
28
|
const SUPPORTED_COMPONENTS = ['button'];
|
|
25
29
|
function main(argv) {
|
|
26
|
-
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
|
+
}
|
|
27
40
|
if (command !== 'add') {
|
|
28
41
|
throw new Error(command ? `Unsupported command: ${command}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
29
42
|
}
|
|
30
|
-
if (!isSupportedComponent(
|
|
31
|
-
throw new Error(
|
|
43
|
+
if (!isSupportedComponent(secondArg)) {
|
|
44
|
+
throw new Error(secondArg ? `Unsupported component: ${secondArg}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
32
45
|
}
|
|
33
|
-
const
|
|
46
|
+
const selection = resolveTargetSelection(process.cwd(), rest, `trine add ${secondArg}`);
|
|
34
47
|
const result = addButton({
|
|
35
|
-
target,
|
|
48
|
+
target: selection.target,
|
|
36
49
|
cwd: process.cwd(),
|
|
37
50
|
});
|
|
38
|
-
|
|
51
|
+
printAddSuccess(secondArg, result, selection);
|
|
39
52
|
}
|
|
40
53
|
function readTarget(argv) {
|
|
41
54
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -45,85 +58,90 @@ function readTarget(argv) {
|
|
|
45
58
|
}
|
|
46
59
|
return undefined;
|
|
47
60
|
}
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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);
|
|
53
96
|
const lines = [
|
|
54
|
-
|
|
97
|
+
'trine init completed.',
|
|
55
98
|
`Target: ${displayTarget}`,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
...result.copiedFiles.map((file) => `- ${file}`),
|
|
59
|
-
'',
|
|
60
|
-
'Created or updated:',
|
|
61
|
-
...result.updatedFiles.map((file) => `- ${file}`),
|
|
99
|
+
`Framework: ${result.framework}`,
|
|
100
|
+
`Resolved stylesheet entry: ${stylesheetDisplay}`,
|
|
62
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'));
|
|
63
106
|
if (result.warnings.length > 0) {
|
|
64
107
|
lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
|
|
65
108
|
}
|
|
66
|
-
lines.push('', 'Manual next steps:',
|
|
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'));
|
|
121
|
+
if (result.warnings.length > 0) {
|
|
122
|
+
lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
|
|
123
|
+
}
|
|
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.');
|
|
67
125
|
if (isRepoDemoVerification) {
|
|
68
126
|
lines.push('- Open /validation-shell and review the CLI delivery proof section for the temporary demo verification path.');
|
|
69
127
|
}
|
|
70
128
|
console.log(lines.join('\n'));
|
|
71
129
|
}
|
|
130
|
+
function renderFileGroup(files, emptyMessage) {
|
|
131
|
+
return files.length > 0 ? files.map((file) => `- ${file}`) : [emptyMessage];
|
|
132
|
+
}
|
|
72
133
|
function isSupportedComponent(value) {
|
|
73
134
|
return SUPPORTED_COMPONENTS.some((component) => component === value);
|
|
74
135
|
}
|
|
75
136
|
function capitalize(value) {
|
|
76
137
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
77
138
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
function autoDetectTarget(cwd) {
|
|
82
|
-
if (looksLikeAngularAppRoot(cwd)) {
|
|
83
|
-
return '.';
|
|
84
|
-
}
|
|
85
|
-
const matches = findAngularAppTargets(cwd);
|
|
86
|
-
if (matches.length === 1) {
|
|
87
|
-
return matches[0];
|
|
88
|
-
}
|
|
89
|
-
if (matches.length > 1) {
|
|
90
|
-
throw new Error([
|
|
91
|
-
'Multiple Angular app targets were found under the current directory. Re-run with --target <app-root>:',
|
|
92
|
-
...matches.map((match) => `- ${match}`),
|
|
93
|
-
].join('\n'));
|
|
94
|
-
}
|
|
95
|
-
return '.';
|
|
96
|
-
}
|
|
97
|
-
function findAngularAppTargets(root) {
|
|
98
|
-
const matches = new Set();
|
|
99
|
-
walkForAngularApps(root, root, matches);
|
|
100
|
-
return [...matches].sort((left, right) => left.localeCompare(right));
|
|
101
|
-
}
|
|
102
|
-
function walkForAngularApps(root, currentDir, matches) {
|
|
103
|
-
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
104
|
-
if (entry.isDirectory()) {
|
|
105
|
-
if (shouldSkipDirectory(entry.name)) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
walkForAngularApps(root, path.join(currentDir, entry.name), matches);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (!entry.isFile() || entry.name !== 'tsconfig.app.json') {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
const candidateRoot = currentDir;
|
|
115
|
-
if (!looksLikeAngularAppRoot(candidateRoot)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const relativeRoot = path.relative(root, candidateRoot) || '.';
|
|
119
|
-
matches.add(toPosixPath(relativeRoot));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function shouldSkipDirectory(name) {
|
|
123
|
-
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;
|
|
124
142
|
}
|
|
125
|
-
function
|
|
126
|
-
return
|
|
143
|
+
function toDisplayFilePath(targetRoot, filePath) {
|
|
144
|
+
return path.relative(targetRoot, filePath) || '.';
|
|
127
145
|
}
|
|
128
146
|
try {
|
|
129
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
|
+
}
|