slicejs-cli 3.5.1 → 3.6.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 +34 -15
- package/client.js +67 -20
- package/commands/doctor/doctor.js +69 -3
- package/commands/getComponent/getComponent.js +33 -25
- package/commands/init/init.js +106 -28
- package/commands/utils/PackageManager.js +148 -0
- package/commands/utils/VersionChecker.js +6 -4
- package/commands/utils/sliceScripts.js +21 -0
- package/commands/utils/updateManager.js +54 -35
- package/package.json +12 -1
- package/post.js +8 -16
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/pull_request_template.md +0 -22
- package/.github/workflows/ci.yml +0 -43
- package/AGENTS.md +0 -247
- package/CODE_OF_CONDUCT.md +0 -126
- package/ECOSYSTEM.md +0 -9
- package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +0 -182
- package/playwright.config.js +0 -51
- package/tests/build-command-integration.test.js +0 -87
- package/tests/build-production-e2e.test.js +0 -140
- package/tests/builder-edge-cases.test.js +0 -322
- package/tests/bundle-generate-e2e.test.js +0 -115
- package/tests/bundle-generator.test.js +0 -691
- package/tests/bundle-v2-register-output.test.js +0 -470
- package/tests/bundling-dependency-edges.test.js +0 -127
- package/tests/bundling-imports-unit.test.js +0 -267
- package/tests/client-launcher-contract.test.js +0 -211
- package/tests/client-update-flow-contract.test.js +0 -272
- package/tests/commands-component-crud.test.js +0 -102
- package/tests/commands-doctor.test.js +0 -80
- package/tests/commands-version-checker.test.js +0 -37
- package/tests/component-registry-parse.test.js +0 -34
- package/tests/dependency-analyzer.test.js +0 -24
- package/tests/e2e/bundles.spec.js +0 -91
- package/tests/e2e/dependency-scenarios.spec.js +0 -56
- package/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
- package/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
- package/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
- package/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
- package/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
- package/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
- package/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
- package/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
- package/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
- package/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
- package/tests/e2e/fixtures/components/registry.json +0 -12
- package/tests/e2e/fixtures/vendor-components.mjs +0 -65
- package/tests/e2e/navigation.spec.js +0 -44
- package/tests/e2e/render.spec.js +0 -34
- package/tests/e2e/serve.mjs +0 -264
- package/tests/e2e/shared-deps.spec.js +0 -61
- package/tests/e2e/unminified.spec.js +0 -33
- package/tests/e2e-serve.test.js +0 -148
- package/tests/fixtures/components.js +0 -8
- package/tests/fixtures/sliceConfig.json +0 -74
- package/tests/getcomponent.test.js +0 -407
- package/tests/helpers/setup.js +0 -102
- package/tests/init-command-contract.test.js +0 -46
- package/tests/local-cli-delegation.test.js +0 -81
- package/tests/path-helper.test.js +0 -206
- package/tests/perf-budget.test.js +0 -86
- package/tests/postinstall-command.test.js +0 -72
- package/tests/types-breakage.test.js +0 -491
- package/tests/types-generator-errors.test.js +0 -361
- package/tests/types-generator.test.js +0 -346
- package/tests/update-manager-notifications.test.js +0 -88
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ This repository contains the Slice.js CLI (`slicejs-cli`), the command-line tool
|
|
|
28
28
|
cd slicejs-cli
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
2. **Install dependencies**
|
|
31
|
+
2. **Install dependencies** (the repo pins pnpm via the `packageManager` field)
|
|
32
32
|
```bash
|
|
33
|
-
|
|
33
|
+
pnpm install --frozen-lockfile
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
3. **Test changes locally**
|
|
@@ -50,18 +50,24 @@ This repository contains the Slice.js CLI (`slicejs-cli`), the command-line tool
|
|
|
50
50
|
|
|
51
51
|
## Installation (for users)
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
For a **new project** you don't install anything manually — `slice init` (see Quick
|
|
54
|
+
start below) installs the CLI locally in the project it creates.
|
|
55
|
+
|
|
56
|
+
### Local in an existing project (Recommended)
|
|
54
57
|
|
|
55
58
|
```bash
|
|
56
|
-
npm install slicejs-cli --save-dev
|
|
59
|
+
npm install slicejs-cli --save-dev # or: pnpm add -D slicejs-cli
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
### Global (
|
|
62
|
+
### Global launcher (optional)
|
|
60
63
|
|
|
61
64
|
```bash
|
|
62
|
-
npm install -g slicejs-cli
|
|
65
|
+
npm install -g slicejs-cli # or: pnpm add -g slicejs-cli
|
|
63
66
|
```
|
|
64
67
|
|
|
68
|
+
The launcher delegates to the nearest project-local `node_modules/slicejs-cli`,
|
|
69
|
+
so each project keeps its pinned CLI version.
|
|
70
|
+
|
|
65
71
|
## Main commands
|
|
66
72
|
|
|
67
73
|
| Command | Description |
|
|
@@ -106,21 +112,34 @@ npx slicejs-cli postinstall
|
|
|
106
112
|
|
|
107
113
|
## Quick start
|
|
108
114
|
|
|
115
|
+
`slice init` creates the project folder itself — no `mkdir` or `npm init` needed.
|
|
116
|
+
Everything (package.json, node_modules, lockfile, src/, api/) lives inside the new folder.
|
|
117
|
+
|
|
109
118
|
```bash
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
# With npm
|
|
120
|
+
npx slicejs-cli init
|
|
121
|
+
cd my-app
|
|
122
|
+
npm run dev
|
|
123
|
+
```
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
```bash
|
|
126
|
+
# With pnpm
|
|
127
|
+
pnpm dlx slicejs-cli init
|
|
128
|
+
cd my-app
|
|
129
|
+
pnpm run dev
|
|
130
|
+
```
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
npx slicejs-cli init
|
|
132
|
+
Non-interactive (for scripts/CI):
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
npx slicejs-cli
|
|
134
|
+
```bash
|
|
135
|
+
npx slicejs-cli init -y my-app --pm pnpm
|
|
122
136
|
```
|
|
123
137
|
|
|
138
|
+
init pins the chosen package manager in the `packageManager` field, installs
|
|
139
|
+
`slicejs-web-framework` as a dependency and `slicejs-cli` as a devDependency of
|
|
140
|
+
the new project. Versions are never hard-pinned at install time, so hardened pnpm
|
|
141
|
+
setups (`minimumReleaseAge` quarantine, `ignore-scripts`) work out of the box.
|
|
142
|
+
|
|
124
143
|
## Tests
|
|
125
144
|
|
|
126
145
|
The CLI uses Node.js native test runner:
|
package/client.js
CHANGED
|
@@ -27,6 +27,13 @@ import {
|
|
|
27
27
|
resolveLocalCliCandidate,
|
|
28
28
|
shouldDelegateToLocalCli
|
|
29
29
|
} from './commands/utils/LocalCliDelegation.js';
|
|
30
|
+
import {
|
|
31
|
+
detectPackageManager,
|
|
32
|
+
getAvailablePackageManagers,
|
|
33
|
+
isPackageManagerAvailable,
|
|
34
|
+
SUPPORTED_PACKAGE_MANAGERS
|
|
35
|
+
} from './commands/utils/PackageManager.js';
|
|
36
|
+
import { SLICE_SCRIPTS } from './commands/utils/sliceScripts.js';
|
|
30
37
|
|
|
31
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
39
|
|
|
@@ -96,6 +103,7 @@ sliceClient
|
|
|
96
103
|
.command("init")
|
|
97
104
|
.description("Initialize a new Slice.js project")
|
|
98
105
|
.option("-y, --yes [name]", "Skip prompts and initialize with project name")
|
|
106
|
+
.option("--pm <packageManager>", "Package manager to use (npm, pnpm or yarn). Auto-detected when omitted")
|
|
99
107
|
.action(async (options) => {
|
|
100
108
|
let projectName = 'my-slice-app';
|
|
101
109
|
if (options.yes) {
|
|
@@ -123,16 +131,62 @@ sliceClient
|
|
|
123
131
|
projectName = answers.projectName;
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
// Resolve the package manager: --pm flag → detection → interactive prompt.
|
|
135
|
+
// Detection here has no project root yet (the folder is new), so it relies on
|
|
136
|
+
// npm_config_user_agent (npx / pnpm dlx / PM scripts) or a single available binary.
|
|
137
|
+
let packageManager = options.pm;
|
|
138
|
+
if (packageManager && !SUPPORTED_PACKAGE_MANAGERS.includes(packageManager)) {
|
|
139
|
+
Print.error(`Unsupported package manager "${packageManager}". Use one of: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (packageManager && !isPackageManagerAvailable(packageManager)) {
|
|
143
|
+
Print.error(`Package manager "${packageManager}" is not available on this system`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!packageManager) {
|
|
147
|
+
const detected = detectPackageManager(null);
|
|
148
|
+
if (detected) {
|
|
149
|
+
packageManager = detected.name;
|
|
150
|
+
} else if (!options.yes) {
|
|
151
|
+
const available = getAvailablePackageManagers();
|
|
152
|
+
if (available.length === 0) {
|
|
153
|
+
Print.error('No package manager found (npm, pnpm or yarn). Install one and retry.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const { pm } = await inquirer.prompt([
|
|
157
|
+
{
|
|
158
|
+
type: 'list',
|
|
159
|
+
name: 'pm',
|
|
160
|
+
message: 'Which package manager do you want to use?',
|
|
161
|
+
choices: available,
|
|
162
|
+
default: available.includes('pnpm') ? 'pnpm' : available[0]
|
|
163
|
+
}
|
|
164
|
+
]);
|
|
165
|
+
packageManager = pm;
|
|
166
|
+
} else {
|
|
167
|
+
const available = getAvailablePackageManagers();
|
|
168
|
+
packageManager = available.includes('npm') ? 'npm' : available[0];
|
|
169
|
+
if (!packageManager) {
|
|
170
|
+
Print.error('No package manager found (npm, pnpm or yarn). Install one and retry.');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
126
176
|
const projectDir = path.resolve(projectName);
|
|
127
177
|
|
|
128
178
|
if (fs.existsSync(projectDir)) {
|
|
129
179
|
const contents = fs.readdirSync(projectDir);
|
|
130
180
|
if (contents.length > 0) {
|
|
181
|
+
if (options.yes) {
|
|
182
|
+
Print.error(`Directory "${projectName}" already exists and is not empty. Aborting.`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
131
185
|
const { overwrite } = await inquirer.prompt([
|
|
132
186
|
{
|
|
133
187
|
type: 'confirm',
|
|
134
188
|
name: 'overwrite',
|
|
135
|
-
message: `Directory "${
|
|
189
|
+
message: `Directory "${projectName}" already exists and is not empty. Continue?`,
|
|
136
190
|
default: false
|
|
137
191
|
}
|
|
138
192
|
]);
|
|
@@ -148,9 +202,8 @@ sliceClient
|
|
|
148
202
|
process.chdir(projectDir);
|
|
149
203
|
process.env.INIT_CWD = projectDir;
|
|
150
204
|
|
|
151
|
-
await runWithVersionCheck(() => {
|
|
152
|
-
initializeProject();
|
|
153
|
-
return Promise.resolve();
|
|
205
|
+
await runWithVersionCheck(async () => {
|
|
206
|
+
await initializeProject({ packageManager });
|
|
154
207
|
});
|
|
155
208
|
});
|
|
156
209
|
|
|
@@ -353,7 +406,7 @@ componentCommand
|
|
|
353
406
|
category = categoryAnswer.category;
|
|
354
407
|
}
|
|
355
408
|
|
|
356
|
-
const config =
|
|
409
|
+
const config = sharedLoadConfigSync(import.meta.url);
|
|
357
410
|
if (!config) {
|
|
358
411
|
Print.error("Could not load configuration");
|
|
359
412
|
return;
|
|
@@ -550,6 +603,7 @@ sliceClient
|
|
|
550
603
|
.option("-y, --yes", "Skip confirmation and update all packages automatically")
|
|
551
604
|
.option("--cli", "Update only the Slice.js CLI")
|
|
552
605
|
.option("-f, --framework", "Update only the Slice.js Framework")
|
|
606
|
+
.option("--update-api", "Also overwrite api/index.js with the framework version (never done by default; creates a .bak backup)")
|
|
553
607
|
.action(async (options) => {
|
|
554
608
|
await updateManager.checkAndPromptUpdates(options);
|
|
555
609
|
});
|
|
@@ -570,30 +624,23 @@ sliceClient
|
|
|
570
624
|
.command("postinstall")
|
|
571
625
|
.description("Configure npm scripts in package.json (alternative to postinstall for --ignore-scripts users)")
|
|
572
626
|
.action(() => {
|
|
573
|
-
|
|
627
|
+
// npm sets npm_config_global; pnpm global installs are detected via PNPM_HOME.
|
|
628
|
+
const pnpmHome = process.env.PNPM_HOME;
|
|
629
|
+
const cliEntryPath = fileURLToPath(import.meta.url);
|
|
630
|
+
const isGlobal = process.env.npm_config_global === 'true'
|
|
631
|
+
|| (pnpmHome && cliEntryPath.startsWith(pnpmHome));
|
|
574
632
|
if (isGlobal) {
|
|
575
633
|
console.log('⚠️ Global installation of slicejs-cli detected.');
|
|
576
634
|
console.log(' We strongly recommend using a local installation to avoid version mismatches.');
|
|
577
|
-
console.log(
|
|
635
|
+
console.log(` Uninstall global: ${pnpmHome ? 'pnpm remove -g slicejs-cli' : 'npm uninstall -g slicejs-cli'}`);
|
|
578
636
|
return;
|
|
579
637
|
}
|
|
580
638
|
|
|
581
639
|
const projectRoot = getProjectRoot(import.meta.url);
|
|
582
640
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
583
641
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
'slice:start': 'slice start',
|
|
587
|
-
'slice:create': 'slice component create',
|
|
588
|
-
'slice:list': 'slice component list',
|
|
589
|
-
'slice:delete': 'slice component delete',
|
|
590
|
-
'slice:init': 'slice init',
|
|
591
|
-
'slice:get': 'slice get',
|
|
592
|
-
'slice:browse': 'slice browse',
|
|
593
|
-
'slice:sync': 'slice sync',
|
|
594
|
-
'slice:version': 'slice version',
|
|
595
|
-
'slice:update': 'slice update',
|
|
596
|
-
};
|
|
642
|
+
// Shared with post.js and slice init — see commands/utils/sliceScripts.js
|
|
643
|
+
const sliceScripts = SLICE_SCRIPTS;
|
|
597
644
|
|
|
598
645
|
try {
|
|
599
646
|
let pkg = {};
|
|
@@ -8,6 +8,7 @@ import inquirer from 'inquirer';
|
|
|
8
8
|
import { exec } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import { getProjectRoot, getSrcPath, getApiPath, getConfigPath, getPath } from '../utils/PathHelper.js';
|
|
11
|
+
import { resolvePackageManager, installCommand } from '../utils/PackageManager.js';
|
|
11
12
|
import updateManager from '../utils/updateManager.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -142,6 +143,69 @@ async function checkPort() {
|
|
|
142
143
|
});
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Checks the package manager setup: mixed lockfiles, packageManager field
|
|
148
|
+
* consistency, and a project-local CLI install (required for local delegation).
|
|
149
|
+
* Exported for tests.
|
|
150
|
+
*/
|
|
151
|
+
export async function checkPackageManagerSetup(projectRoot = getProjectRoot(import.meta.url)) {
|
|
152
|
+
const issues = [];
|
|
153
|
+
const suggestions = [];
|
|
154
|
+
|
|
155
|
+
const LOCKFILE_PM = {
|
|
156
|
+
'package-lock.json': 'npm',
|
|
157
|
+
'pnpm-lock.yaml': 'pnpm',
|
|
158
|
+
'yarn.lock': 'yarn'
|
|
159
|
+
};
|
|
160
|
+
const lockfiles = [];
|
|
161
|
+
for (const lockfile of Object.keys(LOCKFILE_PM)) {
|
|
162
|
+
if (await fs.pathExists(path.join(projectRoot, lockfile))) {
|
|
163
|
+
lockfiles.push(lockfile);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let pkg = null;
|
|
168
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
169
|
+
if (await fs.pathExists(pkgPath)) {
|
|
170
|
+
try { pkg = await fs.readJson(pkgPath); } catch { /* reported by Dependencies check */ }
|
|
171
|
+
}
|
|
172
|
+
const pmField = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
|
|
173
|
+
const pm = resolvePackageManager(projectRoot).name;
|
|
174
|
+
|
|
175
|
+
if (lockfiles.length > 1) {
|
|
176
|
+
issues.push(`Mixed lockfiles found: ${lockfiles.join(', ')}`);
|
|
177
|
+
const keep = pmField || pm;
|
|
178
|
+
const keepLockfile = Object.entries(LOCKFILE_PM).find(([, name]) => name === keep)?.[0];
|
|
179
|
+
suggestions.push(`Keep only ${keepLockfile ?? 'the lockfile of your package manager'} and delete the rest`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (pkg && !pmField) {
|
|
183
|
+
issues.push('No "packageManager" field in package.json');
|
|
184
|
+
suggestions.push(`Pin it (e.g. "packageManager": "${pm}@<version>") so every tool resolves the same package manager`);
|
|
185
|
+
} else if (pmField && lockfiles.length === 1 && LOCKFILE_PM[lockfiles[0]] !== pmField) {
|
|
186
|
+
issues.push(`packageManager is "${pmField}" but the lockfile is ${lockfiles[0]}`);
|
|
187
|
+
suggestions.push(`Reinstall with ${pmField} (or update the packageManager field) so they agree`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const localCliPath = path.join(projectRoot, 'node_modules', 'slicejs-cli', 'package.json');
|
|
191
|
+
if (pkg && !(await fs.pathExists(localCliPath))) {
|
|
192
|
+
issues.push('slicejs-cli is not installed locally');
|
|
193
|
+
suggestions.push(`Run "${installCommand(pm, 'slicejs-cli', { dev: true })}" so the launcher delegates to a version pinned per project`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (issues.length === 0) {
|
|
197
|
+
return {
|
|
198
|
+
pass: true,
|
|
199
|
+
message: `Package manager setup is consistent (${pmField || pm})`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
warn: true,
|
|
204
|
+
message: issues.join(' | '),
|
|
205
|
+
suggestion: suggestions.join(' | ')
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
145
209
|
/**
|
|
146
210
|
* Checks dependencies in package.json
|
|
147
211
|
*/
|
|
@@ -176,8 +240,8 @@ async function checkDependencies() {
|
|
|
176
240
|
warn: true,
|
|
177
241
|
message: `Missing dependencies: ${missing.join(', ')}`,
|
|
178
242
|
suggestion: missing.includes('slicejs-web-framework')
|
|
179
|
-
?
|
|
180
|
-
:
|
|
243
|
+
? `Run "${installCommand(resolvePackageManager(getProjectRoot(import.meta.url)).name, 'slicejs-web-framework@latest')}" in your project`
|
|
244
|
+
: `Run "${installCommand(resolvePackageManager(getProjectRoot(import.meta.url)).name, 'slicejs-cli@latest', { dev: true })}" in your project`,
|
|
181
245
|
missing
|
|
182
246
|
};
|
|
183
247
|
}
|
|
@@ -278,6 +342,7 @@ export default async function runDiagnostics() {
|
|
|
278
342
|
{ name: 'Project Structure', fn: checkDirectoryStructure },
|
|
279
343
|
{ name: 'Configuration', fn: checkConfig },
|
|
280
344
|
{ name: 'Port Availability', fn: checkPort },
|
|
345
|
+
{ name: 'Package Manager', fn: checkPackageManagerSetup },
|
|
281
346
|
{ name: 'Dependencies', fn: checkDependencies },
|
|
282
347
|
{ name: 'Components', fn: checkComponents }
|
|
283
348
|
];
|
|
@@ -340,9 +405,10 @@ export default async function runDiagnostics() {
|
|
|
340
405
|
}
|
|
341
406
|
]);
|
|
342
407
|
if (confirmInstall) {
|
|
408
|
+
const pm = resolvePackageManager(projectRoot).name;
|
|
343
409
|
for (const pkg of depsResult.missing) {
|
|
344
410
|
try {
|
|
345
|
-
const cmd =
|
|
411
|
+
const cmd = installCommand(pm, `${pkg}@latest`);
|
|
346
412
|
Print.info(`Installing ${pkg}...`);
|
|
347
413
|
await execAsync(cmd, { cwd: projectRoot });
|
|
348
414
|
Print.success(`${pkg} installed`);
|
|
@@ -50,6 +50,10 @@ class ComponentRegistry {
|
|
|
50
50
|
this.componentsRegistry = null;
|
|
51
51
|
this.config = null;
|
|
52
52
|
this._configPromise = null;
|
|
53
|
+
// Serializes read-modify-write cycles on components.js: installs run
|
|
54
|
+
// concurrently (runConcurrent), and parallel writers corrupt the file
|
|
55
|
+
// (partial reads) or silently drop registrations (lost updates).
|
|
56
|
+
this._registryLock = Promise.resolve();
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
async _ensureConfig() {
|
|
@@ -249,31 +253,35 @@ filterOfficialComponents(allComponents) {
|
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
async updateLocalRegistrySafe(componentName, category) {
|
|
256
|
+
// Queue behind any in-flight registry update; keep the chain alive even
|
|
257
|
+
// when an update throws so later updates still run.
|
|
258
|
+
const run = this._registryLock.then(() => this._updateLocalRegistry(componentName, category));
|
|
259
|
+
this._registryLock = run.catch(() => {});
|
|
260
|
+
return run;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async _updateLocalRegistry(componentName, category) {
|
|
252
264
|
const componentsPath = getComponentsJsPath(import.meta.url);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
Print.info(`${componentName} already exists in local registry`);
|
|
274
|
-
}
|
|
275
|
-
} catch (error) {
|
|
276
|
-
throw error;
|
|
265
|
+
if (!await fs.pathExists(componentsPath)) {
|
|
266
|
+
const dir = path.dirname(componentsPath);
|
|
267
|
+
await fs.ensureDir(dir);
|
|
268
|
+
const initial = `const components = {};\n\nexport default components;\n`;
|
|
269
|
+
await fs.writeFile(componentsPath, initial, 'utf8');
|
|
270
|
+
}
|
|
271
|
+
const content = await fs.readFile(componentsPath, 'utf8');
|
|
272
|
+
const match = content.match(/const components = ({[\s\S]*?});/);
|
|
273
|
+
if (!match) throw new Error('Invalid components.js format in local project');
|
|
274
|
+
const componentsObj = JSON.parse(match[1]);
|
|
275
|
+
if (!componentsObj[componentName]) {
|
|
276
|
+
componentsObj[componentName] = category;
|
|
277
|
+
const sorted = Object.keys(componentsObj)
|
|
278
|
+
.sort()
|
|
279
|
+
.reduce((obj, key) => { obj[key] = componentsObj[key]; return obj; }, {});
|
|
280
|
+
const newContent = `const components = ${JSON.stringify(sorted, null, 2)};\n\nexport default components;\n`;
|
|
281
|
+
await fs.writeFile(componentsPath, newContent, 'utf8');
|
|
282
|
+
Print.registryUpdate(`Registered ${componentName} in local components.js`);
|
|
283
|
+
} else {
|
|
284
|
+
Print.info(`${componentName} already exists in local registry`);
|
|
277
285
|
}
|
|
278
286
|
}
|
|
279
287
|
|
|
@@ -339,7 +347,7 @@ filterOfficialComponents(allComponents) {
|
|
|
339
347
|
await this.updateLocalRegistrySafe(componentName, category);
|
|
340
348
|
|
|
341
349
|
Print.success(`${componentName} installed successfully from official repository!`);
|
|
342
|
-
console.log(`📁 Location: ${folderSuffix
|
|
350
|
+
console.log(`📁 Location: ${[folderSuffix, categoryPath, componentName].join('/').replace(/\/+/g, '/')}/`);
|
|
343
351
|
console.log(`📄 Files: ${downloadedFiles.join(', ')}`);
|
|
344
352
|
|
|
345
353
|
return true;
|
package/commands/init/init.js
CHANGED
|
@@ -5,10 +5,62 @@ import ora from 'ora';
|
|
|
5
5
|
import Print from '../Print.js';
|
|
6
6
|
import { getProjectRoot, getApiPath, getSrcPath, getPath } from '../utils/PathHelper.js';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
+
import {
|
|
9
|
+
resolvePackageManager,
|
|
10
|
+
getPackageManagerVersion,
|
|
11
|
+
installCommand
|
|
12
|
+
} from '../utils/PackageManager.js';
|
|
13
|
+
import { SLICE_SCRIPTS } from '../utils/sliceScripts.js';
|
|
8
14
|
|
|
9
15
|
// Import ComponentRegistry class from getComponent
|
|
10
16
|
import { ComponentRegistry } from '../getComponent/getComponent.js';
|
|
11
17
|
|
|
18
|
+
// Fetch the latest published version straight from the npm registry. This is
|
|
19
|
+
// informational only (we never pin installs to it): it avoids depending on
|
|
20
|
+
// `npm view` (absent on pnpm-only machines) and plays nice with pnpm's
|
|
21
|
+
// minimumReleaseAge quarantine, which may legitimately resolve an older version.
|
|
22
|
+
async function fetchLatestVersion(packageName) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
25
|
+
headers: { 'Accept': 'application/json' }
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) return null;
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
return data.version || null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create the project manifest BEFORE any install runs. Without a package.json in
|
|
36
|
+
// the project folder, npm/pnpm walk up the directory tree looking for the nearest
|
|
37
|
+
// manifest and anchor node_modules (and the dependency entry) OUTSIDE the project.
|
|
38
|
+
// Exported for tests (init-project-isolation.test.js).
|
|
39
|
+
export async function ensureProjectManifest(projectRoot, packageManager) {
|
|
40
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
41
|
+
if (await fs.pathExists(pkgPath)) return pkgPath;
|
|
42
|
+
|
|
43
|
+
const pkg = {
|
|
44
|
+
name: path.basename(projectRoot),
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
description: 'Slice.js project',
|
|
47
|
+
main: 'api/index.js',
|
|
48
|
+
type: 'module',
|
|
49
|
+
engines: { node: '>=20.0.0' },
|
|
50
|
+
scripts: {}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Persist the chosen package manager (corepack convention) so every later
|
|
54
|
+
// command — slice update, slice doctor — detects it deterministically.
|
|
55
|
+
const pmVersion = getPackageManagerVersion(packageManager);
|
|
56
|
+
if (pmVersion) {
|
|
57
|
+
pkg.packageManager = `${packageManager}@${pmVersion}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
|
|
61
|
+
return pkgPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
12
64
|
// Visual components used by the App Shell + MultiRoute starter project.
|
|
13
65
|
// We install only these on init; newcomers add more on demand with `slice get <Name>`.
|
|
14
66
|
const STARTER_VISUAL_COMPONENTS = [
|
|
@@ -30,32 +82,52 @@ const STARTER_SERVICE_COMPONENTS = [
|
|
|
30
82
|
'LocalStorageManager'
|
|
31
83
|
];
|
|
32
84
|
|
|
33
|
-
export default async function initializeProject(
|
|
85
|
+
export default async function initializeProject(options = {}) {
|
|
34
86
|
try {
|
|
35
87
|
const projectRoot = getProjectRoot(import.meta.url);
|
|
36
88
|
const destinationApi = getApiPath(import.meta.url);
|
|
37
89
|
const destinationSrc = getSrcPath(import.meta.url);
|
|
38
90
|
|
|
91
|
+
// Resolve the package manager chosen in `slice init` (or detect it when
|
|
92
|
+
// initializeProject is invoked directly, e.g. inside an existing folder).
|
|
93
|
+
const packageManager = options.packageManager
|
|
94
|
+
|| resolvePackageManager(projectRoot).name;
|
|
95
|
+
|
|
96
|
+
// 0. CREATE PROJECT MANIFEST FIRST — must exist before any install so the
|
|
97
|
+
// package manager anchors node_modules inside the project folder.
|
|
98
|
+
await ensureProjectManifest(projectRoot, packageManager);
|
|
99
|
+
|
|
39
100
|
const fwSpinner = ora('Ensuring latest Slice framework...').start();
|
|
40
101
|
let latestVersion = null;
|
|
102
|
+
let installedVersion = null;
|
|
41
103
|
let sliceBaseDir;
|
|
42
104
|
try {
|
|
43
|
-
|
|
44
|
-
latestVersion = latest;
|
|
105
|
+
latestVersion = await fetchLatestVersion('slicejs-web-framework');
|
|
45
106
|
const installedPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework', 'package.json');
|
|
46
107
|
let installed = null;
|
|
47
108
|
if (await fs.pathExists(installedPkgPath)) {
|
|
48
109
|
const pkg = await fs.readJson(installedPkgPath);
|
|
49
110
|
installed = pkg.version;
|
|
50
111
|
}
|
|
51
|
-
if (installed !==
|
|
52
|
-
|
|
112
|
+
if (!installed || (latestVersion && installed !== latestVersion)) {
|
|
113
|
+
// Install WITHOUT pinning an exact version: the package manager
|
|
114
|
+
// resolves it under its own policies (e.g. pnpm minimumReleaseAge
|
|
115
|
+
// quarantines versions younger than the configured age — pinning
|
|
116
|
+
// the registry's freshest version would make resolution fail).
|
|
117
|
+
execSync(installCommand(packageManager, 'slicejs-web-framework'), { cwd: projectRoot, stdio: 'inherit' });
|
|
118
|
+
}
|
|
119
|
+
if (await fs.pathExists(installedPkgPath)) {
|
|
120
|
+
const pkg = await fs.readJson(installedPkgPath);
|
|
121
|
+
installedVersion = pkg.version;
|
|
53
122
|
}
|
|
54
123
|
sliceBaseDir = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework');
|
|
55
|
-
fwSpinner.succeed(`slicejs-web-framework@${
|
|
124
|
+
fwSpinner.succeed(`slicejs-web-framework@${installedVersion || 'unknown'} ready`);
|
|
125
|
+
if (latestVersion && installedVersion && installedVersion !== latestVersion) {
|
|
126
|
+
Print.info(`Latest published is ${latestVersion}; your package manager resolved ${installedVersion} (release-age policy or cached registry).`);
|
|
127
|
+
}
|
|
56
128
|
} catch (err) {
|
|
57
129
|
// Fallback uses __dirname-style path because it looks for a local development copy,
|
|
58
|
-
// not a project-relative path —
|
|
130
|
+
// not a project-relative path — the install failed, so we fall back to monorepo sibling.
|
|
59
131
|
const fallback = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../slicejs-web-framework');
|
|
60
132
|
if (await fs.pathExists(fallback)) {
|
|
61
133
|
sliceBaseDir = fallback;
|
|
@@ -67,6 +139,21 @@ export default async function initializeProject(projectType) {
|
|
|
67
139
|
}
|
|
68
140
|
}
|
|
69
141
|
|
|
142
|
+
// 0b. INSTALL THE CLI LOCALLY (devDependency) so the generated scripts
|
|
143
|
+
// (`npm run dev` → `slice dev`) resolve via local delegation to a version
|
|
144
|
+
// pinned per project, as the docs recommend.
|
|
145
|
+
const cliSpinner = ora('Installing slicejs-cli as devDependency...').start();
|
|
146
|
+
try {
|
|
147
|
+
const cliPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-cli', 'package.json');
|
|
148
|
+
if (!(await fs.pathExists(cliPkgPath))) {
|
|
149
|
+
execSync(installCommand(packageManager, 'slicejs-cli', { dev: true }), { cwd: projectRoot, stdio: 'inherit' });
|
|
150
|
+
}
|
|
151
|
+
cliSpinner.succeed('slicejs-cli installed locally');
|
|
152
|
+
} catch (err) {
|
|
153
|
+
cliSpinner.warn('Could not install slicejs-cli locally — scripts will use the global CLI');
|
|
154
|
+
Print.info(`You can add it later with: ${installCommand(packageManager, 'slicejs-cli', { dev: true })}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
70
157
|
// These derive from sliceBaseDir (which comes from npm install or fallback),
|
|
71
158
|
// so they're already dynamic — no PathHelper needed.
|
|
72
159
|
const apiDir = path.join(sliceBaseDir, 'api');
|
|
@@ -245,6 +332,7 @@ export default async function initializeProject(projectType) {
|
|
|
245
332
|
|
|
246
333
|
// Comandos principales
|
|
247
334
|
pkg.scripts['dev'] = 'slice dev';
|
|
335
|
+
pkg.scripts['build'] = 'slice build';
|
|
248
336
|
pkg.scripts['start'] = 'slice start';
|
|
249
337
|
|
|
250
338
|
// Component management
|
|
@@ -257,39 +345,28 @@ export default async function initializeProject(projectType) {
|
|
|
257
345
|
pkg.scripts['browse'] = 'slice browse';
|
|
258
346
|
pkg.scripts['sync'] = 'slice sync';
|
|
259
347
|
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
pkg.scripts
|
|
263
|
-
pkg.scripts['slice:types'] = 'slice types generate';
|
|
264
|
-
|
|
265
|
-
// Legacy (compatibility)
|
|
266
|
-
pkg.scripts['slice:init'] = 'slice init';
|
|
267
|
-
pkg.scripts['slice:start'] = 'slice start';
|
|
268
|
-
pkg.scripts['slice:dev'] = 'slice dev';
|
|
269
|
-
pkg.scripts['slice:create'] = 'slice component create';
|
|
270
|
-
pkg.scripts['slice:list'] = 'slice component list';
|
|
271
|
-
pkg.scripts['slice:delete'] = 'slice component delete';
|
|
272
|
-
pkg.scripts['slice:get'] = 'slice get';
|
|
273
|
-
pkg.scripts['slice:browse'] = 'slice browse';
|
|
274
|
-
pkg.scripts['slice:sync'] = 'slice sync';
|
|
348
|
+
// slice:* namespaced set — shared with post.js and `slice postinstall`
|
|
349
|
+
// (commands/utils/sliceScripts.js) so the three never drift apart.
|
|
350
|
+
Object.assign(pkg.scripts, SLICE_SCRIPTS);
|
|
275
351
|
pkg.scripts['run'] = 'slice dev';
|
|
276
352
|
|
|
277
353
|
// Module configuration
|
|
278
354
|
pkg.type = 'module';
|
|
279
355
|
pkg.engines = pkg.engines || { node: '>=20.0.0' };
|
|
280
356
|
|
|
281
|
-
// Ensure framework dependency is present
|
|
357
|
+
// Ensure framework dependency is present (the install above normally
|
|
358
|
+
// already wrote it; this is a fallback for the monorepo-sibling path).
|
|
282
359
|
if (!pkg.dependencies['slicejs-web-framework']) {
|
|
283
|
-
pkg.dependencies['slicejs-web-framework'] =
|
|
360
|
+
pkg.dependencies['slicejs-web-framework'] = installedVersion ? `^${installedVersion}` : 'latest';
|
|
284
361
|
}
|
|
285
362
|
|
|
286
363
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
|
|
287
|
-
pkgSpinner.succeed('
|
|
364
|
+
pkgSpinner.succeed('Package scripts configured successfully');
|
|
288
365
|
|
|
289
366
|
Print.title('New recommended commands:');
|
|
290
|
-
console.log(
|
|
291
|
-
console.log(
|
|
292
|
-
console.log(
|
|
367
|
+
console.log(` ${packageManager} run dev - Start development server`);
|
|
368
|
+
console.log(` ${packageManager} run get - Install components`);
|
|
369
|
+
console.log(` ${packageManager} run browse - Browse components`);
|
|
293
370
|
} catch (error) {
|
|
294
371
|
pkgSpinner.fail('Failed to configure npm scripts');
|
|
295
372
|
Print.error(error.message);
|
|
@@ -300,6 +377,7 @@ export default async function initializeProject(projectType) {
|
|
|
300
377
|
Print.newLine();
|
|
301
378
|
Print.title('Next steps:');
|
|
302
379
|
console.log(` cd ${projectName}`);
|
|
380
|
+
console.log(` ${packageManager} run dev - Start development server`);
|
|
303
381
|
console.log(' slice browse - View available components');
|
|
304
382
|
console.log(' slice get Button - Install specific components');
|
|
305
383
|
console.log(' slice sync - Update all components to latest versions');
|