@trineui/cli 0.2.0 → 0.3.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 +8 -2
- package/dist/index.js +170 -24
- package/dist/init.js +23 -8
- package/dist/project.js +131 -38
- package/dist/prompt.js +59 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,17 +16,22 @@ Registry status note:
|
|
|
16
16
|
Repository-local current command surface:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
trine init --target <app-root>
|
|
19
|
+
trine init [--target <app-root>] [--yes]
|
|
20
20
|
trine add button --target <app-root>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Notes:
|
|
24
24
|
|
|
25
25
|
- `init` is now the preferred first step for project readiness; `add button` still works without it for backward compatibility
|
|
26
|
+
- `init` is guided by default: it detects the target, surfaces warnings, previews file changes, and asks for confirmation before mutation
|
|
27
|
+
- `init --yes` keeps a non-interactive fast path for automation and scripting
|
|
26
28
|
- `button` is the only supported component in this public-style baseline
|
|
27
29
|
- omitting `--target` uses the current directory when it already matches the supported Angular app shape
|
|
28
30
|
- 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
|
|
29
|
-
- when multiple Angular app targets are found,
|
|
31
|
+
- when multiple Angular app targets are found, guided `init` lets the user choose one interactively
|
|
32
|
+
- when multiple Angular app targets are found, `init --yes` and `add button` fail clearly and ask for `--target <app-root>`
|
|
33
|
+
- when multiple plausible stylesheet entries are found, guided `init` lets the user choose one interactively
|
|
34
|
+
- when multiple plausible stylesheet entries are found, `init --yes` fails clearly and asks the user to rerun without `--yes`
|
|
30
35
|
- 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
36
|
- `init` detects `angular` and `ionic-angular` targets conservatively; unsupported targets fail clearly
|
|
32
37
|
- the canonical public package name is `@trineui/cli`
|
|
@@ -36,6 +41,7 @@ Notes:
|
|
|
36
41
|
- packaged/public-style proof uses a packed local tarball to simulate `npx @trineui/cli@latest add button`
|
|
37
42
|
- the packaged CLI ships compiled runtime files plus Button templates so it can execute from `node_modules` in a real `npx`-style flow
|
|
38
43
|
- `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
|
|
44
|
+
- rerunning `init` on an already prepared target exits cleanly with a no-op summary instead of prompting again
|
|
39
45
|
- consumer-owned component destination files cause a clear failure
|
|
40
46
|
- existing shared baseline files (`tokens.css` and `trine-consumer.css`) are preserved so a second component can be added into the same target repo
|
|
41
47
|
- the command copies consumer-owned source instead of wiring runtime back to `packages/ui`
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { addButton } from "./add-button.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { applyInitPlan, planInitProject } from "./init.js";
|
|
5
|
+
import { chooseFromList, confirmAction } from "./prompt.js";
|
|
6
|
+
import { findAngularAppTargets, inspectProjectTarget, looksLikeAngularAppRoot } from "./project.js";
|
|
6
7
|
const HELP_TEXT = `Usage:
|
|
7
|
-
npx @trineui/cli@latest init [--target <app-root>]
|
|
8
|
+
npx @trineui/cli@latest init [--target <app-root>] [--yes]
|
|
8
9
|
npx @trineui/cli@latest add button [--target <app-root>]
|
|
9
|
-
trine init [--target <app-root>]
|
|
10
|
+
trine init [--target <app-root>] [--yes]
|
|
10
11
|
trine add button [--target <app-root>]
|
|
11
12
|
|
|
12
13
|
Defaults:
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- when
|
|
14
|
+
- init is guided by default and asks for confirmation before mutating project files
|
|
15
|
+
- init --yes keeps a non-interactive fast path for automation and scripting
|
|
16
|
+
- current directory is used when it already matches the supported Trine app target shape
|
|
17
|
+
- otherwise init/add auto-detect a single supported Angular app target under the current directory
|
|
18
|
+
- when multiple supported app targets are found, guided init lets you choose and add still asks for --target
|
|
16
19
|
|
|
17
20
|
Notes:
|
|
18
21
|
- v0 supports init plus add button only
|
|
19
22
|
- 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
|
|
23
|
+
- add button still works without init for backward compatibility, but init is the preferred first step
|
|
21
24
|
- 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
25
|
- v0 distinguishes angular and ionic-angular targets; unsupported frameworks fail clearly
|
|
23
26
|
- the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
|
|
@@ -26,13 +29,32 @@ Notes:
|
|
|
26
29
|
- @trine/ui is configured as a consumer-local alias inside the target app
|
|
27
30
|
- apps/demo keeps a temporary @trine/ui/* bridge for non-localized components during local repo verification`;
|
|
28
31
|
const SUPPORTED_COMPONENTS = ['button'];
|
|
29
|
-
function main(argv) {
|
|
32
|
+
async function main(argv) {
|
|
30
33
|
const [command, secondArg, ...rest] = argv;
|
|
31
34
|
if (command === 'init') {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
35
|
+
const flags = parseFlags(argv.slice(1), ['--target', '--yes']);
|
|
36
|
+
const selection = flags.yes
|
|
37
|
+
? resolveNonInteractiveInitSelection(process.cwd(), flags.target)
|
|
38
|
+
: await resolveGuidedInitSelection(process.cwd(), flags.target);
|
|
39
|
+
const plan = planInitProject({
|
|
34
40
|
target: selection.target,
|
|
35
41
|
cwd: process.cwd(),
|
|
42
|
+
targetStylesEntry: selection.targetStylesEntry,
|
|
43
|
+
});
|
|
44
|
+
if (!hasInitMutations(plan)) {
|
|
45
|
+
printInitNoop(plan, selection);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!flags.yes) {
|
|
49
|
+
printInitPreview(plan, selection);
|
|
50
|
+
const shouldProceed = await confirmAction('Proceed? (Y/n)');
|
|
51
|
+
if (!shouldProceed) {
|
|
52
|
+
console.log('trine init cancelled. No changes were made.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const result = applyInitPlan(plan, {
|
|
57
|
+
cwd: process.cwd(),
|
|
36
58
|
});
|
|
37
59
|
printInitSuccess(result, selection);
|
|
38
60
|
return;
|
|
@@ -43,23 +65,54 @@ function main(argv) {
|
|
|
43
65
|
if (!isSupportedComponent(secondArg)) {
|
|
44
66
|
throw new Error(secondArg ? `Unsupported component: ${secondArg}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
45
67
|
}
|
|
46
|
-
const
|
|
68
|
+
const flags = parseFlags(rest, ['--target']);
|
|
69
|
+
const selection = resolveTargetSelection(process.cwd(), flags.target, `trine add ${secondArg}`);
|
|
47
70
|
const result = addButton({
|
|
48
71
|
target: selection.target,
|
|
49
72
|
cwd: process.cwd(),
|
|
50
73
|
});
|
|
51
74
|
printAddSuccess(secondArg, result, selection);
|
|
52
75
|
}
|
|
53
|
-
function
|
|
76
|
+
function resolveNonInteractiveInitSelection(cwd, explicitTarget) {
|
|
77
|
+
const selection = resolveTargetSelection(cwd, explicitTarget, 'trine init');
|
|
78
|
+
const targetRoot = path.resolve(cwd, selection.target);
|
|
79
|
+
const inspection = inspectProjectTarget(targetRoot);
|
|
80
|
+
if (inspection.framework !== 'unsupported' &&
|
|
81
|
+
inspection.targetStylesEntryCandidates.length > 1 &&
|
|
82
|
+
inspection.targetStylesEntryResolution === 'default') {
|
|
83
|
+
const displayTarget = selection.mode === 'cwd' ? '.' : toDisplayTarget(cwd, targetRoot);
|
|
84
|
+
throw new Error([
|
|
85
|
+
`Multiple plausible global stylesheet entries were found for target (${displayTarget}). Re-run without --yes to choose one interactively:`,
|
|
86
|
+
...inspection.targetStylesEntryCandidates.map((candidate) => `- ${toDisplayFilePath(targetRoot, candidate)}`),
|
|
87
|
+
].join('\n'));
|
|
88
|
+
}
|
|
89
|
+
return selection;
|
|
90
|
+
}
|
|
91
|
+
function parseFlags(argv, allowedFlags) {
|
|
92
|
+
const flags = {
|
|
93
|
+
yes: false,
|
|
94
|
+
};
|
|
54
95
|
for (let index = 0; index < argv.length; index += 1) {
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
const token = argv[index];
|
|
97
|
+
if (!allowedFlags.includes(token)) {
|
|
98
|
+
throw new Error(`Unsupported flag: ${token}\n\n${HELP_TEXT}`);
|
|
99
|
+
}
|
|
100
|
+
if (token === '--yes') {
|
|
101
|
+
flags.yes = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (token === '--target') {
|
|
105
|
+
const target = argv[index + 1];
|
|
106
|
+
if (!target || target.startsWith('--')) {
|
|
107
|
+
throw new Error(`--target requires a value.\n\n${HELP_TEXT}`);
|
|
108
|
+
}
|
|
109
|
+
flags.target = target;
|
|
110
|
+
index += 1;
|
|
57
111
|
}
|
|
58
112
|
}
|
|
59
|
-
return
|
|
113
|
+
return flags;
|
|
60
114
|
}
|
|
61
|
-
function resolveTargetSelection(cwd,
|
|
62
|
-
const explicitTarget = readTarget(argv);
|
|
115
|
+
function resolveTargetSelection(cwd, explicitTarget, commandLabel) {
|
|
63
116
|
if (explicitTarget) {
|
|
64
117
|
return {
|
|
65
118
|
target: explicitTarget,
|
|
@@ -90,6 +143,92 @@ function resolveTargetSelection(cwd, argv, commandLabel) {
|
|
|
90
143
|
mode: 'cwd',
|
|
91
144
|
};
|
|
92
145
|
}
|
|
146
|
+
async function resolveGuidedInitSelection(cwd, explicitTarget) {
|
|
147
|
+
const targetSelection = explicitTarget
|
|
148
|
+
? {
|
|
149
|
+
target: explicitTarget,
|
|
150
|
+
mode: 'explicit',
|
|
151
|
+
}
|
|
152
|
+
: await chooseInitTarget(cwd);
|
|
153
|
+
const targetRoot = path.resolve(cwd, targetSelection.target);
|
|
154
|
+
const inspection = inspectProjectTarget(targetRoot);
|
|
155
|
+
if (inspection.framework === 'unsupported') {
|
|
156
|
+
return targetSelection;
|
|
157
|
+
}
|
|
158
|
+
if (inspection.targetStylesEntryCandidates.length > 1 &&
|
|
159
|
+
inspection.targetStylesEntryResolution === 'default') {
|
|
160
|
+
const targetStylesEntry = await chooseFromList('Multiple plausible global stylesheet entries were found:', inspection.targetStylesEntryCandidates, {
|
|
161
|
+
renderItem: (item, index) => `${String(index + 1)}. ${toDisplayFilePath(targetRoot, item)}`,
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
...targetSelection,
|
|
165
|
+
targetStylesEntry,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return targetSelection;
|
|
169
|
+
}
|
|
170
|
+
async function chooseInitTarget(cwd) {
|
|
171
|
+
if (looksLikeAngularAppRoot(cwd)) {
|
|
172
|
+
return {
|
|
173
|
+
target: '.',
|
|
174
|
+
mode: 'cwd',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const matches = findAngularAppTargets(cwd);
|
|
178
|
+
if (matches.length === 1) {
|
|
179
|
+
return {
|
|
180
|
+
target: matches[0],
|
|
181
|
+
mode: 'auto-detected',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (matches.length > 1) {
|
|
185
|
+
const selected = await chooseFromList('Multiple supported apps found:', matches);
|
|
186
|
+
return {
|
|
187
|
+
target: selected,
|
|
188
|
+
mode: 'selected',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
target: '.',
|
|
193
|
+
mode: 'cwd',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function printInitPreview(plan, selection) {
|
|
197
|
+
const displayTarget = toDisplayTarget(process.cwd(), plan.targetRoot);
|
|
198
|
+
const stylesheetDisplay = toDisplayFilePath(plan.targetRoot, plan.targetStylesEntry);
|
|
199
|
+
const lines = [
|
|
200
|
+
`Detected target: ${displayTarget}`,
|
|
201
|
+
`Detected framework: ${plan.framework}`,
|
|
202
|
+
`Detected stylesheet entry: ${stylesheetDisplay}`,
|
|
203
|
+
];
|
|
204
|
+
if (selection.mode === 'auto-detected' || selection.mode === 'selected') {
|
|
205
|
+
lines.push(`Selected app: ${displayTarget}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push('', 'Trine will:', ...renderPlannedInitActions(plan));
|
|
208
|
+
if (plan.warnings.length > 0) {
|
|
209
|
+
lines.push('', 'Warnings:', ...plan.warnings.map((warning) => `- ${warning}`));
|
|
210
|
+
}
|
|
211
|
+
console.log(lines.join('\n'));
|
|
212
|
+
}
|
|
213
|
+
function printInitNoop(plan, selection) {
|
|
214
|
+
const displayTarget = toDisplayTarget(process.cwd(), plan.targetRoot);
|
|
215
|
+
const stylesheetDisplay = toDisplayFilePath(plan.targetRoot, plan.targetStylesEntry);
|
|
216
|
+
const lines = [
|
|
217
|
+
'trine init found the Trine baseline already in place.',
|
|
218
|
+
`Target: ${displayTarget}`,
|
|
219
|
+
`Framework: ${plan.framework}`,
|
|
220
|
+
`Resolved stylesheet entry: ${stylesheetDisplay}`,
|
|
221
|
+
];
|
|
222
|
+
if (selection.mode === 'auto-detected' || selection.mode === 'selected') {
|
|
223
|
+
lines.push(`Selected app: ${displayTarget}`);
|
|
224
|
+
}
|
|
225
|
+
lines.push('', 'Already in place:', ...renderFileGroup(plan.reusedFiles, '- nothing to report'));
|
|
226
|
+
if (plan.warnings.length > 0) {
|
|
227
|
+
lines.push('', 'Warnings:', ...plan.warnings.map((warning) => `- ${warning}`));
|
|
228
|
+
}
|
|
229
|
+
lines.push('', 'No changes were made.');
|
|
230
|
+
console.log(lines.join('\n'));
|
|
231
|
+
}
|
|
93
232
|
function printInitSuccess(result, selection) {
|
|
94
233
|
const displayTarget = toDisplayTarget(process.cwd(), result.targetRoot);
|
|
95
234
|
const stylesheetDisplay = toDisplayFilePath(result.targetRoot, result.targetStylesEntry);
|
|
@@ -99,7 +238,7 @@ function printInitSuccess(result, selection) {
|
|
|
99
238
|
`Framework: ${result.framework}`,
|
|
100
239
|
`Resolved stylesheet entry: ${stylesheetDisplay}`,
|
|
101
240
|
];
|
|
102
|
-
if (selection.mode === 'auto-detected') {
|
|
241
|
+
if (selection.mode === 'auto-detected' || selection.mode === 'selected') {
|
|
103
242
|
lines.push(`Selected app: ${displayTarget}`);
|
|
104
243
|
}
|
|
105
244
|
lines.push('', 'Created:', ...renderFileGroup(result.createdFiles, '- nothing created'), '', 'Reused:', ...renderFileGroup(result.reusedFiles, '- nothing reused'), '', 'Updated:', ...renderFileGroup(result.updatedFiles, '- nothing updated'));
|
|
@@ -130,6 +269,16 @@ function printAddSuccess(component, result, selection) {
|
|
|
130
269
|
function renderFileGroup(files, emptyMessage) {
|
|
131
270
|
return files.length > 0 ? files.map((file) => `- ${file}`) : [emptyMessage];
|
|
132
271
|
}
|
|
272
|
+
function renderPlannedInitActions(plan) {
|
|
273
|
+
return [
|
|
274
|
+
...plan.createdFiles.map((file) => `- create ${file}`),
|
|
275
|
+
...plan.updatedFiles.map((file) => `- update ${file}`),
|
|
276
|
+
...plan.reusedFiles.map((file) => `- keep ${file}`),
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
function hasInitMutations(plan) {
|
|
280
|
+
return plan.createdFiles.length > 0 || plan.updatedFiles.length > 0;
|
|
281
|
+
}
|
|
133
282
|
function isSupportedComponent(value) {
|
|
134
283
|
return SUPPORTED_COMPONENTS.some((component) => component === value);
|
|
135
284
|
}
|
|
@@ -143,11 +292,8 @@ function toDisplayTarget(cwd, targetRoot) {
|
|
|
143
292
|
function toDisplayFilePath(targetRoot, filePath) {
|
|
144
293
|
return path.relative(targetRoot, filePath) || '.';
|
|
145
294
|
}
|
|
146
|
-
|
|
147
|
-
main(process.argv.slice(2));
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
295
|
+
await main(process.argv.slice(2)).catch((error) => {
|
|
150
296
|
const message = error instanceof Error ? error.message : String(error);
|
|
151
297
|
console.error(message);
|
|
152
298
|
process.exitCode = 1;
|
|
153
|
-
}
|
|
299
|
+
});
|
package/dist/init.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { ensureSharedStyleBaseline, ensureStylesImport, ensureTsconfigAlias, ensureUiRootBarrel, isDemoTarget, readTargetDependencyWarnings, resolveSupportedProjectTarget, toTargetRelativePath, } from "./project.js";
|
|
3
|
-
export function
|
|
2
|
+
import { ensureSharedStyleBaseline, ensureStylesImport, ensureTsconfigAlias, ensureUiRootBarrel, inspectSharedStyleBaseline, inspectStylesImport, inspectTsconfigAlias, inspectUiRootBarrel, isDemoTarget, readTargetDependencyWarnings, resolveSupportedProjectTarget, toTargetRelativePath, } from "./project.js";
|
|
3
|
+
export function planInitProject(options) {
|
|
4
4
|
const targetRoot = path.resolve(options.cwd, options.target);
|
|
5
|
-
const { framework, targetStylesEntry, targetTsconfig } = resolveSupportedProjectTarget('trine init', targetRoot
|
|
5
|
+
const { framework, targetStylesEntry, targetTsconfig } = resolveSupportedProjectTarget('trine init', targetRoot, {
|
|
6
|
+
preferredStylesEntry: options.targetStylesEntry,
|
|
7
|
+
});
|
|
6
8
|
const createdFiles = [];
|
|
7
9
|
const updatedFiles = [];
|
|
8
10
|
const reusedFiles = [];
|
|
9
11
|
const warnings = [];
|
|
10
|
-
const sharedStylesResult =
|
|
12
|
+
const sharedStylesResult = inspectSharedStyleBaseline(targetRoot, 'Trine components');
|
|
11
13
|
createdFiles.push(...sharedStylesResult.createdFiles);
|
|
12
14
|
reusedFiles.push(...sharedStylesResult.reusedFiles);
|
|
13
15
|
warnings.push(...sharedStylesResult.warnings);
|
|
14
|
-
const uiRootResult =
|
|
16
|
+
const uiRootResult = inspectUiRootBarrel(targetRoot);
|
|
15
17
|
if (uiRootResult.created) {
|
|
16
18
|
createdFiles.push(uiRootResult.file);
|
|
17
19
|
}
|
|
@@ -19,15 +21,15 @@ export function initProject(options) {
|
|
|
19
21
|
reusedFiles.push(uiRootResult.file);
|
|
20
22
|
}
|
|
21
23
|
const tsconfigDisplayPath = toTargetRelativePath(targetRoot, targetTsconfig);
|
|
22
|
-
if (
|
|
24
|
+
if (inspectTsconfigAlias(targetTsconfig, targetRoot, options.cwd).needsUpdate) {
|
|
23
25
|
updatedFiles.push(tsconfigDisplayPath);
|
|
24
26
|
}
|
|
25
27
|
else {
|
|
26
28
|
reusedFiles.push(tsconfigDisplayPath);
|
|
27
29
|
}
|
|
28
30
|
const stylesDisplayPath = toTargetRelativePath(targetRoot, targetStylesEntry);
|
|
29
|
-
const stylesResult =
|
|
30
|
-
if (stylesResult.
|
|
31
|
+
const stylesResult = inspectStylesImport(targetStylesEntry);
|
|
32
|
+
if (stylesResult.needsUpdate) {
|
|
31
33
|
updatedFiles.push(stylesDisplayPath);
|
|
32
34
|
}
|
|
33
35
|
else {
|
|
@@ -44,6 +46,7 @@ export function initProject(options) {
|
|
|
44
46
|
}
|
|
45
47
|
return {
|
|
46
48
|
targetRoot,
|
|
49
|
+
targetTsconfig,
|
|
47
50
|
framework,
|
|
48
51
|
targetStylesEntry,
|
|
49
52
|
createdFiles,
|
|
@@ -52,3 +55,15 @@ export function initProject(options) {
|
|
|
52
55
|
warnings,
|
|
53
56
|
};
|
|
54
57
|
}
|
|
58
|
+
export function applyInitPlan(plan, options) {
|
|
59
|
+
ensureSharedStyleBaseline(plan.targetRoot, 'Trine components');
|
|
60
|
+
ensureUiRootBarrel(plan.targetRoot);
|
|
61
|
+
ensureTsconfigAlias(plan.targetTsconfig, plan.targetRoot, options.cwd);
|
|
62
|
+
ensureStylesImport(plan.targetStylesEntry);
|
|
63
|
+
return plan;
|
|
64
|
+
}
|
|
65
|
+
export function initProject(options) {
|
|
66
|
+
return applyInitPlan(planInitProject(options), {
|
|
67
|
+
cwd: options.cwd,
|
|
68
|
+
});
|
|
69
|
+
}
|
package/dist/project.js
CHANGED
|
@@ -8,10 +8,11 @@ const CONVENTIONAL_STYLE_ENTRY_PATHS = ['src/styles.scss', 'src/styles.css', 'sr
|
|
|
8
8
|
export const TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
9
9
|
const LOCAL_REPO_ROOT = path.resolve(TEMPLATE_ROOT, '../../..');
|
|
10
10
|
export const LOCAL_REPO_DEMO_ROOT = path.resolve(TEMPLATE_ROOT, '../../../apps/demo');
|
|
11
|
-
export function inspectProjectTarget(targetRoot) {
|
|
11
|
+
export function inspectProjectTarget(targetRoot, options = {}) {
|
|
12
12
|
const targetAppDir = path.join(targetRoot, 'src', 'app');
|
|
13
13
|
const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
|
|
14
|
-
const
|
|
14
|
+
const targetStylesEntryCandidates = resolveStylesEntryCandidates(targetRoot);
|
|
15
|
+
const { targetStylesEntry, targetStylesEntryResolution } = resolveStylesEntrySelection(targetRoot, targetStylesEntryCandidates, options.preferredStylesEntry);
|
|
15
16
|
const missingPaths = [];
|
|
16
17
|
if (!existsSync(targetRoot)) {
|
|
17
18
|
missingPaths.push(targetRoot);
|
|
@@ -30,13 +31,15 @@ export function inspectProjectTarget(targetRoot) {
|
|
|
30
31
|
targetRoot,
|
|
31
32
|
targetAppDir,
|
|
32
33
|
targetTsconfig,
|
|
34
|
+
targetStylesEntryCandidates,
|
|
33
35
|
targetStylesEntry,
|
|
36
|
+
targetStylesEntryResolution,
|
|
34
37
|
framework,
|
|
35
38
|
missingPaths,
|
|
36
39
|
};
|
|
37
40
|
}
|
|
38
|
-
export function resolveSupportedProjectTarget(commandLabel, targetRoot) {
|
|
39
|
-
const inspection = inspectProjectTarget(targetRoot);
|
|
41
|
+
export function resolveSupportedProjectTarget(commandLabel, targetRoot, options = {}) {
|
|
42
|
+
const inspection = inspectProjectTarget(targetRoot, options);
|
|
40
43
|
if (inspection.framework === 'unsupported' || !inspection.targetStylesEntry) {
|
|
41
44
|
throw new Error([
|
|
42
45
|
`${commandLabel} requires a supported Angular or Ionic Angular app target with src/app, a global stylesheet entry, and tsconfig.app.json.`,
|
|
@@ -60,17 +63,31 @@ export function looksLikeAngularAppRoot(root) {
|
|
|
60
63
|
return inspectProjectTarget(root).framework !== 'unsupported';
|
|
61
64
|
}
|
|
62
65
|
export function ensureSharedStyleBaseline(targetRoot, componentLabel) {
|
|
66
|
+
const inspection = inspectSharedStyleBaseline(targetRoot, componentLabel);
|
|
67
|
+
const stylesDestDir = path.join(targetRoot, 'src', 'styles');
|
|
68
|
+
mkdirSync(stylesDestDir, { recursive: true });
|
|
69
|
+
for (const sourceFile of STYLE_SOURCE_FILES) {
|
|
70
|
+
const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
|
|
71
|
+
const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
|
|
72
|
+
if (!inspection.createdFiles.includes(toTargetRelativePath(targetRoot, destinationPath))) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!existsSync(destinationPath)) {
|
|
76
|
+
copyFileSync(templatePath, destinationPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return inspection;
|
|
80
|
+
}
|
|
81
|
+
export function inspectSharedStyleBaseline(targetRoot, componentLabel) {
|
|
63
82
|
const stylesDestDir = path.join(targetRoot, 'src', 'styles');
|
|
64
83
|
const createdFiles = [];
|
|
65
84
|
const reusedFiles = [];
|
|
66
85
|
const warnings = [];
|
|
67
|
-
mkdirSync(stylesDestDir, { recursive: true });
|
|
68
86
|
for (const sourceFile of STYLE_SOURCE_FILES) {
|
|
69
87
|
const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
|
|
70
88
|
const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
|
|
71
89
|
const relativeDestination = toTargetRelativePath(targetRoot, destinationPath);
|
|
72
90
|
if (!existsSync(destinationPath)) {
|
|
73
|
-
copyFileSync(templatePath, destinationPath);
|
|
74
91
|
createdFiles.push(relativeDestination);
|
|
75
92
|
continue;
|
|
76
93
|
}
|
|
@@ -88,16 +105,18 @@ export function ensureSharedStyleBaseline(targetRoot, componentLabel) {
|
|
|
88
105
|
}
|
|
89
106
|
export function ensureUiRootBarrel(targetRoot) {
|
|
90
107
|
const uiRootPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
file: toTargetRelativePath(targetRoot, uiRootPath),
|
|
95
|
-
};
|
|
108
|
+
const inspection = inspectUiRootBarrel(targetRoot);
|
|
109
|
+
if (!inspection.created) {
|
|
110
|
+
return inspection;
|
|
96
111
|
}
|
|
97
112
|
mkdirSync(path.dirname(uiRootPath), { recursive: true });
|
|
98
113
|
writeFileSync(uiRootPath, '\n');
|
|
114
|
+
return inspection;
|
|
115
|
+
}
|
|
116
|
+
export function inspectUiRootBarrel(targetRoot) {
|
|
117
|
+
const uiRootPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
|
|
99
118
|
return {
|
|
100
|
-
created:
|
|
119
|
+
created: !existsSync(uiRootPath),
|
|
101
120
|
file: toTargetRelativePath(targetRoot, uiRootPath),
|
|
102
121
|
};
|
|
103
122
|
}
|
|
@@ -122,6 +141,10 @@ export function ensureLinesFile(filePath, lines) {
|
|
|
122
141
|
return changed;
|
|
123
142
|
}
|
|
124
143
|
export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
144
|
+
const inspection = inspectTsconfigAlias(tsconfigPath, targetRoot, cwd);
|
|
145
|
+
if (!inspection.needsUpdate) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
125
148
|
const currentText = readFileSync(tsconfigPath, 'utf8');
|
|
126
149
|
const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
|
|
127
150
|
if (parsed.error) {
|
|
@@ -130,20 +153,7 @@ export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
|
130
153
|
const config = (parsed.config ?? {});
|
|
131
154
|
config.compilerOptions ??= {};
|
|
132
155
|
config.compilerOptions.paths ??= {};
|
|
133
|
-
const aliasTarget =
|
|
134
|
-
? toPosixPath(path.relative(LOCAL_REPO_ROOT, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
|
|
135
|
-
: toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
|
|
136
|
-
const wildcardTarget = 'packages/ui/*';
|
|
137
|
-
const currentAlias = config.compilerOptions.paths['@trine/ui'];
|
|
138
|
-
const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
|
|
139
|
-
const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
|
|
140
|
-
const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
|
|
141
|
-
(Array.isArray(currentWildcardAlias) &&
|
|
142
|
-
currentWildcardAlias.length === 1 &&
|
|
143
|
-
currentWildcardAlias[0] === wildcardTarget);
|
|
144
|
-
if (aliasIsCurrent && wildcardIsCurrent) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
156
|
+
const { aliasTarget, wildcardTarget } = buildAliasTargets(tsconfigPath, targetRoot);
|
|
147
157
|
config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
|
|
148
158
|
if (isDemoTarget(targetRoot)) {
|
|
149
159
|
config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
|
|
@@ -154,14 +164,41 @@ export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
|
154
164
|
writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
155
165
|
return true;
|
|
156
166
|
}
|
|
167
|
+
export function inspectTsconfigAlias(tsconfigPath, targetRoot, cwd) {
|
|
168
|
+
const currentText = readFileSync(tsconfigPath, 'utf8');
|
|
169
|
+
const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
|
|
170
|
+
if (parsed.error) {
|
|
171
|
+
throw new Error(`Unable to parse ${path.relative(cwd, tsconfigPath)} as JSONC.`);
|
|
172
|
+
}
|
|
173
|
+
const config = (parsed.config ?? {});
|
|
174
|
+
config.compilerOptions ??= {};
|
|
175
|
+
config.compilerOptions.paths ??= {};
|
|
176
|
+
const { aliasTarget, wildcardTarget } = buildAliasTargets(tsconfigPath, targetRoot);
|
|
177
|
+
const currentAlias = config.compilerOptions.paths['@trine/ui'];
|
|
178
|
+
const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
|
|
179
|
+
const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
|
|
180
|
+
const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
|
|
181
|
+
(Array.isArray(currentWildcardAlias) &&
|
|
182
|
+
currentWildcardAlias.length === 1 &&
|
|
183
|
+
currentWildcardAlias[0] === wildcardTarget);
|
|
184
|
+
if (aliasIsCurrent && wildcardIsCurrent) {
|
|
185
|
+
return {
|
|
186
|
+
needsUpdate: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
needsUpdate: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
157
193
|
export function ensureStylesImport(stylesPath) {
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
194
|
+
const inspection = inspectStylesImport(stylesPath);
|
|
195
|
+
if (!inspection.needsUpdate) {
|
|
160
196
|
return {
|
|
161
197
|
updated: false,
|
|
162
|
-
authoringImportStillPresent:
|
|
198
|
+
authoringImportStillPresent: inspection.authoringImportStillPresent,
|
|
163
199
|
};
|
|
164
200
|
}
|
|
201
|
+
const current = readFileSync(stylesPath, 'utf8');
|
|
165
202
|
const lines = current.split('\n');
|
|
166
203
|
let insertAt = -1;
|
|
167
204
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -179,6 +216,13 @@ export function ensureStylesImport(stylesPath) {
|
|
|
179
216
|
writeFileSync(stylesPath, lines.join('\n'));
|
|
180
217
|
return {
|
|
181
218
|
updated: true,
|
|
219
|
+
authoringImportStillPresent: inspection.authoringImportStillPresent,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function inspectStylesImport(stylesPath) {
|
|
223
|
+
const current = readFileSync(stylesPath, 'utf8');
|
|
224
|
+
return {
|
|
225
|
+
needsUpdate: !current.includes(STYLE_IMPORT_LINE),
|
|
182
226
|
authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
|
|
183
227
|
};
|
|
184
228
|
}
|
|
@@ -215,20 +259,23 @@ export function readTargetDependencyWarnings(targetRoot, componentLabel) {
|
|
|
215
259
|
}
|
|
216
260
|
}
|
|
217
261
|
export function resolveStylesEntry(targetRoot) {
|
|
262
|
+
return inspectProjectTarget(targetRoot).targetStylesEntry;
|
|
263
|
+
}
|
|
264
|
+
export function resolveStylesEntryCandidates(targetRoot) {
|
|
265
|
+
const candidates = new Set();
|
|
218
266
|
for (const relativePath of CONVENTIONAL_STYLE_ENTRY_PATHS) {
|
|
219
267
|
const absolutePath = path.join(targetRoot, relativePath);
|
|
220
268
|
if (existsSync(absolutePath)) {
|
|
221
|
-
|
|
269
|
+
candidates.add(absolutePath);
|
|
222
270
|
}
|
|
223
271
|
}
|
|
224
272
|
const angularJsonPaths = findFilesUpward(targetRoot, 'angular.json');
|
|
225
273
|
for (const angularJsonPath of angularJsonPaths) {
|
|
226
|
-
const resolvedFromWorkspace
|
|
227
|
-
|
|
228
|
-
return resolvedFromWorkspace;
|
|
274
|
+
for (const resolvedFromWorkspace of resolveStylesEntriesFromAngularWorkspace(angularJsonPath, targetRoot)) {
|
|
275
|
+
candidates.add(resolvedFromWorkspace);
|
|
229
276
|
}
|
|
230
277
|
}
|
|
231
|
-
return
|
|
278
|
+
return [...candidates];
|
|
232
279
|
}
|
|
233
280
|
export function isDemoTarget(targetRoot) {
|
|
234
281
|
return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
|
|
@@ -287,7 +334,7 @@ function packageJsonUsesIonicAngular(packageJsonPath) {
|
|
|
287
334
|
return false;
|
|
288
335
|
}
|
|
289
336
|
}
|
|
290
|
-
function
|
|
337
|
+
function resolveStylesEntriesFromAngularWorkspace(angularJsonPath, targetRoot) {
|
|
291
338
|
try {
|
|
292
339
|
const angularJson = JSON.parse(readFileSync(angularJsonPath, 'utf8'));
|
|
293
340
|
const workspaceRoot = path.dirname(angularJsonPath);
|
|
@@ -298,6 +345,7 @@ function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
|
|
|
298
345
|
: path.resolve(targetRoot) === path.resolve(workspaceRoot)
|
|
299
346
|
? projects
|
|
300
347
|
: [];
|
|
348
|
+
const matches = [];
|
|
301
349
|
for (const project of candidateProjects) {
|
|
302
350
|
const styles = project.architect?.build?.options?.styles ?? project.targets?.build?.options?.styles ?? [];
|
|
303
351
|
for (const style of styles) {
|
|
@@ -307,15 +355,15 @@ function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
|
|
|
307
355
|
}
|
|
308
356
|
const absolutePath = path.resolve(workspaceRoot, input);
|
|
309
357
|
if (existsSync(absolutePath)) {
|
|
310
|
-
|
|
358
|
+
matches.push(absolutePath);
|
|
311
359
|
}
|
|
312
360
|
}
|
|
313
361
|
}
|
|
362
|
+
return matches;
|
|
314
363
|
}
|
|
315
364
|
catch {
|
|
316
|
-
return
|
|
365
|
+
return [];
|
|
317
366
|
}
|
|
318
|
-
return undefined;
|
|
319
367
|
}
|
|
320
368
|
function projectMatchesTargetRoot(project, workspaceRoot, targetRoot) {
|
|
321
369
|
const resolvedTargetRoot = path.resolve(targetRoot);
|
|
@@ -359,3 +407,48 @@ function looksLikeTailwindV4(range) {
|
|
|
359
407
|
const versionTokenPattern = /(?:^|[<>=~^|\s:])v?(\d+)(?:(?:\.\d+){0,2})/g;
|
|
360
408
|
return [...range.matchAll(versionTokenPattern)].some((match) => Number(match[1]) === 4);
|
|
361
409
|
}
|
|
410
|
+
function buildAliasTargets(tsconfigPath, targetRoot) {
|
|
411
|
+
const aliasTarget = isDemoTarget(targetRoot)
|
|
412
|
+
? toPosixPath(path.relative(LOCAL_REPO_ROOT, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
|
|
413
|
+
: toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
|
|
414
|
+
const wildcardTarget = 'packages/ui/*';
|
|
415
|
+
return {
|
|
416
|
+
aliasTarget,
|
|
417
|
+
wildcardTarget,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function resolveStylesEntrySelection(targetRoot, candidates, preferredStylesEntry) {
|
|
421
|
+
if (preferredStylesEntry) {
|
|
422
|
+
const preferredAbsolutePath = path.resolve(targetRoot, preferredStylesEntry);
|
|
423
|
+
if (candidates.includes(preferredAbsolutePath)) {
|
|
424
|
+
return {
|
|
425
|
+
targetStylesEntry: preferredAbsolutePath,
|
|
426
|
+
targetStylesEntryResolution: 'preferred',
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const trineOwnedCandidates = candidates.filter((candidate) => {
|
|
431
|
+
try {
|
|
432
|
+
return readFileSync(candidate, 'utf8').includes(STYLE_IMPORT_LINE);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (trineOwnedCandidates.length === 1) {
|
|
439
|
+
return {
|
|
440
|
+
targetStylesEntry: trineOwnedCandidates[0],
|
|
441
|
+
targetStylesEntryResolution: 'trine-import',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (candidates.length > 0) {
|
|
445
|
+
return {
|
|
446
|
+
targetStylesEntry: candidates[0],
|
|
447
|
+
targetStylesEntryResolution: 'default',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
targetStylesEntry: undefined,
|
|
452
|
+
targetStylesEntryResolution: 'none',
|
|
453
|
+
};
|
|
454
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
export async function chooseFromList(message, items, options = {}) {
|
|
4
|
+
assertInteractiveTerminal();
|
|
5
|
+
if (items.length === 0) {
|
|
6
|
+
throw new Error('Cannot choose from an empty list.');
|
|
7
|
+
}
|
|
8
|
+
if (items.length === 1) {
|
|
9
|
+
return items[0];
|
|
10
|
+
}
|
|
11
|
+
const renderItem = options.renderItem ?? ((item, index) => `${String(index + 1)}. ${item}`);
|
|
12
|
+
console.log(message);
|
|
13
|
+
for (const [index, item] of items.entries()) {
|
|
14
|
+
console.log(renderItem(item, index));
|
|
15
|
+
}
|
|
16
|
+
const prompt = createPrompt();
|
|
17
|
+
try {
|
|
18
|
+
for (;;) {
|
|
19
|
+
const answer = (await prompt.question(`Choose an option [1-${String(items.length)}]: `)).trim();
|
|
20
|
+
const selectedIndex = Number(answer);
|
|
21
|
+
if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= items.length) {
|
|
22
|
+
return items[selectedIndex - 1];
|
|
23
|
+
}
|
|
24
|
+
console.log(`Enter a number between 1 and ${String(items.length)}.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
prompt.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function confirmAction(message) {
|
|
32
|
+
assertInteractiveTerminal();
|
|
33
|
+
const prompt = createPrompt();
|
|
34
|
+
try {
|
|
35
|
+
const answer = (await prompt.question(`${message} `)).trim().toLowerCase();
|
|
36
|
+
if (answer === '' || answer === 'y' || answer === 'yes') {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (answer === 'n' || answer === 'no') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
console.log('Enter Y, yes, N, or no.');
|
|
43
|
+
return await confirmAction(message);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
prompt.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function assertInteractiveTerminal() {
|
|
50
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
51
|
+
throw new Error('trine init default mode is guided and needs an interactive terminal. Re-run with --yes for non-interactive mode.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function createPrompt() {
|
|
55
|
+
return createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout,
|
|
58
|
+
});
|
|
59
|
+
}
|