@shokirovr16/frontend-library 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin/cmfrt.js +69 -0
- package/package.json +47 -0
- package/src/auth/README.md +193 -0
- package/src/auth/core/AuthEngine.js +623 -0
- package/src/auth/core/OidcClient.js +79 -0
- package/src/auth/core/OidcDiscovery.js +17 -0
- package/src/auth/core/Pkce.js +18 -0
- package/src/auth/events/AuthEventBus.js +22 -0
- package/src/auth/http/authFetch.js +32 -0
- package/src/auth/http/createAuthHttpClient.js +42 -0
- package/src/auth/index.js +90 -0
- package/src/auth/permissions/ClaimsNormalizer.js +69 -0
- package/src/auth/permissions/permissions.js +26 -0
- package/src/auth/react/AuthProvider.js +34 -0
- package/src/auth/react/guards/RequireAuth.js +35 -0
- package/src/auth/react/guards/RequirePermission.js +16 -0
- package/src/auth/react/guards/withAuthGuard.js +12 -0
- package/src/auth/react/hooks/useRequireAuth.js +24 -0
- package/src/auth/react/index.js +6 -0
- package/src/auth/react/useAuth.js +29 -0
- package/src/auth/silent/silentCallback.js +42 -0
- package/src/auth/singleton.js +22 -0
- package/src/auth/storage/InMemoryTokenStore.js +56 -0
- package/src/auth/storage/TransactionStore.js +51 -0
- package/src/auth/sync/BroadcastChannelSync.js +29 -0
- package/src/auth/tenancy/TenantResolver.js +39 -0
- package/src/auth/types.js +113 -0
- package/src/auth/utils/base64url.js +15 -0
- package/src/auth/utils/jwt.js +26 -0
- package/src/auth/utils/random.js +13 -0
- package/src/auth/utils/url.js +27 -0
- package/src/commands/add.js +80 -0
- package/src/commands/init.js +113 -0
- package/src/commands/list.js +92 -0
- package/src/commands/remove.js +150 -0
- package/src/commands/status.js +96 -0
- package/src/commands/theme.js +47 -0
- package/src/commands/uninstall.js +198 -0
- package/src/commands/update.js +151 -0
- package/src/lib/config.js +55 -0
- package/src/lib/fs.js +13 -0
- package/src/lib/packageManager.js +30 -0
- package/src/lib/paths.js +14 -0
- package/src/lib/registry.js +11 -0
- package/src/lib/styles.js +223 -0
- package/src/lib/targets.js +15 -0
- package/src/lib/theme.js +102 -0
- package/templates/docs/cmfrt-doc.md +82 -0
- package/templates/lib/utils.js +6 -0
- package/templates/registry.json +42 -0
- package/templates/styles/theme.cjs +832 -0
- package/templates/styles/type-utilities.css +136 -0
- package/templates/styles/type-utility-classes.css +138 -0
- package/templates/styles/variables.css +1560 -0
- package/templates/styles/variables.json +6870 -0
- package/templates/ui/button.jsx +117 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { configExists, loadConfig } from '../lib/config.js';
|
|
4
|
+
import { readRegistry } from '../lib/registry.js';
|
|
5
|
+
|
|
6
|
+
const { green, yellow, red } = pc;
|
|
7
|
+
|
|
8
|
+
function coerceVersion(version) {
|
|
9
|
+
const coerced = semver.coerce(String(version ?? ''));
|
|
10
|
+
return coerced?.version ?? '0.0.0';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isUpdateAvailable(installedVersion, latestVersion) {
|
|
14
|
+
const installed = coerceVersion(installedVersion);
|
|
15
|
+
const latest = coerceVersion(latestVersion);
|
|
16
|
+
return semver.gt(latest, installed);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function statusCommand(options = {}) {
|
|
20
|
+
if (!(await configExists())) {
|
|
21
|
+
console.error(red('cmfrt.json is missing. Run cmfrt init first.'));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
const registry = await readRegistry();
|
|
29
|
+
const installed = config.installed ?? {};
|
|
30
|
+
|
|
31
|
+
const items = Object.entries(installed).map(([name, entry]) => {
|
|
32
|
+
const installedVersion = entry?.version ?? '0.0.0';
|
|
33
|
+
const registryEntry = registry?.[name];
|
|
34
|
+
|
|
35
|
+
if (!registryEntry) {
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
installed: installedVersion,
|
|
39
|
+
latest: null,
|
|
40
|
+
status: 'missing',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const latestVersion = registryEntry?.version ?? '0.0.0';
|
|
45
|
+
const status = isUpdateAvailable(installedVersion, latestVersion) ? 'update' : 'latest';
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name,
|
|
49
|
+
installed: installedVersion,
|
|
50
|
+
latest: latestVersion,
|
|
51
|
+
status,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const updatesCount = items.filter((item) => item.status === 'update').length;
|
|
56
|
+
const missingCount = items.filter((item) => item.status === 'missing').length;
|
|
57
|
+
|
|
58
|
+
const filteredItems = options.onlyUpdates
|
|
59
|
+
? items.filter((item) => item.status === 'update')
|
|
60
|
+
: items;
|
|
61
|
+
|
|
62
|
+
if (options.json) {
|
|
63
|
+
const payload = {
|
|
64
|
+
installedCount: items.length,
|
|
65
|
+
updatesCount,
|
|
66
|
+
missingCount,
|
|
67
|
+
items: filteredItems.map(({ name, installed: installedVersion, latest, status }) => ({
|
|
68
|
+
name,
|
|
69
|
+
installed: installedVersion,
|
|
70
|
+
latest,
|
|
71
|
+
status,
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const item of filteredItems) {
|
|
80
|
+
if (item.status === 'missing') {
|
|
81
|
+
console.log(red(`${item.name} ${item.installed} (missing in registry)`));
|
|
82
|
+
} else if (item.status === 'update') {
|
|
83
|
+
console.log(yellow(`${item.name} ${item.installed} -> ${item.latest} (update available)`));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(green(`${item.name} ${item.installed} (latest)`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
`Summary: ${items.length} installed, ${updatesCount} updates available, ${missingCount} missing`
|
|
91
|
+
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(red(error?.message ?? 'Failed to load cmfrt status.'));
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { configExists, loadConfig, writeConfig } from '../lib/config.js';
|
|
3
|
+
import { applyThemeMode, getAvailableModes, resolveMode } from '../lib/theme.js';
|
|
4
|
+
|
|
5
|
+
const { green, yellow, red } = pc;
|
|
6
|
+
|
|
7
|
+
export default async function themeCommand(options = {}) {
|
|
8
|
+
if (!(await configExists())) {
|
|
9
|
+
console.error(red('cmfrt.json is missing. Run cmfrt init first.'));
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const config = await loadConfig();
|
|
15
|
+
const modes = await getAvailableModes(config);
|
|
16
|
+
|
|
17
|
+
if (options.list) {
|
|
18
|
+
if (!modes.length) {
|
|
19
|
+
console.log(yellow('No modes found.'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const mode of modes) {
|
|
23
|
+
console.log(`${mode.name}`);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.set) {
|
|
29
|
+
const selected = resolveMode(modes, options.set);
|
|
30
|
+
if (!selected) {
|
|
31
|
+
console.error(red('Mode not found. Use `cmfrt theme --list` to see available modes.'));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await applyThemeMode(selected.safe);
|
|
37
|
+
if (result.updated) {
|
|
38
|
+
await writeConfig({ ...config, themeMode: selected.name });
|
|
39
|
+
console.log(green(`Theme mode set: ${selected.name}`));
|
|
40
|
+
} else {
|
|
41
|
+
console.log(yellow('Theme mode unchanged.'));
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(yellow('Use `cmfrt theme --list` or `cmfrt theme --set <mode>`'));
|
|
47
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { configExists, loadConfig, writeConfig } from '../lib/config.js';
|
|
6
|
+
import { readRegistry } from '../lib/registry.js';
|
|
7
|
+
import { pathExists, removePath } from '../lib/fs.js';
|
|
8
|
+
import { componentsDir, projectRoot } from '../lib/paths.js';
|
|
9
|
+
import { resolveTargetRoot } from '../lib/targets.js';
|
|
10
|
+
|
|
11
|
+
const { green, yellow, red, cyan } = pc;
|
|
12
|
+
|
|
13
|
+
async function confirmRemoval(message) {
|
|
14
|
+
const response = await prompts({
|
|
15
|
+
type: 'confirm',
|
|
16
|
+
name: 'confirm',
|
|
17
|
+
message,
|
|
18
|
+
initial: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return Boolean(response.confirm);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveComponentsRoot(config) {
|
|
25
|
+
return config.componentsPath
|
|
26
|
+
? path.resolve(projectRoot(), config.componentsPath)
|
|
27
|
+
: componentsDir();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function shouldSkipUtils(targetPath, config) {
|
|
31
|
+
if (!config.utilsPath) return false;
|
|
32
|
+
const utilsPath = path.resolve(projectRoot(), config.utilsPath);
|
|
33
|
+
return path.resolve(targetPath) === utilsPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function removeInstalledFiles(componentName, installedEntry, config, registry) {
|
|
37
|
+
const registryEntry = registry?.[componentName];
|
|
38
|
+
const root = registryEntry
|
|
39
|
+
? resolveTargetRoot(registryEntry, config)
|
|
40
|
+
: resolveComponentsRoot(config);
|
|
41
|
+
|
|
42
|
+
for (const file of installedEntry.files ?? []) {
|
|
43
|
+
const destination = path.resolve(root, file);
|
|
44
|
+
|
|
45
|
+
if (shouldSkipUtils(destination, config)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (await pathExists(destination)) {
|
|
50
|
+
await removePath(destination);
|
|
51
|
+
const relativePath = path.relative(projectRoot(), destination) || destination;
|
|
52
|
+
console.log(`Deleted: ${relativePath}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function pruneDependencies(removedNames, updatedInstalled, registry) {
|
|
58
|
+
for (const componentName of removedNames) {
|
|
59
|
+
const entry = registry?.[componentName];
|
|
60
|
+
if (!entry) {
|
|
61
|
+
console.log(yellow(`Registry entry for "${componentName}" not found. Skipping prune.`));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dependencies = Array.isArray(entry.dependencies) ? entry.dependencies : [];
|
|
66
|
+
const otherInstalled = Object.keys(updatedInstalled);
|
|
67
|
+
|
|
68
|
+
for (const dependency of dependencies) {
|
|
69
|
+
let usedElsewhere = false;
|
|
70
|
+
|
|
71
|
+
for (const otherName of otherInstalled) {
|
|
72
|
+
const otherEntry = registry?.[otherName];
|
|
73
|
+
if (!otherEntry) {
|
|
74
|
+
usedElsewhere = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
const otherDeps = Array.isArray(otherEntry.dependencies) ? otherEntry.dependencies : [];
|
|
78
|
+
if (otherDeps.includes(dependency)) {
|
|
79
|
+
usedElsewhere = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (usedElsewhere) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
console.log(cyan(`Uninstalling: ${dependency}`));
|
|
90
|
+
await execa('npm', ['uninstall', dependency]);
|
|
91
|
+
console.log(`Uninstalled: ${dependency}`);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(red(`Failed to uninstall ${dependency}.`));
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default async function uninstallCommand(componentName, options = {}) {
|
|
101
|
+
if (!(await configExists())) {
|
|
102
|
+
console.error(red('cmfrt.json is missing. Run cmfrt init first.'));
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const config = await loadConfig();
|
|
108
|
+
const installed = config.installed ?? {};
|
|
109
|
+
const installedNames = Object.keys(installed);
|
|
110
|
+
|
|
111
|
+
if (options.all) {
|
|
112
|
+
if (!installedNames.length) {
|
|
113
|
+
console.log(yellow('No installed components to remove.'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!options.yes) {
|
|
118
|
+
const confirmed = await confirmRemoval('Remove all installed components and styles?');
|
|
119
|
+
if (!confirmed) {
|
|
120
|
+
console.log(yellow('Aborted.'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let registry = null;
|
|
126
|
+
try {
|
|
127
|
+
registry = await readRegistry();
|
|
128
|
+
} catch {
|
|
129
|
+
registry = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('Removing all installed components...');
|
|
133
|
+
for (const name of installedNames) {
|
|
134
|
+
await removeInstalledFiles(name, installed[name], config, registry);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const updatedConfig = { ...config, installed: {} };
|
|
138
|
+
await writeConfig(updatedConfig);
|
|
139
|
+
|
|
140
|
+
if (options.prune) {
|
|
141
|
+
if (!registry) {
|
|
142
|
+
console.error(red('Failed to load registry for pruning dependencies.'));
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
} else {
|
|
145
|
+
await pruneDependencies(installedNames, {}, registry);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(green('All components removed successfully.'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!componentName) {
|
|
154
|
+
console.error(red('Please provide a component name or use --all.'));
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const installedEntry = installed?.[componentName];
|
|
160
|
+
if (!installedEntry) {
|
|
161
|
+
console.log(yellow(`Component "${componentName}" is not installed.`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!options.yes) {
|
|
166
|
+
const confirmed = await confirmRemoval(`Are you sure you want to remove ${componentName}?`);
|
|
167
|
+
if (!confirmed) {
|
|
168
|
+
console.log(yellow('Aborted.'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let registry = null;
|
|
174
|
+
try {
|
|
175
|
+
registry = await readRegistry();
|
|
176
|
+
} catch {
|
|
177
|
+
registry = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`Removing: ${componentName}`);
|
|
181
|
+
await removeInstalledFiles(componentName, installedEntry, config, registry);
|
|
182
|
+
|
|
183
|
+
const updatedInstalled = { ...installed };
|
|
184
|
+
delete updatedInstalled[componentName];
|
|
185
|
+
const updatedConfig = { ...config, installed: updatedInstalled };
|
|
186
|
+
await writeConfig(updatedConfig);
|
|
187
|
+
|
|
188
|
+
if (options.prune) {
|
|
189
|
+
if (!registry) {
|
|
190
|
+
console.error(red('Failed to load registry for pruning dependencies.'));
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
} else {
|
|
193
|
+
await pruneDependencies([componentName], updatedInstalled, registry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(green('Component removed successfully.'));
|
|
198
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { copyFile, ensureDirectory, pathExists } from '../lib/fs.js';
|
|
4
|
+
import { configExists, loadConfig, writeConfig } from '../lib/config.js';
|
|
5
|
+
import { readRegistry } from '../lib/registry.js';
|
|
6
|
+
import { templatePath } from '../lib/paths.js';
|
|
7
|
+
import { resolveTargetRoot } from '../lib/targets.js';
|
|
8
|
+
import { ensureStylesInstalled } from '../lib/styles.js';
|
|
9
|
+
|
|
10
|
+
const { cyan, green, yellow, red } = pc;
|
|
11
|
+
|
|
12
|
+
function parseVersion(version) {
|
|
13
|
+
if (!version) return [0, 0, 0];
|
|
14
|
+
return String(version)
|
|
15
|
+
.split('.')
|
|
16
|
+
.map((part) => Number.parseInt(part, 10))
|
|
17
|
+
.map((num) => (Number.isNaN(num) ? 0 : num));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compareSemver(a, b) {
|
|
21
|
+
const left = parseVersion(a);
|
|
22
|
+
const right = parseVersion(b);
|
|
23
|
+
const length = Math.max(left.length, right.length, 3);
|
|
24
|
+
for (let i = 0; i < length; i += 1) {
|
|
25
|
+
const l = left[i] ?? 0;
|
|
26
|
+
const r = right[i] ?? 0;
|
|
27
|
+
if (l > r) return 1;
|
|
28
|
+
if (l < r) return -1;
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function updateComponent(name, entry, config) {
|
|
34
|
+
if (!entry) {
|
|
35
|
+
console.log(yellow(`Component "${name}" is not found in the registry.`));
|
|
36
|
+
return { updated: false, config };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const componentsRoot = resolveTargetRoot(entry, config);
|
|
40
|
+
let installed = config.installed?.[name];
|
|
41
|
+
if (!installed) {
|
|
42
|
+
const detectedFiles = [];
|
|
43
|
+
for (const file of entry.files ?? []) {
|
|
44
|
+
const destination = path.resolve(componentsRoot, file.target);
|
|
45
|
+
if (await pathExists(destination)) {
|
|
46
|
+
detectedFiles.push(path.relative(componentsRoot, destination));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (detectedFiles.length > 0) {
|
|
51
|
+
installed = {
|
|
52
|
+
version: '0.0.0',
|
|
53
|
+
files: detectedFiles,
|
|
54
|
+
installedAt: null,
|
|
55
|
+
};
|
|
56
|
+
console.log(yellow(`Component "${name}" was not tracked; treating existing files as installed.`));
|
|
57
|
+
} else {
|
|
58
|
+
console.log(yellow(`Component "${name}" is not installed.`));
|
|
59
|
+
return { updated: false, config };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const installedVersion = installed.version ?? '0.0.0';
|
|
64
|
+
const registryVersion = entry.version ?? '0.0.0';
|
|
65
|
+
|
|
66
|
+
if (compareSemver(installedVersion, registryVersion) >= 0) {
|
|
67
|
+
console.log(green(`${name} is already up to date.`));
|
|
68
|
+
return { updated: false, config };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(cyan(`Updating component ${name}...`));
|
|
72
|
+
|
|
73
|
+
const updatedFiles = new Set(installed.files ?? []);
|
|
74
|
+
|
|
75
|
+
for (const file of entry.files ?? []) {
|
|
76
|
+
const source = templatePath(file.source);
|
|
77
|
+
const destination = path.resolve(componentsRoot, file.target);
|
|
78
|
+
await ensureDirectory(path.dirname(destination));
|
|
79
|
+
|
|
80
|
+
await copyFile(source, destination, { overwrite: true });
|
|
81
|
+
updatedFiles.add(path.relative(componentsRoot, destination));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const updatedConfig = {
|
|
85
|
+
...config,
|
|
86
|
+
installed: {
|
|
87
|
+
...config.installed,
|
|
88
|
+
[name]: {
|
|
89
|
+
...installed,
|
|
90
|
+
version: registryVersion,
|
|
91
|
+
files: Array.from(updatedFiles),
|
|
92
|
+
installedAt: installed.installedAt ?? new Date().toISOString(),
|
|
93
|
+
updatedAt: new Date().toISOString(),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return { updated: true, config: updatedConfig };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default async function updateCommand(componentName, options = {}) {
|
|
102
|
+
if (!(await configExists())) {
|
|
103
|
+
console.error(red('cmfrt.json is missing. Run cmfrt init first.'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const config = await loadConfig();
|
|
108
|
+
const registry = await readRegistry();
|
|
109
|
+
let currentConfig = config;
|
|
110
|
+
let didUpdate = false;
|
|
111
|
+
|
|
112
|
+
const stylesResult = await ensureStylesInstalled(registry, currentConfig);
|
|
113
|
+
currentConfig = stylesResult.config;
|
|
114
|
+
if (stylesResult.updated) {
|
|
115
|
+
didUpdate = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.all) {
|
|
119
|
+
const installedNames = Object.keys(config.installed ?? {});
|
|
120
|
+
|
|
121
|
+
for (const name of installedNames) {
|
|
122
|
+
if (name === 'styles') continue;
|
|
123
|
+
const result = await updateComponent(name, registry[name], currentConfig);
|
|
124
|
+
currentConfig = result.config;
|
|
125
|
+
if (result.updated) {
|
|
126
|
+
didUpdate = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (didUpdate) {
|
|
131
|
+
await writeConfig(currentConfig);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!componentName) {
|
|
138
|
+
console.error(red('Please provide a component name or use --all.'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = await updateComponent(componentName, registry[componentName], currentConfig);
|
|
143
|
+
currentConfig = result.config;
|
|
144
|
+
if (result.updated) {
|
|
145
|
+
didUpdate = true;
|
|
146
|
+
}
|
|
147
|
+
if (didUpdate) {
|
|
148
|
+
await writeConfig(currentConfig);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { pathExists, readJson, writeJson } from './fs.js';
|
|
2
|
+
import { configFilePath } from './paths.js';
|
|
3
|
+
|
|
4
|
+
const defaultConfig = {
|
|
5
|
+
componentsPath: 'src/components/ui',
|
|
6
|
+
stylesPath: 'src/styles',
|
|
7
|
+
utilsPath: 'src/lib/utils.js',
|
|
8
|
+
themeMode: null,
|
|
9
|
+
installed: {},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const createDefaultConfig = () => ({ ...defaultConfig });
|
|
13
|
+
|
|
14
|
+
export const configExists = () => pathExists(configFilePath());
|
|
15
|
+
|
|
16
|
+
export async function loadConfig() {
|
|
17
|
+
const path = configFilePath();
|
|
18
|
+
if (!(await pathExists(path))) {
|
|
19
|
+
throw new Error('cmfrt.json is missing. Run cmfrt init first.');
|
|
20
|
+
}
|
|
21
|
+
const raw = await readJson(path);
|
|
22
|
+
const normalized = { ...defaultConfig, ...raw };
|
|
23
|
+
let needsWrite = false;
|
|
24
|
+
|
|
25
|
+
if (!normalized.installed) {
|
|
26
|
+
if (Array.isArray(raw.components)) {
|
|
27
|
+
const installed = {};
|
|
28
|
+
for (const name of raw.components) {
|
|
29
|
+
installed[name] = {
|
|
30
|
+
version: '0.0.0',
|
|
31
|
+
files: [],
|
|
32
|
+
installedAt: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
normalized.installed = installed;
|
|
36
|
+
} else {
|
|
37
|
+
normalized.installed = {};
|
|
38
|
+
}
|
|
39
|
+
needsWrite = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { components, ...cleaned } = normalized;
|
|
43
|
+
|
|
44
|
+
if (components) {
|
|
45
|
+
needsWrite = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (needsWrite) {
|
|
49
|
+
await writeJson(path, cleaned, { spaces: 2 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return cleaned;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const writeConfig = (config) => writeJson(configFilePath(), config);
|
package/src/lib/fs.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
|
|
3
|
+
export const readJson = (filePath) => fs.readJson(filePath);
|
|
4
|
+
export const writeJson = (filePath, data, options = {}) => fs.writeJson(filePath, data, { spaces: 2, ...options });
|
|
5
|
+
export const ensureDirectory = (dirPath) => fs.ensureDir(dirPath);
|
|
6
|
+
export const copyFile = (source, destination, options = {}) => {
|
|
7
|
+
const { overwrite = true, ...rest } = options;
|
|
8
|
+
return fs.copy(source, destination, { overwrite, ...rest });
|
|
9
|
+
};
|
|
10
|
+
export const pathExists = (target) => fs.pathExists(target);
|
|
11
|
+
export const readFile = (filePath, options = {}) => fs.readFile(filePath, options);
|
|
12
|
+
export const writeFile = (filePath, data, options = {}) => fs.writeFile(filePath, data, options);
|
|
13
|
+
export const removePath = (target) => fs.remove(target);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
|
|
5
|
+
const packageManagerMap = [
|
|
6
|
+
{ lockFile: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
7
|
+
{ lockFile: 'yarn.lock', manager: 'yarn' },
|
|
8
|
+
{ lockFile: 'package-lock.json', manager: 'npm' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export async function detectPackageManager(cwd = process.cwd()) {
|
|
12
|
+
for (const entry of packageManagerMap) {
|
|
13
|
+
if (await fs.pathExists(path.resolve(cwd, entry.lockFile))) {
|
|
14
|
+
return entry.manager;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return 'npm';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function installDependencies(dependencies, cwd = process.cwd()) {
|
|
21
|
+
if (!dependencies || dependencies.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const manager = await detectPackageManager(cwd);
|
|
26
|
+
const baseCommand = manager === 'yarn' || manager === 'pnpm' ? 'add' : 'install';
|
|
27
|
+
const args = [baseCommand, ...dependencies];
|
|
28
|
+
|
|
29
|
+
await execa(manager, args, { cwd, stdio: 'inherit' });
|
|
30
|
+
}
|
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
const cliRoot = path.resolve(__dirname, '../../');
|
|
7
|
+
const templatesRoot = path.resolve(cliRoot, 'templates');
|
|
8
|
+
|
|
9
|
+
export const projectRoot = () => process.cwd();
|
|
10
|
+
export const configFilePath = () => path.resolve(projectRoot(), 'cmfrt.json');
|
|
11
|
+
export const componentsDir = () => path.resolve(projectRoot(), 'src/components/ui');
|
|
12
|
+
export const libDir = () => path.resolve(projectRoot(), 'src/lib');
|
|
13
|
+
export const templatesDir = () => templatesRoot;
|
|
14
|
+
export const templatePath = (...segments) => path.resolve(templatesRoot, ...segments);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readJson } from './fs.js';
|
|
2
|
+
import { templatePath } from './paths.js';
|
|
3
|
+
|
|
4
|
+
const registryPath = templatePath('registry.json');
|
|
5
|
+
|
|
6
|
+
export const readRegistry = () => readJson(registryPath);
|
|
7
|
+
|
|
8
|
+
export async function getComponentEntry(name) {
|
|
9
|
+
const registry = await readRegistry();
|
|
10
|
+
return registry[name];
|
|
11
|
+
}
|