cascivo 0.6.2 → 0.7.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/dist/{audit-Cn6HJkuo.mjs → audit-C4ul-2Ix.mjs} +35 -9
- package/dist/audit-C4ul-2Ix.mjs.map +1 -0
- package/dist/{config-C6GdrbvF.mjs → config-D7ddWN_9.mjs} +27 -4
- package/dist/{config-C6GdrbvF.mjs.map → config-D7ddWN_9.mjs.map} +1 -1
- package/dist/{drift-qTzJ75Ds.mjs → drift-B02BbFN_.mjs} +22 -15
- package/dist/drift-B02BbFN_.mjs.map +1 -0
- package/dist/generated/audit-contract.json +500 -8
- package/dist/index.mjs +404 -55
- package/dist/index.mjs.map +1 -1
- package/dist/lock-CW8UuEPJ.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/audit-Cn6HJkuo.mjs.map +0 -1
- package/dist/drift-qTzJ75Ds.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as detectPackageManager, c as isPackageManager, l as loadConfig, n as DEFAULT_CONFIG$1, o as installCommand, r as THEMES, s as installHint, t as CASCIVO_HOST, u as __exportAll } from "./config-
|
|
2
|
+
import { a as detectPackageManager, c as isPackageManager, l as loadConfig, n as DEFAULT_CONFIG$1, o as installCommand, r as THEMES, s as installHint, t as CASCIVO_HOST, u as __exportAll } from "./config-D7ddWN_9.mjs";
|
|
3
3
|
import { n as resolveOutputPath, r as writeFileSafe, t as readFileSafe } from "./fs-m7ZvuBBm.mjs";
|
|
4
4
|
import { r as fetchTextRetry, t as fetchJson } from "./http-CJZ5W0Fa.mjs";
|
|
5
5
|
import { a as writeLock, c as findComponent, i as updateLockEntry, n as readLock, o as fetchRegistry, r as sha256, s as fileName, t as createLock } from "./lock-CW8UuEPJ.mjs";
|
|
@@ -24,7 +24,11 @@ import { createInterface } from "node:readline/promises";
|
|
|
24
24
|
function installPackages(packages, cwd = process.cwd(), opts = {}) {
|
|
25
25
|
if (packages.length === 0) return true;
|
|
26
26
|
const pm = opts.pm ?? detectPackageManager(cwd);
|
|
27
|
-
const [cmd, args] = installCommand(pm, packages, {
|
|
27
|
+
const [cmd, args] = installCommand(pm, packages, {
|
|
28
|
+
dev: opts.dev ?? false,
|
|
29
|
+
...opts.floors ? { floors: opts.floors } : {},
|
|
30
|
+
...opts.pin ? { pin: opts.pin } : {}
|
|
31
|
+
});
|
|
28
32
|
console.log(`Installing ${packages.join(", ")} with ${cmd}…`);
|
|
29
33
|
if (spawnSync(cmd, args, {
|
|
30
34
|
cwd,
|
|
@@ -351,6 +355,17 @@ async function add(names, config, opts = {}) {
|
|
|
351
355
|
}
|
|
352
356
|
const { resolved, missing } = resolveBareClosure(registry, bareSpecs);
|
|
353
357
|
for (const name of missing) console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
|
|
358
|
+
const announced = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const { entry, requested } of resolved) {
|
|
360
|
+
if (!requested || !entry.install || announced.has(entry.install)) continue;
|
|
361
|
+
announced.add(entry.install);
|
|
362
|
+
console.log(`\n"${entry.name}" ships in the ${entry.install} npm package — no source is copied into your project. Updates come from your package manager, not \`cascivo update\`.`);
|
|
363
|
+
}
|
|
364
|
+
for (const { entry, requested } of resolved) {
|
|
365
|
+
if (!requested || !entry.deprecated) continue;
|
|
366
|
+
const { replacement, since, note } = entry.deprecated;
|
|
367
|
+
console.warn(`\n⚠ "${entry.name}" is deprecated since ${since}. Use "${replacement}" instead:\n cascivo add ${replacement}\n` + (note ? ` ${note}\n` : "") + ` Adding "${entry.name}" anyway — it still works.\n`);
|
|
368
|
+
}
|
|
354
369
|
const peerFloors = {};
|
|
355
370
|
for (const { entry } of resolved) for (const [pkg, floor] of Object.entries(entry.peerVersions ?? {})) peerFloors[pkg] = floor;
|
|
356
371
|
const npmPackages = /* @__PURE__ */ new Set();
|
|
@@ -490,9 +505,35 @@ function resolvePackageManagerFlag(args) {
|
|
|
490
505
|
return { error: `Unknown package manager "${raw}". Use one of: pnpm, yarn, npm, bun.` };
|
|
491
506
|
}
|
|
492
507
|
//#endregion
|
|
508
|
+
//#region src/generated/versions.ts
|
|
509
|
+
/**
|
|
510
|
+
* Exact published versions, baked in at build time.
|
|
511
|
+
*
|
|
512
|
+
* Scaffolded apps pin these rather than `"latest"`: the cascivo packages version
|
|
513
|
+
* independently on 0.x, so a floating specifier can resolve a mutually incompatible set,
|
|
514
|
+
* and GETTING-STARTED.md tells adopters to pin exactly. Regenerate with `pnpm regen`.
|
|
515
|
+
*/
|
|
516
|
+
const CASCIVO_VERSIONS = {
|
|
517
|
+
"@cascivo/react": "0.16.1",
|
|
518
|
+
"@cascivo/themes": "0.4.11",
|
|
519
|
+
"@cascivo/charts": "0.16.1",
|
|
520
|
+
"@cascivo/icons": "0.3.8",
|
|
521
|
+
"@cascivo/eslint-config": "0.2.2"
|
|
522
|
+
};
|
|
523
|
+
/** `@cascivo/core`'s declared `@preact/signals-react` peer range. */
|
|
524
|
+
const SIGNALS_PEER = ">=3.0.0";
|
|
525
|
+
//#endregion
|
|
493
526
|
//#region src/commands/create.ts
|
|
494
|
-
/**
|
|
495
|
-
|
|
527
|
+
/**
|
|
528
|
+
* Exact published versions, baked in at build time by `scripts/registry/cli-versions.ts`.
|
|
529
|
+
*
|
|
530
|
+
* This used to be the literal `'latest'` for every cascivo dependency — the loosest
|
|
531
|
+
* possible specifier on a set of independently-versioned 0.x packages, and the direct
|
|
532
|
+
* opposite of GETTING-STARTED.md's "pin **exact** versions (no `^`)". A scaffold that
|
|
533
|
+
* contradicts the docs on the very first file an adopter opens undermines every other rule
|
|
534
|
+
* those docs state, so the pins are generated rather than hand-written.
|
|
535
|
+
*/
|
|
536
|
+
const V = CASCIVO_VERSIONS;
|
|
496
537
|
/** Install-everything command for a package manager (`pnpm install`, `yarn`, …). */
|
|
497
538
|
function installAllCommand(pm) {
|
|
498
539
|
return pm === "yarn" ? "yarn" : `${pm} install`;
|
|
@@ -510,6 +551,24 @@ function pascalCase(label) {
|
|
|
510
551
|
const safe = label.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("") || "Section";
|
|
511
552
|
return /^[0-9]/.test(safe) ? `Section${safe}` : safe;
|
|
512
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Short, human-readable brand for the shell header.
|
|
556
|
+
*
|
|
557
|
+
* The directory name is the wrong thing to render verbatim: a dated demo directory
|
|
558
|
+
* (`vercel-dashboard-2026-07-30-take2`) produced a 45-character brand in the top-left of
|
|
559
|
+
* every page. Take the leading words, title-case them, and stop — a brand is a label, not a
|
|
560
|
+
* slug. Bare numbers and `v2`-style segments are dropped rather than counted.
|
|
561
|
+
*/
|
|
562
|
+
function brandName(name) {
|
|
563
|
+
const words = name.trim().split(/[^a-zA-Z0-9]+/).filter((w) => w !== "" && !/^\d+$/.test(w) && !/^v\d+$/i.test(w));
|
|
564
|
+
const kept = [];
|
|
565
|
+
for (const word of words) {
|
|
566
|
+
if (kept.length >= 3) break;
|
|
567
|
+
if (kept.length > 0 && kept.join(" ").length + 1 + word.length > 24) break;
|
|
568
|
+
kept.push(word.charAt(0).toUpperCase() + word.slice(1));
|
|
569
|
+
}
|
|
570
|
+
return kept.join(" ") || "App";
|
|
571
|
+
}
|
|
513
572
|
/** Normalize the project name into a valid npm package name. */
|
|
514
573
|
function packageName(name) {
|
|
515
574
|
return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "cascivo-app";
|
|
@@ -553,20 +612,25 @@ function packageJson(opts) {
|
|
|
553
612
|
scripts: {
|
|
554
613
|
dev: "vite",
|
|
555
614
|
build: "tsc && vite build",
|
|
556
|
-
preview: "vite preview"
|
|
615
|
+
preview: "vite preview",
|
|
616
|
+
typecheck: "tsc --noEmit",
|
|
617
|
+
lint: "eslint ."
|
|
557
618
|
},
|
|
558
619
|
dependencies: {
|
|
559
|
-
"@cascivo/
|
|
560
|
-
"@cascivo/
|
|
561
|
-
"@
|
|
562
|
-
"@cascivo/tokens": CASCIVO_DEP,
|
|
620
|
+
"@cascivo/react": V["@cascivo/react"],
|
|
621
|
+
"@cascivo/themes": V["@cascivo/themes"],
|
|
622
|
+
"@preact/signals-react": SIGNALS_PEER,
|
|
563
623
|
react: "^19.0.0",
|
|
564
624
|
"react-dom": "^19.0.0"
|
|
565
625
|
},
|
|
566
626
|
devDependencies: {
|
|
627
|
+
"@cascivo/eslint-config": V["@cascivo/eslint-config"],
|
|
628
|
+
"@eslint/js": "^9.0.0",
|
|
567
629
|
"@types/react": "^19.0.0",
|
|
568
630
|
"@types/react-dom": "^19.0.0",
|
|
569
631
|
"@vitejs/plugin-react": "^5.0.0",
|
|
632
|
+
eslint: "^9.0.0",
|
|
633
|
+
"eslint-plugin-react-hooks": "^7.0.0",
|
|
570
634
|
typescript: "^5.7.0",
|
|
571
635
|
vite: "^7.0.0"
|
|
572
636
|
}
|
|
@@ -617,7 +681,13 @@ function indexHtml(opts) {
|
|
|
617
681
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
618
682
|
<title>${opts.name}</title>
|
|
619
683
|
<style>
|
|
620
|
-
@layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
|
|
684
|
+
@layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
|
|
685
|
+
cascivo.platform, cascivo.theme, cascivo.blocks, cascivo.example, cascivo.override;
|
|
686
|
+
/* cascivo.example is this app's own slot — above the component/blocks layers so your
|
|
687
|
+
styles win, below cascivo.override which stays free for one-off hotfixes. The
|
|
688
|
+
generated AGENTS.md tells the agent to write there, and this statement is what makes
|
|
689
|
+
that legal: a layer used but never declared falls to the end of the cascade and
|
|
690
|
+
beats everything, which is the opposite of what the ordering is for. */
|
|
621
691
|
/* Third-party CSS goes in the low-priority vendor layer so it can't beat cascivo:
|
|
622
692
|
@import url('some-lib/styles.css') layer(vendor); — see docs/THIRD-PARTY-CSS.md */
|
|
623
693
|
@layer cascivo.reset {
|
|
@@ -674,11 +744,16 @@ function appTsx(opts, sections) {
|
|
|
674
744
|
},`).join("\n");
|
|
675
745
|
const renderedSections = sections.map((s) => ` {section.value === '${s.key}' && <${s.component} />}`).join("\n");
|
|
676
746
|
return `'use client'
|
|
677
|
-
import {
|
|
678
|
-
|
|
747
|
+
import {
|
|
748
|
+
AppShell,
|
|
749
|
+
ShellHeader,
|
|
750
|
+
SideNav,
|
|
751
|
+
signal,
|
|
752
|
+
useSignals,
|
|
753
|
+
type SideNavItem,
|
|
754
|
+
} from '@cascivo/react'
|
|
679
755
|
${sectionImports}
|
|
680
756
|
|
|
681
|
-
import '@cascivo/tokens'
|
|
682
757
|
import '@cascivo/themes/${opts.theme}.css'
|
|
683
758
|
import '@cascivo/react/styles.css'
|
|
684
759
|
|
|
@@ -695,7 +770,7 @@ ${navItems}
|
|
|
695
770
|
|
|
696
771
|
return (
|
|
697
772
|
<AppShell
|
|
698
|
-
header={<ShellHeader brand={{ name: '${opts.name.replace(/'/g, "\\'")}' }} />}
|
|
773
|
+
header={<ShellHeader brand={{ name: '${brandName(opts.name).replace(/'/g, "\\'")}' }} />}
|
|
699
774
|
nav={<SideNav items={navItems} />}
|
|
700
775
|
>
|
|
701
776
|
${renderedSections}
|
|
@@ -740,16 +815,31 @@ export function ${section.component}() {
|
|
|
740
815
|
}
|
|
741
816
|
`;
|
|
742
817
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
818
|
+
/**
|
|
819
|
+
* ESLint flat config for the scaffold.
|
|
820
|
+
*
|
|
821
|
+
* `@cascivo/eslint-config` is wired in from the start because
|
|
822
|
+
* `eslint-plugin-react-hooks@7`'s `recommended-latest` reports every `signal.value = next`
|
|
823
|
+
* — the idiom AI-RULES.md mandates and the generated `App.tsx` below uses — as
|
|
824
|
+
* `Error: This value cannot be modified`. Without this, `pnpm lint` on a freshly scaffolded
|
|
825
|
+
* app errors on every piece of state it ships with.
|
|
826
|
+
*/
|
|
827
|
+
function eslintConfig() {
|
|
828
|
+
return `import js from '@eslint/js'
|
|
829
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
830
|
+
import cascivo from '@cascivo/eslint-config'
|
|
751
831
|
|
|
752
|
-
export default
|
|
832
|
+
export default [
|
|
833
|
+
js.configs.recommended,
|
|
834
|
+
// NOTE the \`.flat\` — the plugin exports both \`configs['recommended-latest']\` (the legacy
|
|
835
|
+
// eslintrc shape, which applies NOTHING here and reports no error) and this one.
|
|
836
|
+
reactHooks.configs.flat['recommended-latest'],
|
|
837
|
+
// Spread LAST — flat config is last-wins. This turns off \`react-hooks/immutability\`,
|
|
838
|
+
// which reports cascivo's signal writes (\`signal.value = next\`) as errors.
|
|
839
|
+
// See https://cascivo.com/docs/using-with-strict-eslint.md
|
|
840
|
+
...cascivo,
|
|
841
|
+
{ ignores: ['dist/**'] },
|
|
842
|
+
]
|
|
753
843
|
`;
|
|
754
844
|
}
|
|
755
845
|
function gitignore() {
|
|
@@ -807,7 +897,7 @@ This app's declared layer order (in \`index.html\`):
|
|
|
807
897
|
|
|
808
898
|
\`\`\`css
|
|
809
899
|
@layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
|
|
810
|
-
cascivo.theme, cascivo.blocks, cascivo.override;
|
|
900
|
+
cascivo.platform, cascivo.theme, cascivo.blocks, cascivo.example, cascivo.override;
|
|
811
901
|
\`\`\`
|
|
812
902
|
|
|
813
903
|
### Worked example — nesting, not new layers
|
|
@@ -850,8 +940,8 @@ function buildScaffold(opts) {
|
|
|
850
940
|
contents: indexHtml(opts)
|
|
851
941
|
},
|
|
852
942
|
{
|
|
853
|
-
path: "
|
|
854
|
-
contents:
|
|
943
|
+
path: "eslint.config.js",
|
|
944
|
+
contents: eslintConfig()
|
|
855
945
|
},
|
|
856
946
|
{
|
|
857
947
|
path: ".gitignore",
|
|
@@ -938,7 +1028,7 @@ async function create(args, cwd = process.cwd()) {
|
|
|
938
1028
|
const templateSpec = flagValue(args, "template");
|
|
939
1029
|
if (templateSpec) {
|
|
940
1030
|
const { add } = await Promise.resolve().then(() => add_exports);
|
|
941
|
-
const { loadConfig } = await import("./config-
|
|
1031
|
+
const { loadConfig } = await import("./config-D7ddWN_9.mjs").then((n) => n.i);
|
|
942
1032
|
console.log(`\nInstalling template "${templateSpec}"…`);
|
|
943
1033
|
await add([templateSpec], await loadConfig(), {
|
|
944
1034
|
cwd: targetDir,
|
|
@@ -956,20 +1046,36 @@ async function create(args, cwd = process.cwd()) {
|
|
|
956
1046
|
//#endregion
|
|
957
1047
|
//#region src/commands/doctor.ts
|
|
958
1048
|
var doctor_exports = /* @__PURE__ */ __exportAll({
|
|
1049
|
+
checkDuplicateCore: () => checkDuplicateCore,
|
|
1050
|
+
checkFormatterIgnore: () => checkFormatterIgnore,
|
|
959
1051
|
checkProjectDependencies: () => checkProjectDependencies,
|
|
960
1052
|
checkSignalsCompat: () => checkSignalsCompat,
|
|
961
1053
|
checkSsrConfig: () => checkSsrConfig,
|
|
962
|
-
|
|
1054
|
+
detectInstallPath: () => detectInstallPath,
|
|
963
1055
|
runDoctor: () => runDoctor,
|
|
964
1056
|
stripCommentsAndStrings: () => stripCommentsAndStrings
|
|
965
1057
|
});
|
|
966
|
-
/** Runtime packages copied cascivo source needs; the last is @cascivo/core's peer. */
|
|
1058
|
+
/** Runtime packages **copied** cascivo source needs; the last is @cascivo/core's peer. */
|
|
967
1059
|
const REQUIRED_RUNTIME_DEPS = [
|
|
968
1060
|
"@cascivo/core",
|
|
969
1061
|
"@cascivo/tokens",
|
|
970
1062
|
"@cascivo/themes",
|
|
971
1063
|
"@preact/signals-react"
|
|
972
1064
|
];
|
|
1065
|
+
/** Runtime packages a **prebuilt** (Path B) app needs. */
|
|
1066
|
+
const REQUIRED_PREBUILT_DEPS = [
|
|
1067
|
+
"@cascivo/react",
|
|
1068
|
+
"@cascivo/themes",
|
|
1069
|
+
"@preact/signals-react"
|
|
1070
|
+
];
|
|
1071
|
+
/**
|
|
1072
|
+
* Packages a prebuilt app must NOT declare directly, with the rule each one breaks. They
|
|
1073
|
+
* are transitive there, and everything they export is re-exported from `@cascivo/react`.
|
|
1074
|
+
*/
|
|
1075
|
+
const FORBIDDEN_PREBUILT_DEPS = {
|
|
1076
|
+
"@cascivo/core": "AI-RULES.md: never add @cascivo/core to a prebuilt-path app — it is transitive, and everything is re-exported from @cascivo/react",
|
|
1077
|
+
"@cascivo/tokens": "GETTING-STARTED.md: @cascivo/tokens comes with @cascivo/themes as a direct dependency — never install it by hand"
|
|
1078
|
+
};
|
|
973
1079
|
/** Installed on demand by `cascivo add` when a component/chart declares them. */
|
|
974
1080
|
const ON_DEMAND_DEPS = ["@cascivo/i18n", "@cascivo/charts"];
|
|
975
1081
|
const CONFIG_FILES = [
|
|
@@ -977,17 +1083,74 @@ const CONFIG_FILES = [
|
|
|
977
1083
|
"cascivo.config.js",
|
|
978
1084
|
"cascivo.config.mjs"
|
|
979
1085
|
];
|
|
980
|
-
/**
|
|
981
|
-
|
|
982
|
-
|
|
1086
|
+
/**
|
|
1087
|
+
* Infer the install path from evidence.
|
|
1088
|
+
*
|
|
1089
|
+
* This used to be `isAdopterProject()` — "does a `cascivo.config.*` exist?" — and
|
|
1090
|
+
* `cascivo create` wrote that config into every scaffold, including prebuilt-path ones. So
|
|
1091
|
+
* every scaffolded app was judged copy-paste and told to install `@cascivo/core` and
|
|
1092
|
+
* `@cascivo/tokens`, which the docs explicitly forbid on that path. `doctor --ci` exited 1
|
|
1093
|
+
* on a correctly-installed app, which made the CI gate the docs recommend
|
|
1094
|
+
* (`cascivo doctor --ci && cascivo audit --ai src`) red on day one.
|
|
1095
|
+
*
|
|
1096
|
+
* A config file now only contributes evidence when it points at a directory that actually
|
|
1097
|
+
* contains copied source, which is what it was ever meant to signal.
|
|
1098
|
+
*/
|
|
1099
|
+
function detectInstallPath(cwd) {
|
|
1100
|
+
let deps = {};
|
|
1101
|
+
let hasPackageJson = true;
|
|
1102
|
+
try {
|
|
1103
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
1104
|
+
deps = {
|
|
1105
|
+
...pkg.dependencies,
|
|
1106
|
+
...pkg.devDependencies
|
|
1107
|
+
};
|
|
1108
|
+
} catch {
|
|
1109
|
+
hasPackageJson = false;
|
|
1110
|
+
}
|
|
1111
|
+
if (!hasPackageJson) return "unknown";
|
|
1112
|
+
const usesPackage = deps["@cascivo/react"] !== void 0;
|
|
1113
|
+
const copied = hasCopiedSource(cwd);
|
|
1114
|
+
if (usesPackage && copied) return "hybrid";
|
|
1115
|
+
if (usesPackage) return "prebuilt";
|
|
1116
|
+
if (copied) return "copied";
|
|
1117
|
+
return "unknown";
|
|
1118
|
+
}
|
|
1119
|
+
/** Whether the configured output directory holds vendored component source. */
|
|
1120
|
+
function hasCopiedSource(cwd) {
|
|
1121
|
+
for (const dir of outputDirCandidates(cwd)) {
|
|
1122
|
+
const full = join(cwd, dir);
|
|
1123
|
+
if (!existsSync(full)) continue;
|
|
1124
|
+
try {
|
|
1125
|
+
if (readdirSync(full).some((f) => f.endsWith(".tsx"))) return true;
|
|
1126
|
+
} catch {}
|
|
1127
|
+
}
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
/** `outputDir` from the config if it declares one, plus the documented default. */
|
|
1131
|
+
function outputDirCandidates(cwd) {
|
|
1132
|
+
const dirs = new Set(["src/components/ui"]);
|
|
1133
|
+
for (const file of CONFIG_FILES) {
|
|
1134
|
+
const full = join(cwd, file);
|
|
1135
|
+
if (!existsSync(full)) continue;
|
|
1136
|
+
try {
|
|
1137
|
+
const declared = /outputDir\s*:\s*['"]([^'"]+)['"]/.exec(readFileSync(full, "utf8"))?.[1];
|
|
1138
|
+
if (declared !== void 0) dirs.add(declared);
|
|
1139
|
+
} catch {}
|
|
1140
|
+
}
|
|
1141
|
+
return [...dirs];
|
|
983
1142
|
}
|
|
984
1143
|
/**
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
1144
|
+
* Check that the runtime dependencies this project's install path needs are declared in its
|
|
1145
|
+
* package.json — and, on the prebuilt path, that it declares none it must not.
|
|
1146
|
+
*
|
|
1147
|
+
* Turns the opaque "cannot find module '@preact/signals-react'" build failure into a
|
|
1148
|
+
* diagnosed condition with a fix. What it demands depends on `detectInstallPath`: requiring
|
|
1149
|
+
* `@cascivo/core` of a prebuilt app is not merely unhelpful, it is the opposite of what the
|
|
1150
|
+
* docs say, and following the advice makes a correct project wrong.
|
|
1151
|
+
*
|
|
1152
|
+
* `@cascivo/i18n`/`@cascivo/charts` stay advisory since not every project uses them, and an
|
|
1153
|
+
* `unknown` path emits nothing at all rather than guessing.
|
|
991
1154
|
*/
|
|
992
1155
|
function checkProjectDependencies(cwd) {
|
|
993
1156
|
let deps = {};
|
|
@@ -1000,13 +1163,24 @@ function checkProjectDependencies(cwd) {
|
|
|
1000
1163
|
} catch {
|
|
1001
1164
|
return [];
|
|
1002
1165
|
}
|
|
1166
|
+
const path = detectInstallPath(cwd);
|
|
1167
|
+
if (path === "unknown") return [];
|
|
1003
1168
|
const pm = detectPackageManager(cwd);
|
|
1004
1169
|
const findings = [];
|
|
1005
|
-
|
|
1170
|
+
const required = path === "prebuilt" ? REQUIRED_PREBUILT_DEPS : REQUIRED_RUNTIME_DEPS;
|
|
1171
|
+
for (const pkg of required) if (deps[pkg] === void 0) findings.push({
|
|
1006
1172
|
package: pkg,
|
|
1007
1173
|
required: true,
|
|
1008
|
-
hint: installHint(pm, [pkg])
|
|
1174
|
+
hint: `detected ${path} install path — ${installHint(pm, [pkg])}`
|
|
1009
1175
|
});
|
|
1176
|
+
if (path === "prebuilt") {
|
|
1177
|
+
for (const [pkg, reason] of Object.entries(FORBIDDEN_PREBUILT_DEPS)) if (deps[pkg] !== void 0) findings.push({
|
|
1178
|
+
package: pkg,
|
|
1179
|
+
required: true,
|
|
1180
|
+
kind: "forbidden",
|
|
1181
|
+
hint: reason
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1010
1184
|
for (const pkg of ON_DEMAND_DEPS) if (deps[pkg] === void 0) findings.push({
|
|
1011
1185
|
package: pkg,
|
|
1012
1186
|
required: false,
|
|
@@ -1014,6 +1188,29 @@ function checkProjectDependencies(cwd) {
|
|
|
1014
1188
|
});
|
|
1015
1189
|
return findings;
|
|
1016
1190
|
}
|
|
1191
|
+
/** Packages that carry their own `@cascivo/core` dependency. */
|
|
1192
|
+
const CORE_DEPENDENTS = [
|
|
1193
|
+
"@cascivo/react",
|
|
1194
|
+
"@cascivo/charts",
|
|
1195
|
+
"@cascivo/flow",
|
|
1196
|
+
"@cascivo/editor"
|
|
1197
|
+
];
|
|
1198
|
+
async function checkDuplicateCore(cwd) {
|
|
1199
|
+
const root = await readInstalledPackageVersion(cwd, "@cascivo/core");
|
|
1200
|
+
const nested = [];
|
|
1201
|
+
for (const owner of CORE_DEPENDENTS) {
|
|
1202
|
+
const ownerRoot = join(cwd, "node_modules", owner);
|
|
1203
|
+
if (!existsSync(ownerRoot)) continue;
|
|
1204
|
+
const version = await readInstalledPackageVersion(ownerRoot, "@cascivo/core");
|
|
1205
|
+
if (version !== null && version !== root) nested.push(`${owner} → ${version}`);
|
|
1206
|
+
}
|
|
1207
|
+
if (nested.length === 0) return null;
|
|
1208
|
+
return {
|
|
1209
|
+
root,
|
|
1210
|
+
nested,
|
|
1211
|
+
hint: "Align the @cascivo/* versions (they are released together — see breaking-changes.json) and reinstall. Two copies of @cascivo/core means two signal registries: writes through one are invisible to components subscribed through the other, with no error."
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1017
1214
|
/**
|
|
1018
1215
|
* Checks the installed `@preact/signals-react` against the installed React.
|
|
1019
1216
|
* React 19 removed the `__SECRET_INTERNALS…` export that signals-react 2.x
|
|
@@ -1071,6 +1268,38 @@ const VITE_CONFIG_FILES = [
|
|
|
1071
1268
|
* cliff the 2026-07-20 report hit (blocker #1). A text match, not a gate. Returns
|
|
1072
1269
|
* null when there's no Vite SSR framework or the config already handles it.
|
|
1073
1270
|
*/
|
|
1271
|
+
/**
|
|
1272
|
+
* Warn when the vendored components dir is not excluded from the project's formatter.
|
|
1273
|
+
*
|
|
1274
|
+
* Owning the code means your formatter rewrites it, and `cascivo update` then reports drift
|
|
1275
|
+
* on files you never edited. `cascivo init` writes the exclusion for you, but a project that
|
|
1276
|
+
* adopted cascivo before that existed — or that added Prettier afterwards — never got it.
|
|
1277
|
+
*/
|
|
1278
|
+
function checkFormatterIgnore(cwd, outputDir) {
|
|
1279
|
+
const pairs = [{
|
|
1280
|
+
configs: [
|
|
1281
|
+
".prettierrc",
|
|
1282
|
+
".prettierrc.json",
|
|
1283
|
+
".prettierrc.js",
|
|
1284
|
+
"prettier.config.js"
|
|
1285
|
+
],
|
|
1286
|
+
ignore: ".prettierignore"
|
|
1287
|
+
}, {
|
|
1288
|
+
configs: [".oxfmtrc", ".oxfmtrc.json"],
|
|
1289
|
+
ignore: ".oxfmtignore"
|
|
1290
|
+
}];
|
|
1291
|
+
const dir = outputDir.replace(/\/+$/, "");
|
|
1292
|
+
for (const { configs, ignore } of pairs) {
|
|
1293
|
+
if (!configs.some((f) => existsSync(join(cwd, f)))) continue;
|
|
1294
|
+
let content = "";
|
|
1295
|
+
try {
|
|
1296
|
+
content = readFileSync(join(cwd, ignore), "utf8");
|
|
1297
|
+
} catch {}
|
|
1298
|
+
if (content.split("\n").some((l) => l.trim().replace(/\/+$/, "") === dir)) continue;
|
|
1299
|
+
return `"${dir}/" is not excluded from your formatter. Running it will rewrite code you own, and \`cascivo update\` will then report drift on files you never edited. Add "${dir}/" to ${ignore}.`;
|
|
1300
|
+
}
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1074
1303
|
function checkSsrConfig(cwd) {
|
|
1075
1304
|
let deps = {};
|
|
1076
1305
|
try {
|
|
@@ -1299,9 +1528,7 @@ async function generate(args, config) {
|
|
|
1299
1528
|
}
|
|
1300
1529
|
const { readFileSync } = await import("node:fs");
|
|
1301
1530
|
const configJson = readFileSync(inputArg, "utf-8");
|
|
1302
|
-
const
|
|
1303
|
-
await fetchRegistry(config.registry);
|
|
1304
|
-
const tsx = generateTsx(viewConfig, /* @__PURE__ */ new Map(), componentsDirArg ?? config.outputDir ?? "./src/components/ui");
|
|
1531
|
+
const tsx = generateTsx(JSON.parse(configJson), /* @__PURE__ */ new Map(), componentsDirArg ?? config.outputDir ?? "./src/components/ui");
|
|
1305
1532
|
const outPath = outArg ?? join(dirname(inputArg), `${basename(inputArg, ".json")}.tsx`);
|
|
1306
1533
|
writeFileSync(outPath, tsx, "utf-8");
|
|
1307
1534
|
console.log(`Generated ${outPath}`);
|
|
@@ -1391,6 +1618,85 @@ function hintEslintIfPresent(cwd) {
|
|
|
1391
1618
|
console.log("\nESLint: copied cascivo source may trip a strict host config on stylistic rules.");
|
|
1392
1619
|
console.log(" Scope them off your components dir — see docs/USING-WITH-STRICT-ESLINT.md");
|
|
1393
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Write dependency entries into `package.json` so a failed install still leaves a
|
|
1623
|
+
* DECLARATIVE-complete project, one `install` away from working.
|
|
1624
|
+
*
|
|
1625
|
+
* The reported failure: one unrelated bad version range elsewhere in the adopter's
|
|
1626
|
+
* `package.json` made `pnpm add` exit non-zero. cascivo had already written
|
|
1627
|
+
* `cascivo.config.ts`, so the project claimed to be cascivo-configured with none of the
|
|
1628
|
+
* runtime present, and the printed advice ("install them yourself") was the same command
|
|
1629
|
+
* that had just failed. Recording the dependencies is the difference between "recoverable
|
|
1630
|
+
* with one command" and "figure out what was supposed to be here".
|
|
1631
|
+
*
|
|
1632
|
+
* Never overwrites an entry that already exists — the app's own pin wins.
|
|
1633
|
+
*/
|
|
1634
|
+
function recordDependencies(cwd, packages, opts) {
|
|
1635
|
+
const pkgPath = join(cwd, "package.json");
|
|
1636
|
+
if (!existsSync(pkgPath)) return [];
|
|
1637
|
+
let pkg;
|
|
1638
|
+
try {
|
|
1639
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1640
|
+
} catch {
|
|
1641
|
+
return [];
|
|
1642
|
+
}
|
|
1643
|
+
const field = opts.dev ? "devDependencies" : "dependencies";
|
|
1644
|
+
const deps = pkg[field] ??= {};
|
|
1645
|
+
const added = [];
|
|
1646
|
+
for (const name of packages) {
|
|
1647
|
+
if (deps[name] !== void 0) continue;
|
|
1648
|
+
deps[name] = "latest";
|
|
1649
|
+
added.push(name);
|
|
1650
|
+
}
|
|
1651
|
+
if (added.length === 0) return [];
|
|
1652
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
1653
|
+
return added;
|
|
1654
|
+
}
|
|
1655
|
+
/** Formatter ignore files, in the order a project is likely to use them. */
|
|
1656
|
+
const FORMATTER_IGNORES = [{
|
|
1657
|
+
config: [
|
|
1658
|
+
".prettierrc",
|
|
1659
|
+
".prettierrc.json",
|
|
1660
|
+
".prettierrc.js",
|
|
1661
|
+
"prettier.config.js"
|
|
1662
|
+
],
|
|
1663
|
+
ignore: ".prettierignore"
|
|
1664
|
+
}, {
|
|
1665
|
+
config: [".oxfmtrc", ".oxfmtrc.json"],
|
|
1666
|
+
ignore: ".oxfmtignore"
|
|
1667
|
+
}];
|
|
1668
|
+
/**
|
|
1669
|
+
* Exclude the vendored components dir from the project's formatter.
|
|
1670
|
+
*
|
|
1671
|
+
* Owning the code means your formatter reformats it, and then `cascivo update` reports drift
|
|
1672
|
+
* on files you never touched. This ACTS rather than hints: the ESLint equivalent below only
|
|
1673
|
+
* prints a pointer, and an adopter ran `prettier --write .` before ever reading it.
|
|
1674
|
+
*
|
|
1675
|
+
* Idempotent, and never rewrites an existing line.
|
|
1676
|
+
*/
|
|
1677
|
+
function ensureFormatterIgnore(cwd, outputDir) {
|
|
1678
|
+
const line = `${outputDir.replace(/\/+$/, "")}/`;
|
|
1679
|
+
for (const { config, ignore } of FORMATTER_IGNORES) {
|
|
1680
|
+
if (!(config.some((f) => existsSync(join(cwd, f))) || hasPrettierKeyInPackageJson(cwd, ignore))) continue;
|
|
1681
|
+
const path = join(cwd, ignore);
|
|
1682
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
1683
|
+
if (current.split("\n").some((l) => l.trim() === line)) continue;
|
|
1684
|
+
const banner = "# cascivo: vendored component source — you own it, so do not reformat it\n";
|
|
1685
|
+
writeFileSync(path, current === "" ? banner + line + "\n" : `${current.replace(/\n*$/, "\n")}\n${banner}${line}\n`, "utf8");
|
|
1686
|
+
console.log(`\nAdded "${line}" to ${ignore} (so your formatter does not rewrite copied source).`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
/** `prettier` key in package.json counts as a Prettier config — only relevant to .prettierignore. */
|
|
1690
|
+
function hasPrettierKeyInPackageJson(cwd, ignore) {
|
|
1691
|
+
if (ignore !== ".prettierignore") return false;
|
|
1692
|
+
const pkgPath = join(cwd, "package.json");
|
|
1693
|
+
if (!existsSync(pkgPath)) return false;
|
|
1694
|
+
try {
|
|
1695
|
+
return "prettier" in JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1696
|
+
} catch {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1394
1700
|
/** The "here's everything you need" summary, printed once at the end of init. */
|
|
1395
1701
|
function printDependencySummary() {
|
|
1396
1702
|
console.log("\nDependencies");
|
|
@@ -1428,13 +1734,36 @@ async function init(args = [], cwd = process.cwd()) {
|
|
|
1428
1734
|
pm,
|
|
1429
1735
|
dev: true
|
|
1430
1736
|
});
|
|
1431
|
-
if (!runtimeOk || !devOk)
|
|
1737
|
+
if (!runtimeOk || !devOk) {
|
|
1738
|
+
const recorded = [...runtimeOk ? [] : recordDependencies(cwd, RUNTIME_DEPS, { dev: false }), ...devOk ? [] : recordDependencies(cwd, DEV_DEPS, { dev: true })];
|
|
1739
|
+
console.error("\nInstall failed — cascivo.config.ts was written but the packages are not installed.");
|
|
1740
|
+
if (recorded.length > 0) {
|
|
1741
|
+
console.error(`Wrote ${recorded.length} dependency entries to package.json: ${recorded.join(", ")}`);
|
|
1742
|
+
console.error(`Recover with:\n ${pm} install`);
|
|
1743
|
+
} else console.error(`Recover with:\n ${installHint(pm, RUNTIME_DEPS)}\n ${installHint(pm, DEV_DEPS, { dev: true })}`);
|
|
1744
|
+
process.exitCode = 1;
|
|
1745
|
+
}
|
|
1432
1746
|
}
|
|
1433
|
-
console.log("\
|
|
1434
|
-
console.log(` import '@cascivo/
|
|
1435
|
-
console.log("
|
|
1747
|
+
console.log("\nStylesheets — import these once, in this order, in your entry file:");
|
|
1748
|
+
console.log(` import '@cascivo/tokens' // primitive tokens — every --cascivo-* value`);
|
|
1749
|
+
console.log(` import '@cascivo/themes/${theme}.css'${" ".repeat(Math.max(1, 16 - theme.length))}// the ${theme} theme's semantic values`);
|
|
1750
|
+
console.log(` // …then your component CSS (\`cascivo add\` writes .module.css beside each component)`);
|
|
1751
|
+
console.log("\nThen set the theme on your root element:");
|
|
1436
1752
|
console.log(` <html data-theme="${theme}">`);
|
|
1753
|
+
console.log("\nSwitching themes at runtime? Use a bundle instead of the single theme:");
|
|
1754
|
+
console.log(" import '@cascivo/themes/light-dark.css' // light + dark — the common case");
|
|
1755
|
+
console.log(" import '@cascivo/themes/all.css' // all twelve themes");
|
|
1756
|
+
console.log(" import { ThemeProvider } from '@cascivo/core'");
|
|
1757
|
+
console.log("\nIn YOUR components, when you read a signal during render:");
|
|
1758
|
+
console.log(" import { useSignals } from '@cascivo/core'");
|
|
1759
|
+
console.log(" function MyComponent() {");
|
|
1760
|
+
console.log(" useSignals() // ← first statement, or the component never re-renders");
|
|
1761
|
+
console.log(" return <span>{count.value}</span>");
|
|
1762
|
+
console.log(" }");
|
|
1763
|
+
console.log("\nAdding a chart later? Charts ship as an npm package with their own stylesheet:");
|
|
1764
|
+
console.log(" import '@cascivo/charts/styles.css' // `cascivo add <chart>` reminds you");
|
|
1437
1765
|
printDependencySummary();
|
|
1766
|
+
ensureFormatterIgnore(cwd, DEFAULT_CONFIG$1.outputDir);
|
|
1438
1767
|
hintEslintIfPresent(cwd);
|
|
1439
1768
|
console.log("\nAdd components with: cascivo add <name>");
|
|
1440
1769
|
}
|
|
@@ -1449,12 +1778,22 @@ const TYPE_LABELS = {
|
|
|
1449
1778
|
flow: "Flow (npm: @cascivo/flow)",
|
|
1450
1779
|
editor: "Editor (npm: @cascivo/editor)"
|
|
1451
1780
|
};
|
|
1781
|
+
/**
|
|
1782
|
+
* Deprecation marker for the listing.
|
|
1783
|
+
*
|
|
1784
|
+
* Shown at DISCOVERY time on purpose. `overflow-menu` carried a `@deprecated` JSDoc in its
|
|
1785
|
+
* source for months, which an adopter only meets after they have already vendored the file —
|
|
1786
|
+
* and it pointed at an import path that cannot resolve on either install path.
|
|
1787
|
+
*/
|
|
1788
|
+
function deprecationSuffix(c) {
|
|
1789
|
+
return c.deprecated ? ` ⚠ deprecated → ${c.deprecated.replacement}` : "";
|
|
1790
|
+
}
|
|
1452
1791
|
/** Render a group of entries as an aligned text table (no section header). */
|
|
1453
1792
|
function formatGroup(entries) {
|
|
1454
1793
|
const rows = entries.map((c) => [
|
|
1455
1794
|
c.name,
|
|
1456
1795
|
c.category,
|
|
1457
|
-
c.description
|
|
1796
|
+
c.description + deprecationSuffix(c)
|
|
1458
1797
|
]);
|
|
1459
1798
|
const headers = [
|
|
1460
1799
|
"Name",
|
|
@@ -2518,32 +2857,42 @@ async function run(args) {
|
|
|
2518
2857
|
case "doctor": {
|
|
2519
2858
|
const ci = rest.includes("--ci");
|
|
2520
2859
|
if (rest.includes("--drift")) {
|
|
2521
|
-
const { runDoctorDrift } = await import("./drift-
|
|
2860
|
+
const { runDoctorDrift } = await import("./drift-B02BbFN_.mjs");
|
|
2522
2861
|
await runDoctorDrift(await loadConfig());
|
|
2523
2862
|
} else {
|
|
2524
2863
|
const cwd = process.cwd();
|
|
2525
2864
|
const result = await runDoctor(cwd);
|
|
2526
|
-
const { checkProjectDependencies, checkSignalsCompat, checkSsrConfig,
|
|
2527
|
-
const adopter =
|
|
2865
|
+
const { checkDuplicateCore, checkFormatterIgnore, checkProjectDependencies, checkSignalsCompat, checkSsrConfig, detectInstallPath } = await Promise.resolve().then(() => doctor_exports);
|
|
2866
|
+
const adopter = detectInstallPath(cwd) !== "unknown";
|
|
2528
2867
|
const deps = adopter ? checkProjectDependencies(cwd) : [];
|
|
2529
2868
|
const missingRequired = deps.filter((d) => d.required);
|
|
2530
2869
|
const signalsCompat = adopter ? await checkSignalsCompat(cwd) : null;
|
|
2531
2870
|
const signalsError = signalsCompat?.severity === "error";
|
|
2532
2871
|
const ssrHint = adopter ? checkSsrConfig(cwd) : null;
|
|
2533
|
-
|
|
2872
|
+
const duplicateCore = adopter ? await checkDuplicateCore(cwd) : null;
|
|
2873
|
+
const formatterHint = adopter ? checkFormatterIgnore(cwd, (await loadConfig()).outputDir) : null;
|
|
2874
|
+
const { runDoctorDrift } = await import("./drift-B02BbFN_.mjs");
|
|
2875
|
+
const driftOutcome = adopter ? await runDoctorDrift(await loadConfig(), cwd) : {
|
|
2876
|
+
ran: false,
|
|
2877
|
+
reason: "no cascivo install detected",
|
|
2878
|
+
issues: 0
|
|
2879
|
+
};
|
|
2880
|
+
if (result.passed && deps.length === 0 && signalsCompat === null && ssrHint === null && duplicateCore === null && formatterHint === null && driftOutcome.issues === 0) console.log(driftOutcome.ran ? "No violations found." : `No violations found (drift: not checked — ${driftOutcome.reason}).`);
|
|
2534
2881
|
else {
|
|
2535
2882
|
for (const v of result.violations) console.error(`[${v.rule}] ${v.detail}\n ${v.file}`);
|
|
2536
|
-
for (const d of missingRequired) console.error(`[
|
|
2883
|
+
for (const d of missingRequired) console.error(d.kind === "forbidden" ? `[forbidden-dependency] ${d.package} must not be a direct dependency here. Remove it — ${d.hint}` : `[missing-dependency] ${d.package} is not in package.json. Install: ${d.hint}`);
|
|
2537
2884
|
if (signalsCompat) (signalsError ? console.error : console.log)(`[${signalsError ? "signals-incompatible" : "signals-outdated"}] ${signalsCompat.detail} Upgrade: ${signalsCompat.hint}`);
|
|
2538
2885
|
if (ssrHint) console.log(`[ssr-config] ${ssrHint}`);
|
|
2886
|
+
if (formatterHint) console.log(`[formatter-drift] ${formatterHint}`);
|
|
2887
|
+
if (duplicateCore) console.error(`[duplicate-core] More than one @cascivo/core is installed (root: ${duplicateCore.root ?? "none"}; ${duplicateCore.nested.join("; ")}). ` + duplicateCore.hint);
|
|
2539
2888
|
for (const d of deps.filter((x) => !x.required)) console.log(`[optional] ${d.package} is not installed; add it when a component or chart needs it: ${d.hint}`);
|
|
2540
|
-
if (ci && (result.violations.length > 0 || missingRequired.length > 0 || signalsError)) process.exitCode = 1;
|
|
2889
|
+
if (ci && (result.violations.length > 0 || missingRequired.length > 0 || signalsError || duplicateCore !== null || driftOutcome.issues > 0)) process.exitCode = 1;
|
|
2541
2890
|
}
|
|
2542
2891
|
}
|
|
2543
2892
|
break;
|
|
2544
2893
|
}
|
|
2545
2894
|
case "audit": {
|
|
2546
|
-
const { audit } = await import("./audit-
|
|
2895
|
+
const { audit } = await import("./audit-C4ul-2Ix.mjs");
|
|
2547
2896
|
await audit(rest, await loadConfig());
|
|
2548
2897
|
break;
|
|
2549
2898
|
}
|