cascivo 0.6.2 → 0.7.1

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/index.mjs CHANGED
@@ -490,9 +490,35 @@ function resolvePackageManagerFlag(args) {
490
490
  return { error: `Unknown package manager "${raw}". Use one of: pnpm, yarn, npm, bun.` };
491
491
  }
492
492
  //#endregion
493
+ //#region src/generated/versions.ts
494
+ /**
495
+ * Exact published versions, baked in at build time.
496
+ *
497
+ * Scaffolded apps pin these rather than `"latest"`: the cascivo packages version
498
+ * independently on 0.x, so a floating specifier can resolve a mutually incompatible set,
499
+ * and GETTING-STARTED.md tells adopters to pin exactly. Regenerate with `pnpm regen`.
500
+ */
501
+ const CASCIVO_VERSIONS = {
502
+ "@cascivo/react": "0.16.0",
503
+ "@cascivo/themes": "0.4.10",
504
+ "@cascivo/charts": "0.16.0",
505
+ "@cascivo/icons": "0.3.7",
506
+ "@cascivo/eslint-config": "0.2.1"
507
+ };
508
+ /** `@cascivo/core`'s declared `@preact/signals-react` peer range. */
509
+ const SIGNALS_PEER = ">=3.0.0";
510
+ //#endregion
493
511
  //#region src/commands/create.ts
494
- /** Version specifier used for every `@cascivo/*` dependency in generated apps. */
495
- const CASCIVO_DEP = "latest";
512
+ /**
513
+ * Exact published versions, baked in at build time by `scripts/registry/cli-versions.ts`.
514
+ *
515
+ * This used to be the literal `'latest'` for every cascivo dependency — the loosest
516
+ * possible specifier on a set of independently-versioned 0.x packages, and the direct
517
+ * opposite of GETTING-STARTED.md's "pin **exact** versions (no `^`)". A scaffold that
518
+ * contradicts the docs on the very first file an adopter opens undermines every other rule
519
+ * those docs state, so the pins are generated rather than hand-written.
520
+ */
521
+ const V = CASCIVO_VERSIONS;
496
522
  /** Install-everything command for a package manager (`pnpm install`, `yarn`, …). */
497
523
  function installAllCommand(pm) {
498
524
  return pm === "yarn" ? "yarn" : `${pm} install`;
@@ -510,6 +536,24 @@ function pascalCase(label) {
510
536
  const safe = label.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("") || "Section";
511
537
  return /^[0-9]/.test(safe) ? `Section${safe}` : safe;
512
538
  }
539
+ /**
540
+ * Short, human-readable brand for the shell header.
541
+ *
542
+ * The directory name is the wrong thing to render verbatim: a dated demo directory
543
+ * (`vercel-dashboard-2026-07-30-take2`) produced a 45-character brand in the top-left of
544
+ * every page. Take the leading words, title-case them, and stop — a brand is a label, not a
545
+ * slug. Bare numbers and `v2`-style segments are dropped rather than counted.
546
+ */
547
+ function brandName(name) {
548
+ const words = name.trim().split(/[^a-zA-Z0-9]+/).filter((w) => w !== "" && !/^\d+$/.test(w) && !/^v\d+$/i.test(w));
549
+ const kept = [];
550
+ for (const word of words) {
551
+ if (kept.length >= 3) break;
552
+ if (kept.length > 0 && kept.join(" ").length + 1 + word.length > 24) break;
553
+ kept.push(word.charAt(0).toUpperCase() + word.slice(1));
554
+ }
555
+ return kept.join(" ") || "App";
556
+ }
513
557
  /** Normalize the project name into a valid npm package name. */
514
558
  function packageName(name) {
515
559
  return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "cascivo-app";
@@ -553,20 +597,25 @@ function packageJson(opts) {
553
597
  scripts: {
554
598
  dev: "vite",
555
599
  build: "tsc && vite build",
556
- preview: "vite preview"
600
+ preview: "vite preview",
601
+ typecheck: "tsc --noEmit",
602
+ lint: "eslint ."
557
603
  },
558
604
  dependencies: {
559
- "@cascivo/core": CASCIVO_DEP,
560
- "@cascivo/react": CASCIVO_DEP,
561
- "@cascivo/themes": CASCIVO_DEP,
562
- "@cascivo/tokens": CASCIVO_DEP,
605
+ "@cascivo/react": V["@cascivo/react"],
606
+ "@cascivo/themes": V["@cascivo/themes"],
607
+ "@preact/signals-react": SIGNALS_PEER,
563
608
  react: "^19.0.0",
564
609
  "react-dom": "^19.0.0"
565
610
  },
566
611
  devDependencies: {
612
+ "@cascivo/eslint-config": V["@cascivo/eslint-config"],
613
+ "@eslint/js": "^9.0.0",
567
614
  "@types/react": "^19.0.0",
568
615
  "@types/react-dom": "^19.0.0",
569
616
  "@vitejs/plugin-react": "^5.0.0",
617
+ eslint: "^9.0.0",
618
+ "eslint-plugin-react-hooks": "^7.0.0",
570
619
  typescript: "^5.7.0",
571
620
  vite: "^7.0.0"
572
621
  }
@@ -617,7 +666,13 @@ function indexHtml(opts) {
617
666
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
618
667
  <title>${opts.name}</title>
619
668
  <style>
620
- @layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component, cascivo.theme, cascivo.blocks, cascivo.override;
669
+ @layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
670
+ cascivo.theme, cascivo.blocks, cascivo.example, cascivo.override;
671
+ /* cascivo.example is this app's own slot — above the component/blocks layers so your
672
+ styles win, below cascivo.override which stays free for one-off hotfixes. The
673
+ generated AGENTS.md tells the agent to write there, and this statement is what makes
674
+ that legal: a layer used but never declared falls to the end of the cascade and
675
+ beats everything, which is the opposite of what the ordering is for. */
621
676
  /* Third-party CSS goes in the low-priority vendor layer so it can't beat cascivo:
622
677
  @import url('some-lib/styles.css') layer(vendor); — see docs/THIRD-PARTY-CSS.md */
623
678
  @layer cascivo.reset {
@@ -674,11 +729,16 @@ function appTsx(opts, sections) {
674
729
  },`).join("\n");
675
730
  const renderedSections = sections.map((s) => ` {section.value === '${s.key}' && <${s.component} />}`).join("\n");
676
731
  return `'use client'
677
- import { signal, useSignals } from '@cascivo/core'
678
- import { AppShell, ShellHeader, SideNav, type SideNavItem } from '@cascivo/react'
732
+ import {
733
+ AppShell,
734
+ ShellHeader,
735
+ SideNav,
736
+ signal,
737
+ useSignals,
738
+ type SideNavItem,
739
+ } from '@cascivo/react'
679
740
  ${sectionImports}
680
741
 
681
- import '@cascivo/tokens'
682
742
  import '@cascivo/themes/${opts.theme}.css'
683
743
  import '@cascivo/react/styles.css'
684
744
 
@@ -695,7 +755,7 @@ ${navItems}
695
755
 
696
756
  return (
697
757
  <AppShell
698
- header={<ShellHeader brand={{ name: '${opts.name.replace(/'/g, "\\'")}' }} />}
758
+ header={<ShellHeader brand={{ name: '${brandName(opts.name).replace(/'/g, "\\'")}' }} />}
699
759
  nav={<SideNav items={navItems} />}
700
760
  >
701
761
  ${renderedSections}
@@ -740,16 +800,29 @@ export function ${section.component}() {
740
800
  }
741
801
  `;
742
802
  }
743
- function cascivoConfig(opts) {
744
- return `import type { CascadeConfig } from 'cascivo'
745
-
746
- const config: CascadeConfig = {
747
- registry: '${CASCIVO_HOST}/registry.json',
748
- outputDir: 'src/components/ui',
749
- theme: '${opts.theme}',
750
- }
803
+ /**
804
+ * ESLint flat config for the scaffold.
805
+ *
806
+ * `@cascivo/eslint-config` is wired in from the start because
807
+ * `eslint-plugin-react-hooks@7`'s `recommended-latest` reports every `signal.value = next`
808
+ * — the idiom AI-RULES.md mandates and the generated `App.tsx` below uses — as
809
+ * `Error: This value cannot be modified`. Without this, `pnpm lint` on a freshly scaffolded
810
+ * app errors on every piece of state it ships with.
811
+ */
812
+ function eslintConfig() {
813
+ return `import js from '@eslint/js'
814
+ import reactHooks from 'eslint-plugin-react-hooks'
815
+ import cascivo from '@cascivo/eslint-config'
751
816
 
752
- export default config
817
+ export default [
818
+ js.configs.recommended,
819
+ reactHooks.configs['recommended-latest'],
820
+ // Spread LAST — flat config is last-wins. This turns off \`react-hooks/immutability\`,
821
+ // which reports cascivo's signal writes (\`signal.value = next\`) as errors.
822
+ // See https://cascivo.com/docs/using-with-strict-eslint.md
823
+ ...cascivo,
824
+ { ignores: ['dist/**'] },
825
+ ]
753
826
  `;
754
827
  }
755
828
  function gitignore() {
@@ -807,7 +880,7 @@ This app's declared layer order (in \`index.html\`):
807
880
 
808
881
  \`\`\`css
809
882
  @layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
810
- cascivo.theme, cascivo.blocks, cascivo.override;
883
+ cascivo.theme, cascivo.blocks, cascivo.example, cascivo.override;
811
884
  \`\`\`
812
885
 
813
886
  ### Worked example — nesting, not new layers
@@ -850,8 +923,8 @@ function buildScaffold(opts) {
850
923
  contents: indexHtml(opts)
851
924
  },
852
925
  {
853
- path: "cascivo.config.ts",
854
- contents: cascivoConfig(opts)
926
+ path: "eslint.config.js",
927
+ contents: eslintConfig()
855
928
  },
856
929
  {
857
930
  path: ".gitignore",
@@ -956,20 +1029,35 @@ async function create(args, cwd = process.cwd()) {
956
1029
  //#endregion
957
1030
  //#region src/commands/doctor.ts
958
1031
  var doctor_exports = /* @__PURE__ */ __exportAll({
1032
+ checkDuplicateCore: () => checkDuplicateCore,
959
1033
  checkProjectDependencies: () => checkProjectDependencies,
960
1034
  checkSignalsCompat: () => checkSignalsCompat,
961
1035
  checkSsrConfig: () => checkSsrConfig,
962
- isAdopterProject: () => isAdopterProject,
1036
+ detectInstallPath: () => detectInstallPath,
963
1037
  runDoctor: () => runDoctor,
964
1038
  stripCommentsAndStrings: () => stripCommentsAndStrings
965
1039
  });
966
- /** Runtime packages copied cascivo source needs; the last is @cascivo/core's peer. */
1040
+ /** Runtime packages **copied** cascivo source needs; the last is @cascivo/core's peer. */
967
1041
  const REQUIRED_RUNTIME_DEPS = [
968
1042
  "@cascivo/core",
969
1043
  "@cascivo/tokens",
970
1044
  "@cascivo/themes",
971
1045
  "@preact/signals-react"
972
1046
  ];
1047
+ /** Runtime packages a **prebuilt** (Path B) app needs. */
1048
+ const REQUIRED_PREBUILT_DEPS = [
1049
+ "@cascivo/react",
1050
+ "@cascivo/themes",
1051
+ "@preact/signals-react"
1052
+ ];
1053
+ /**
1054
+ * Packages a prebuilt app must NOT declare directly, with the rule each one breaks. They
1055
+ * are transitive there, and everything they export is re-exported from `@cascivo/react`.
1056
+ */
1057
+ const FORBIDDEN_PREBUILT_DEPS = {
1058
+ "@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",
1059
+ "@cascivo/tokens": "GETTING-STARTED.md: @cascivo/tokens comes with @cascivo/themes as a direct dependency — never install it by hand"
1060
+ };
973
1061
  /** Installed on demand by `cascivo add` when a component/chart declares them. */
974
1062
  const ON_DEMAND_DEPS = ["@cascivo/i18n", "@cascivo/charts"];
975
1063
  const CONFIG_FILES = [
@@ -977,17 +1065,74 @@ const CONFIG_FILES = [
977
1065
  "cascivo.config.js",
978
1066
  "cascivo.config.mjs"
979
1067
  ];
980
- /** Whether cwd looks like a cascivo adopter project (has a generated config). */
981
- function isAdopterProject(cwd) {
982
- return CONFIG_FILES.some((f) => existsSync(join(cwd, f)));
1068
+ /**
1069
+ * Infer the install path from evidence.
1070
+ *
1071
+ * This used to be `isAdopterProject()` — "does a `cascivo.config.*` exist?" — and
1072
+ * `cascivo create` wrote that config into every scaffold, including prebuilt-path ones. So
1073
+ * every scaffolded app was judged copy-paste and told to install `@cascivo/core` and
1074
+ * `@cascivo/tokens`, which the docs explicitly forbid on that path. `doctor --ci` exited 1
1075
+ * on a correctly-installed app, which made the CI gate the docs recommend
1076
+ * (`cascivo doctor --ci && cascivo audit --ai src`) red on day one.
1077
+ *
1078
+ * A config file now only contributes evidence when it points at a directory that actually
1079
+ * contains copied source, which is what it was ever meant to signal.
1080
+ */
1081
+ function detectInstallPath(cwd) {
1082
+ let deps = {};
1083
+ let hasPackageJson = true;
1084
+ try {
1085
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
1086
+ deps = {
1087
+ ...pkg.dependencies,
1088
+ ...pkg.devDependencies
1089
+ };
1090
+ } catch {
1091
+ hasPackageJson = false;
1092
+ }
1093
+ if (!hasPackageJson) return "unknown";
1094
+ const usesPackage = deps["@cascivo/react"] !== void 0;
1095
+ const copied = hasCopiedSource(cwd);
1096
+ if (usesPackage && copied) return "hybrid";
1097
+ if (usesPackage) return "prebuilt";
1098
+ if (copied) return "copied";
1099
+ return "unknown";
1100
+ }
1101
+ /** Whether the configured output directory holds vendored component source. */
1102
+ function hasCopiedSource(cwd) {
1103
+ for (const dir of outputDirCandidates(cwd)) {
1104
+ const full = join(cwd, dir);
1105
+ if (!existsSync(full)) continue;
1106
+ try {
1107
+ if (readdirSync(full).some((f) => f.endsWith(".tsx"))) return true;
1108
+ } catch {}
1109
+ }
1110
+ return false;
1111
+ }
1112
+ /** `outputDir` from the config if it declares one, plus the documented default. */
1113
+ function outputDirCandidates(cwd) {
1114
+ const dirs = new Set(["src/components/ui"]);
1115
+ for (const file of CONFIG_FILES) {
1116
+ const full = join(cwd, file);
1117
+ if (!existsSync(full)) continue;
1118
+ try {
1119
+ const declared = /outputDir\s*:\s*['"]([^'"]+)['"]/.exec(readFileSync(full, "utf8"))?.[1];
1120
+ if (declared !== void 0) dirs.add(declared);
1121
+ } catch {}
1122
+ }
1123
+ return [...dirs];
983
1124
  }
984
1125
  /**
985
- * Advisory check that the runtime dependencies copied source needs are declared
986
- * in the project's package.json. Turns the opaque "cannot find module
987
- * '@preact/signals-react'" build failure — the report's #4, where the peer was
988
- * invisible — into a diagnosed condition with a fix. Adopter-only (gated on a
989
- * cascivo.config by the caller); `@cascivo/i18n`/`@cascivo/charts` are advisory
990
- * since not every project uses them.
1126
+ * Check that the runtime dependencies this project's install path needs are declared in its
1127
+ * package.json — and, on the prebuilt path, that it declares none it must not.
1128
+ *
1129
+ * Turns the opaque "cannot find module '@preact/signals-react'" build failure into a
1130
+ * diagnosed condition with a fix. What it demands depends on `detectInstallPath`: requiring
1131
+ * `@cascivo/core` of a prebuilt app is not merely unhelpful, it is the opposite of what the
1132
+ * docs say, and following the advice makes a correct project wrong.
1133
+ *
1134
+ * `@cascivo/i18n`/`@cascivo/charts` stay advisory since not every project uses them, and an
1135
+ * `unknown` path emits nothing at all rather than guessing.
991
1136
  */
992
1137
  function checkProjectDependencies(cwd) {
993
1138
  let deps = {};
@@ -1000,13 +1145,24 @@ function checkProjectDependencies(cwd) {
1000
1145
  } catch {
1001
1146
  return [];
1002
1147
  }
1148
+ const path = detectInstallPath(cwd);
1149
+ if (path === "unknown") return [];
1003
1150
  const pm = detectPackageManager(cwd);
1004
1151
  const findings = [];
1005
- for (const pkg of REQUIRED_RUNTIME_DEPS) if (deps[pkg] === void 0) findings.push({
1152
+ const required = path === "prebuilt" ? REQUIRED_PREBUILT_DEPS : REQUIRED_RUNTIME_DEPS;
1153
+ for (const pkg of required) if (deps[pkg] === void 0) findings.push({
1006
1154
  package: pkg,
1007
1155
  required: true,
1008
- hint: installHint(pm, [pkg])
1156
+ hint: `detected ${path} install path — ${installHint(pm, [pkg])}`
1009
1157
  });
1158
+ if (path === "prebuilt") {
1159
+ for (const [pkg, reason] of Object.entries(FORBIDDEN_PREBUILT_DEPS)) if (deps[pkg] !== void 0) findings.push({
1160
+ package: pkg,
1161
+ required: true,
1162
+ kind: "forbidden",
1163
+ hint: reason
1164
+ });
1165
+ }
1010
1166
  for (const pkg of ON_DEMAND_DEPS) if (deps[pkg] === void 0) findings.push({
1011
1167
  package: pkg,
1012
1168
  required: false,
@@ -1014,6 +1170,29 @@ function checkProjectDependencies(cwd) {
1014
1170
  });
1015
1171
  return findings;
1016
1172
  }
1173
+ /** Packages that carry their own `@cascivo/core` dependency. */
1174
+ const CORE_DEPENDENTS = [
1175
+ "@cascivo/react",
1176
+ "@cascivo/charts",
1177
+ "@cascivo/flow",
1178
+ "@cascivo/editor"
1179
+ ];
1180
+ async function checkDuplicateCore(cwd) {
1181
+ const root = await readInstalledPackageVersion(cwd, "@cascivo/core");
1182
+ const nested = [];
1183
+ for (const owner of CORE_DEPENDENTS) {
1184
+ const ownerRoot = join(cwd, "node_modules", owner);
1185
+ if (!existsSync(ownerRoot)) continue;
1186
+ const version = await readInstalledPackageVersion(ownerRoot, "@cascivo/core");
1187
+ if (version !== null && version !== root) nested.push(`${owner} → ${version}`);
1188
+ }
1189
+ if (nested.length === 0) return null;
1190
+ return {
1191
+ root,
1192
+ nested,
1193
+ 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."
1194
+ };
1195
+ }
1017
1196
  /**
1018
1197
  * Checks the installed `@preact/signals-react` against the installed React.
1019
1198
  * React 19 removed the `__SECRET_INTERNALS…` export that signals-react 2.x
@@ -2523,27 +2702,29 @@ async function run(args) {
2523
2702
  } else {
2524
2703
  const cwd = process.cwd();
2525
2704
  const result = await runDoctor(cwd);
2526
- const { checkProjectDependencies, checkSignalsCompat, checkSsrConfig, isAdopterProject } = await Promise.resolve().then(() => doctor_exports);
2527
- const adopter = isAdopterProject(cwd);
2705
+ const { checkDuplicateCore, checkProjectDependencies, checkSignalsCompat, checkSsrConfig, detectInstallPath } = await Promise.resolve().then(() => doctor_exports);
2706
+ const adopter = detectInstallPath(cwd) !== "unknown";
2528
2707
  const deps = adopter ? checkProjectDependencies(cwd) : [];
2529
2708
  const missingRequired = deps.filter((d) => d.required);
2530
2709
  const signalsCompat = adopter ? await checkSignalsCompat(cwd) : null;
2531
2710
  const signalsError = signalsCompat?.severity === "error";
2532
2711
  const ssrHint = adopter ? checkSsrConfig(cwd) : null;
2533
- if (result.passed && deps.length === 0 && signalsCompat === null && ssrHint === null) console.log("No violations found.");
2712
+ const duplicateCore = adopter ? await checkDuplicateCore(cwd) : null;
2713
+ if (result.passed && deps.length === 0 && signalsCompat === null && ssrHint === null && duplicateCore === null) console.log("No violations found.");
2534
2714
  else {
2535
2715
  for (const v of result.violations) console.error(`[${v.rule}] ${v.detail}\n ${v.file}`);
2536
- for (const d of missingRequired) console.error(`[missing-dependency] ${d.package} is not in package.json — copied cascivo source needs it. Install: ${d.hint}`);
2716
+ 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
2717
  if (signalsCompat) (signalsError ? console.error : console.log)(`[${signalsError ? "signals-incompatible" : "signals-outdated"}] ${signalsCompat.detail} Upgrade: ${signalsCompat.hint}`);
2538
2718
  if (ssrHint) console.log(`[ssr-config] ${ssrHint}`);
2719
+ if (duplicateCore) console.error(`[duplicate-core] More than one @cascivo/core is installed (root: ${duplicateCore.root ?? "none"}; ${duplicateCore.nested.join("; ")}). ` + duplicateCore.hint);
2539
2720
  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;
2721
+ if (ci && (result.violations.length > 0 || missingRequired.length > 0 || signalsError || duplicateCore !== null)) process.exitCode = 1;
2541
2722
  }
2542
2723
  }
2543
2724
  break;
2544
2725
  }
2545
2726
  case "audit": {
2546
- const { audit } = await import("./audit-Cn6HJkuo.mjs");
2727
+ const { audit } = await import("./audit-C4ul-2Ix.mjs");
2547
2728
  await audit(rest, await loadConfig());
2548
2729
  break;
2549
2730
  }