diff --git a/.changeset/tidy-scaffolds-anchor-init.md b/.changeset/tidy-scaffolds-anchor-init.md new file mode 100644 index 0000000000..d46724d2d7 --- /dev/null +++ b/.changeset/tidy-scaffolds-anchor-init.md @@ -0,0 +1,9 @@ +--- +'@object-ui/cli': patch +--- + +`objectui init` now versions the project it scaffolds against the CLI that wrote it, and stops writing a `tailwind.config.js` Tailwind 4 never reads. + +The generated `package.json` asked for `@object-ui/components` and `@object-ui/react` at `^2.0.0` while those packages publish at 17.x, so `npm install` in a fresh scaffold resolved a major unrelated to the CLI that produced it. Both ranges are now derived from the CLI's own version, which is sound because `.changeset/config.json` releases the CLI and every platform package from one `fixed` group. The scaffold's toolchain ranges had drifted the same way — vite `^7.3.1` against the repo's `^8.2.0`, typescript `^5.9.3` against `^6.0.3`, and seven more — and now read from the same table the temp-app generators use rather than from literals of their own. + +The scaffold's CSS pipeline was already Tailwind 4 (`@tailwindcss/postcss`, `@import 'tailwindcss'`), and v4 reads a JS config only when a stylesheet points `@config` at one. The `tailwind.config.js` written beside it was therefore inert — an authoritative-looking `content` list nothing consumed — and is no longer written. diff --git a/packages/cli/src/__tests__/app-generator.test.ts b/packages/cli/src/__tests__/app-generator.test.ts index 4818e35ca9..212995db55 100644 --- a/packages/cli/src/__tests__/app-generator.test.ts +++ b/packages/cli/src/__tests__/app-generator.test.ts @@ -59,6 +59,7 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; +import { buildInitPackageJson } from '../commands/init.js'; import { buildAppFiles, buildAppPackageJson, @@ -270,7 +271,7 @@ function inRepoRangesOf(name: string): Record { } /** - * Where each range in the two generated manifests must come from. + * Where each range in the THREE generated manifests must come from. * * The anchoring discipline objectui#3742/objectui#3754 established: one range * per dependency in this repo, quoted rather than invented, so bumping an @@ -278,6 +279,17 @@ function inRepoRangesOf(name: string): Record { * shipping. These literals live in `.ts` source, outside the objectui#3711 * version-claims gate's scan face, so this map is the only gate they have. * + * The third manifest is `commands/init.ts`'s (`buildInitPackageJson`), folded + * in by objectui#3892. It is the one an EXTERNAL user gets — `objectui init` is + * the first command they run — and it sat outside this table until then, which + * is how it came to ask for `@object-ui/*` at `^2.0.0` against packages + * publishing at 17.x while its toolchain ranges drifted a major behind the repo + * (vite `^7.3.1`, typescript `^5.9.3`). Its key set is a strict subset of this + * table's: it declares neither the seven plugins nor `lucide-react` / + * `react-router-dom`, because its generated sources import none of them — so + * the completeness check below unions the three and the per-range check finds + * each name in whichever manifest declares it. + * * - `root` — the repo root declares it; the generated range must match. * - `in-repo` — the root does not, but sibling manifests do, unanimously. * - `cli-version` — derived from this CLI's own version at generation time, not @@ -447,12 +459,15 @@ describe('generated app manifests', () => { expect(unusedVersionedDependencies(withUnused, plainFiles())).toEqual(['lucide-react']); }); - it('keeps both generated dependency maps under one anchor table', () => { - // Completeness: a range added to either generator without naming its anchor - // fails here, which is what kept the eight fossils invisible before. + it('keeps all three generated dependency maps under one anchor table', () => { + // Completeness: a range added to any generator without naming its anchor + // fails here, which is what kept the eight fossils invisible before — and, + // until the init manifest joined the union (objectui#3892), what let a whole + // third generator fossilise without ever failing anything. const declared = new Set([ ...Object.keys(allRangesOf(buildAppPackageJson(STANDALONE))), - ...Object.keys(allRangesOf(buildRoutedAppPackageJson())) + ...Object.keys(allRangesOf(buildRoutedAppPackageJson())), + ...Object.keys(allRangesOf(buildInitPackageJson('sample-app'))) ]); expect([...declared].sort()).toEqual(Object.keys(DEPENDENCY_ANCHORS).sort()); }); @@ -460,33 +475,91 @@ describe('generated app manifests', () => { it('sources every range from this repo instead of inventing one', () => { const routed = allRangesOf(buildRoutedAppPackageJson()); const plain = allRangesOf(buildAppPackageJson(STANDALONE)); + const init = allRangesOf(buildInitPackageJson('sample-app')); const cliVersion = readManifest(CLI_MANIFEST_PATH).version as string; for (const [name, anchor] of Object.entries(DEPENDENCY_ANCHORS)) { - const generated = routed[name] ?? plain[name]; - expect(generated, `${name} must be declared by at least one generator`).toBeTruthy(); - - if (anchor === 'cli-version') { - expect(generated, `${name} must track this CLI's own version`).toBe(`^${cliVersion}`); - continue; + // Every manifest that declares the name is judged, not just the first — + // one generator anchored and another fossilised is exactly the state + // objectui#3892 found, and reading `routed ?? plain ?? init` would have + // reported it green. + const declaredBy = { routed: routed[name], plain: plain[name], init: init[name] }; + expect( + Object.values(declaredBy).some((range) => range !== undefined), + `${name} must be declared by at least one generator` + ).toBe(true); + + for (const [generator, generated] of Object.entries(declaredBy)) { + if (generated === undefined) continue; + const where = `${generator} manifest's ${name}`; + + if (anchor === 'cli-version') { + expect(generated, `${where} must track this CLI's own version`).toBe(`^${cliVersion}`); + continue; + } + + if (anchor === 'root') { + const rootRange = rootRangeOf(name); + expect(rootRange, `${name} must exist in the root manifest`).toBeTruthy(); + expect(generated, `${where} must match the repo root`).toBe(rootRange); + continue; + } + + const byRange = inRepoRangesOf(name); + const ranges = Object.keys(byRange); + expect(ranges.length, `${name} must be declared in-repo to anchor to`).toBeGreaterThan(0); + expect( + ranges.sort(), + `in-repo manifests disagree on ${name}: ${JSON.stringify(byRange)} — settle on one range first` + ).toHaveLength(1); + expect(generated, `${where} must match its in-repo range`).toBe(ranges[0]); } + } + }); - if (anchor === 'root') { - const rootRange = rootRangeOf(name); - expect(rootRange, `${name} must exist in the root manifest`).toBeTruthy(); - expect(generated, `${name} must match the repo root`).toBe(rootRange); - continue; - } + it('names every range the pre-fix init manifest had drifted on', () => { + // The reverse verification for objectui#3892, direction predicted before + // running: feed the anchor rule the literal map `commands/init.ts` shipped + // and every one of its 13 ranges must be judged WRONG — the two + // `@object-ui/*` at `^2.0.0` against a CLI at 17.x (the reported defect), + // and the eleven toolchain fossils the measurement turned up beside it. + // Thirteen, not two, is the size of the drift. + // + // The direction is plain red rather than the inverted shape objectui#5009 + // hit, because this rule compares a generated string against a repo fact: + // there is no schema underneath it to re-judge the same input differently. + const preFix: Record = { + '@object-ui/components': '^2.0.0', + '@object-ui/react': '^2.0.0', + react: '^19.2.0', + 'react-dom': '^19.2.0', + '@tailwindcss/postcss': '^4.1.18', + '@types/react': '^19.2.13', + '@types/react-dom': '^19.2.6', + '@vitejs/plugin-react': '^5.1.3', + autoprefixer: '^10.4.23', + postcss: '^8.5.6', + tailwindcss: '^4.1.18', + typescript: '^5.9.3', + vite: '^7.3.1' + }; + const cliVersion = readManifest(CLI_MANIFEST_PATH).version as string; - const byRange = inRepoRangesOf(name); - const ranges = Object.keys(byRange); - expect(ranges.length, `${name} must be declared in-repo to anchor to`).toBeGreaterThan(0); - expect( - ranges.sort(), - `in-repo manifests disagree on ${name}: ${JSON.stringify(byRange)} — settle on one range first` - ).toHaveLength(1); - expect(generated, `${name} must match its in-repo range`).toBe(ranges[0]); - } + const drifted = Object.entries(preFix) + .filter(([name, range]) => { + const anchor = DEPENDENCY_ANCHORS[name]; + if (anchor === 'cli-version') return range !== `^${cliVersion}`; + if (anchor === 'root') return range !== rootRangeOf(name); + return range !== Object.keys(inRepoRangesOf(name))[0]; + }) + .map(([name]) => name) + .sort(); + + expect(drifted).toEqual(Object.keys(preFix).sort()); + // And the manifest shipping today is the exact complement: nothing drifted. + expect(Object.keys(allRangesOf(buildInitPackageJson('sample-app'))).sort()).toEqual( + Object.keys(preFix).sort() + ); }); it('keeps the root and in-repo anchors consistent wherever both declare one', () => { diff --git a/packages/cli/src/__tests__/cli-bin.test.ts b/packages/cli/src/__tests__/cli-bin.test.ts index c972b64fbf..0c1b9d92bc 100644 --- a/packages/cli/src/__tests__/cli-bin.test.ts +++ b/packages/cli/src/__tests__/cli-bin.test.ts @@ -196,14 +196,18 @@ describe('@object-ui/cli bin', () => { describe('init', () => { let work: string; + let appDir: string; + // Scaffolded once in `beforeAll` rather than by the first `it`, so the + // assertions below do not silently depend on test ordering (objectui#3892 + // added three of them to what was a single self-contained case). beforeAll(() => { work = mkdtempSync(join(tmpdir(), 'objectui-cli-init-')); + appDir = join(work, 'sample-app'); + const res = run(['init', 'sample-app', '-t', 'simple'], { cwd: work }); + expect(res.code, res.stdout + res.stderr).toBe(0); }); it('scaffolds a project with the simple template', () => { - const res = run(['init', 'sample-app', '-t', 'simple'], { cwd: work }); - expect(res.code, res.stdout + res.stderr).toBe(0); - const appDir = join(work, 'sample-app'); for (const f of [ 'app.json', 'package.json', @@ -219,5 +223,50 @@ describe('@object-ui/cli bin', () => { const schema = JSON.parse(readFileSync(join(appDir, 'app.json'), 'utf-8')); expect(schema).toHaveProperty('type'); }); + + it('writes no tailwind.config.js, because v4 would never read it', () => { + // objectui#3892, mirroring what objectui#3852 did for the temp-app + // generators. The scaffold's pipeline is Tailwind 4 end to end + // (`@tailwindcss/postcss` in `postcss.config.js`, `@import 'tailwindcss'` + // in `src/index.css`), and v4 reads a JS config only when a stylesheet + // points `@config` at it. The file this used to write was inert: an + // authoritative-looking `content` list nothing consumed. + // + // Asserted through the real bin rather than over a file map, because + // `init()` writes with `fs` directly — an absence is only meaningful + // where the writes actually happen. + expect(existsSync(join(appDir, 'tailwind.config.js'))).toBe(false); + expect(existsSync(join(appDir, 'tailwind.config.ts'))).toBe(false); + expect(readFileSync(join(appDir, 'src/index.css'), 'utf-8')).not.toContain('@config'); + // The v4 pipeline it is inert *relative to*, so the absence above cannot + // be read as "this scaffold is not on Tailwind at all". + expect(readFileSync(join(appDir, 'postcss.config.js'), 'utf-8')).toContain( + `'@tailwindcss/postcss': {}` + ); + expect(readFileSync(join(appDir, 'src/index.css'), 'utf-8')).toContain( + `@import 'tailwindcss';` + ); + }); + + it('versions the scaffold against the CLI that wrote it, not a literal', () => { + // objectui#3892's reported defect, gated where the user meets it: the + // manifest asked for `@object-ui/*` at `^2.0.0` while the CLI and every + // platform package publish at 17.x from one `fixed` changeset group, so + // `npm install` in a fresh scaffold resolved a major unrelated to the CLI + // that produced it. + // + // `app-generator.test.ts` anchors `buildInitPackageJson`'s ranges; this + // asserts `init()` actually WRITES that manifest, which is the half a + // unit test on the builder cannot see. + const cliVersion = JSON.parse(readFileSync(PKG_PATH, 'utf-8')).version as string; + const manifest = JSON.parse(readFileSync(join(appDir, 'package.json'), 'utf-8')) as { dependencies: Record; devDependencies: Record }; + + expect(manifest.dependencies['@object-ui/react']).toBe(`^${cliVersion}`); + expect(manifest.dependencies['@object-ui/components']).toBe(`^${cliVersion}`); + expect(manifest.dependencies['@object-ui/react']).not.toBe('^2.0.0'); + // A spot check that the toolchain half is written too — the full anchor + // judgement lives in `app-generator.test.ts`. + expect(manifest.devDependencies.vite).not.toBe('^7.3.1'); + }); }); }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 848b25bc58..a10ccfcb6d 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -10,6 +10,12 @@ import { existsSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import chalk from 'chalk'; +import { + platformPackageRange, + REACT_RANGE, + SCAFFOLD_DEV_DEPENDENCIES +} from '../utils/scaffold-dependencies.js'; + interface InitOptions { template: string; } @@ -357,6 +363,42 @@ const templates = { }, }; +/** + * The `package.json` `objectui init` writes into a new user project. + * + * Split out of `init()` so it can be judged without running the command: + * `app-generator.test.ts`'s `DEPENDENCY_ANCHORS` now covers this manifest + * alongside the two temp-app generators, which is the gate it had none of + * before (objectui#3892 — the literals live in `.ts` source, so objectui#3711's + * version-claims scan never saw them either). + * + * Not one range is written here. Every value comes from + * `utils/scaffold-dependencies.ts`, which is also what the app generators read; + * see that module for why the `@object-ui/*` range is derived from the CLI's + * own version rather than declared. + */ +export function buildInitPackageJson(name: string): Record { + const platformRange = platformPackageRange(); + return { + name, + version: '0.1.0', + private: true, + type: 'module', + scripts: { + dev: 'vite', + build: 'tsc && vite build', + preview: 'vite preview', + }, + dependencies: { + '@object-ui/components': platformRange, + '@object-ui/react': platformRange, + react: REACT_RANGE, + 'react-dom': REACT_RANGE, + }, + devDependencies: { ...SCAFFOLD_DEV_DEPENDENCIES }, + }; +} + export async function init(name: string, options: InitOptions) { const cwd = process.cwd(); const projectDir = join(cwd, name); @@ -450,36 +492,7 @@ dist console.log(chalk.green('✓ Created .gitignore')); // Create package.json - const packageJson = { - name, - version: '0.1.0', - private: true, - type: 'module', - scripts: { - dev: 'vite', - build: 'tsc && vite build', - preview: 'vite preview', - }, - dependencies: { - '@object-ui/components': '^2.0.0', - '@object-ui/react': '^2.0.0', - react: '^19.2.0', - 'react-dom': '^19.2.0', - }, - devDependencies: { - '@tailwindcss/postcss': '^4.1.18', - '@types/react': '^19.2.13', - '@types/react-dom': '^19.2.6', - '@vitejs/plugin-react': '^5.1.3', - autoprefixer: '^10.4.23', - postcss: '^8.5.6', - tailwindcss: '^4.1.18', - typescript: '^5.9.3', - vite: '^7.3.1', - }, - }; - - writeFileSync(join(targetDir, 'package.json'), JSON.stringify(packageJson, null, 2)); + writeFileSync(join(targetDir, 'package.json'), JSON.stringify(buildInitPackageJson(name), null, 2)); console.log(chalk.green('✓ Created package.json')); // Create vite.config.ts @@ -497,15 +510,14 @@ export default defineConfig({ writeFileSync(join(targetDir, 'vite.config.ts'), viteConfig); console.log(chalk.green('✓ Created vite.config.ts')); - // Create tailwind.config.js - const tailwindConfig = `/** @type {import('tailwindcss').Config} */ -export default { - content: ['./index.html', './src/**/*.{ts,tsx}'], -}; -`; - - writeFileSync(join(targetDir, 'tailwind.config.js'), tailwindConfig); - console.log(chalk.green('✓ Created tailwind.config.js')); + // No tailwind.config.js is written: this scaffold's CSS pipeline is Tailwind 4 + // (`postcss.config.js` names `@tailwindcss/postcss`, `src/index.css` does + // `@import 'tailwindcss'`), and v4 reads a JS config only when a stylesheet + // points `@config` at one — none does. The file this used to write was + // therefore inert: an authoritative-looking `content` list that nothing + // consumed, while v4's own source detection already covers `index.html` and + // `src/**` from the project root. objectui#3852 removed the same dead file + // from the temp-app generators; objectui#3892 removes it here. // Create postcss.config.js const postcssConfig = `export default { diff --git a/packages/cli/src/utils/app-generator.ts b/packages/cli/src/utils/app-generator.ts index 785aa9a41e..5fd57b0c52 100644 --- a/packages/cli/src/utils/app-generator.ts +++ b/packages/cli/src/utils/app-generator.ts @@ -8,10 +8,15 @@ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from 'fs'; import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; import chalk from 'chalk'; import * as yaml from 'js-yaml'; +import { + platformPackageRange, + REACT_RANGE, + SCAFFOLD_DEV_DEPENDENCIES +} from './scaffold-dependencies.js'; + export interface RouteInfo { path: string; filePath: string; @@ -111,9 +116,6 @@ export function scanPagesDirectory(pagesDir: string): RouteInfo[] { return routes; } -/** This package's own name, used to locate its manifest for the platform range. */ -const CLI_PACKAGE_NAME = '@object-ui/cli'; - /** * `@object-ui/*` packages the generated apps IMPORT, and therefore must declare. * @@ -143,76 +145,6 @@ const PLATFORM_RUNTIME_PACKAGES = [ '@object-ui/plugin-view' ] as const; -/** Memoised so the manifest walk happens at most once per process. */ -let cachedCliVersion: string | undefined; - -/** - * This CLI's own version, read from its own `package.json`. - * - * Walks up from this module rather than joining a fixed `../package.json`, - * because the same source sits at `src/utils/` in the repo and is bundled into - * `dist/` when published — a relative depth that is correct in one is wrong in - * the other. - */ -function cliVersion(): string { - if (cachedCliVersion !== undefined) return cachedCliVersion; - - let dir = dirname(fileURLToPath(import.meta.url)); - for (;;) { - const candidate = join(dir, 'package.json'); - if (existsSync(candidate)) { - const manifest = JSON.parse(readFileSync(candidate, 'utf-8')) as { - name?: string; - version?: string; - }; - if (manifest.name === CLI_PACKAGE_NAME && typeof manifest.version === 'string') { - cachedCliVersion = manifest.version; - return cachedCliVersion; - } - } - const parent = dirname(dir); - if (parent === dir) break; - dir = parent; - } - - throw new Error( - `Could not locate the ${CLI_PACKAGE_NAME} manifest to version the generated app's ` + - `platform dependencies against. Refusing to write a package.json with a guessed range.` - ); -} - -/** - * The range the generated apps declare for every `@object-ui/*` dependency. - * - * Derived from this CLI's own version instead of being written out as a - * literal, because `.changeset/config.json` puts `@object-ui/cli` in the SAME - * `fixed` group as every package in `PLATFORM_RUNTIME_PACKAGES`: they are - * released together and always carry the identical version. So `^` is both current and guaranteed to exist on the registry — whatever - * CLI version a user is running was published alongside its siblings. - * - * The literal it replaces was `^0.1.0` for packages published at 17.x, which - * never resolved to any published version at all (objectui#3827; the registry - * has no 0.1.0 for `@object-ui/react`). A literal here is not merely a fossil - * risk, it is a fossil generator: the fixed group re-versions on every release, - * so any hard-coded range is stale the next day. Deriving it removes the class. - */ -function platformPackageRange(): string { - return `^${cliVersion()}`; -} - -/** - * React's range in both generated manifests. - * - * Quotes the repo root verbatim (an exact pin there) rather than the wider - * `^18.0.0 || ^19.0.0` the platform packages accept as a peer. The peer range - * says what CAN work; this says what is actually exercised — inside this - * workspace the temp app resolves React by hoisting to the root, so the root's - * version is the only one the generated code has ever run against. Declaring - * `^18.3.1`, as it did until objectui#3827, named a major nothing here tests. - */ -const REACT_RANGE = '19.2.8'; - /** * `dependencies` for an app generated WITHOUT routing (`createTempApp`). * @@ -258,32 +190,6 @@ function buildRoutedAppDependencies(): Record { } -/** - * `devDependencies` shared by both generated apps (identical in both today). - * - * Every range is anchored to an in-repo manifest, and - * `app-generator.test.ts`'s `DEPENDENCY_ANCHORS` names the anchor for each one - * and fails on an unanchored addition. The Tailwind-side entries were held at - * v3 as an explicit `TAILWIND_V3_DEFERRED` ledger by objectui#3827, because - * bumping the range without rewriting the CSS pipeline beside it would produce - * an app that installs and renders unstyled. objectui#3852 rewrote that - * pipeline, so they are anchored to this repo's Tailwind 4 like everything - * else — `tailwindcss` to the root, `@tailwindcss/postcss` (v4's PostCSS - * plugin, a separate package) to the in-repo range every `postcss.config.*` - * here already names. - */ -const APP_DEV_DEPENDENCIES: Record = { - '@tailwindcss/postcss': '^4.3.3', - '@types/react': '19.2.18', - '@types/react-dom': '19.2.4', - '@vitejs/plugin-react': '^6.0.5', - autoprefixer: '^10.5.4', - postcss: '^8.5.26', - tailwindcss: '^4.3.3', - typescript: '^6.0.3', - vite: '^8.2.0' -}; - /** The generated `tsconfig.json`, identical for both generators. */ const APP_TSCONFIG = { compilerOptions: { @@ -358,7 +264,7 @@ export function buildAppPackageJson(context: Pick { private: true, type: 'module', dependencies: buildRoutedAppDependencies(), - devDependencies: { ...APP_DEV_DEPENDENCIES } + devDependencies: { ...SCAFFOLD_DEV_DEPENDENCIES } }; } diff --git a/packages/cli/src/utils/scaffold-dependencies.ts b/packages/cli/src/utils/scaffold-dependencies.ts new file mode 100644 index 0000000000..be3c5f0f8a --- /dev/null +++ b/packages/cli/src/utils/scaffold-dependencies.ts @@ -0,0 +1,147 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The one place a generated manifest's version ranges come from. + * + * This CLI writes a `package.json` from THREE generators, not two: + * `utils/app-generator.ts` builds the plain and routed temp apps that + * `objectui dev` runs, and `commands/init.ts` builds the project scaffold + * `objectui init` hands to an external user. objectui#3742 / objectui#3754 / + * objectui#3827 established the discipline for the first two — one range per + * dependency, quoted from this repo rather than invented, derived from this + * CLI's own version where the package ships in lockstep with it — and + * `app-generator.test.ts`'s `DEPENDENCY_ANCHORS` gates it. + * + * The init scaffold sat outside all of it (objectui#3892). It asked for + * `@object-ui/components`/`@object-ui/react` at `^2.0.0` while those packages + * publish at 17.x, so the first command an external user runs produced a + * project whose `npm install` resolved a major with nothing to do with the CLI + * that wrote it — the same defect objectui#3827 fixed one generator over, where + * the literal was `^0.1.0`. Its toolchain ranges had fossilised a major behind + * the repo besides (vite `^7.3.1` vs `^8.2.0`, typescript `^5.9.3` vs `^6.0.3`). + * + * Both halves are cured by not having a literal to fossilise: this module owns + * the values, all three generators read them, and the anchor table judges the + * union. Copying a range into a fourth generator is still possible, but it now + * fails the completeness gate instead of shipping. + */ + +import { existsSync, readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +/** This package's own name, used to locate its manifest for the platform range. */ +const CLI_PACKAGE_NAME = '@object-ui/cli'; + +/** Memoised so the manifest walk happens at most once per process. */ +let cachedCliVersion: string | undefined; + +/** + * This CLI's own version, read from its own `package.json`. + * + * Walks up from this module rather than joining a fixed `../package.json`, + * because the same source sits at `src/utils/` in the repo and is bundled into + * `dist/` when published — a relative depth that is correct in one is wrong in + * the other. + */ +export function cliVersion(): string { + if (cachedCliVersion !== undefined) return cachedCliVersion; + + let dir = dirname(fileURLToPath(import.meta.url)); + for (;;) { + const candidate = join(dir, 'package.json'); + if (existsSync(candidate)) { + const manifest = JSON.parse(readFileSync(candidate, 'utf-8')) as { + name?: string; + version?: string; + }; + if (manifest.name === CLI_PACKAGE_NAME && typeof manifest.version === 'string') { + cachedCliVersion = manifest.version; + return cachedCliVersion; + } + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + + throw new Error( + `Could not locate the ${CLI_PACKAGE_NAME} manifest to version the generated app's ` + + `platform dependencies against. Refusing to write a package.json with a guessed range.` + ); +} + +/** + * The range every generated manifest declares for every `@object-ui/*` package. + * + * Derived from this CLI's own version instead of being written out as a + * literal, because `.changeset/config.json` puts `@object-ui/cli` in the SAME + * `fixed` group as every platform package a generated app declares: they are + * released together and always carry the identical version. So `^` is both current and guaranteed to exist on the registry — whatever + * CLI version a user is running was published alongside its siblings. + * + * The literals it replaces were `^0.1.0` in the app generators (objectui#3827) + * and `^2.0.0` in the init scaffold (objectui#3892), for packages published at + * 17.x. Neither resolved to anything the CLI had been tested against; `^0.1.0` + * resolved to nothing published at all. A literal here is not merely a fossil + * risk, it is a fossil generator: the fixed group re-versions on every release, + * so any hard-coded range is stale the next day. Deriving it removes the class. + */ +export function platformPackageRange(): string { + return `^${cliVersion()}`; +} + +/** + * React's range in every generated manifest. + * + * Quotes the repo root verbatim (an exact pin there) rather than the wider + * `^18.0.0 || ^19.0.0` the platform packages accept as a peer. The peer range + * says what CAN work; this says what is actually exercised — inside this + * workspace the temp app resolves React by hoisting to the root, so the root's + * version is the only one the generated code has ever run against. Declaring + * `^18.3.1`, as the app generators did until objectui#3827, or `^19.2.0`, as + * the init scaffold did until objectui#3892, names a version nothing here + * tests: the second is not even wrong about the major, which is exactly how it + * survived unnoticed. + */ +export const REACT_RANGE = '19.2.8'; + +/** + * `devDependencies` shared by every generated manifest (identical in all three). + * + * Every range is anchored to an in-repo manifest, and + * `app-generator.test.ts`'s `DEPENDENCY_ANCHORS` names the anchor for each one + * and fails on an unanchored addition. The Tailwind-side entries were held at + * v3 as an explicit `TAILWIND_V3_DEFERRED` ledger by objectui#3827, because + * bumping the range without rewriting the CSS pipeline beside it would produce + * an app that installs and renders unstyled. objectui#3852 rewrote that + * pipeline, so they are anchored to this repo's Tailwind 4 like everything + * else — `tailwindcss` to the root, `@tailwindcss/postcss` (v4's PostCSS + * plugin, a separate package) to the in-repo range every `postcss.config.*` + * here already names. + * + * The init scaffold declared this exact key set at its own, older literals + * until objectui#3892 — same nine packages, every range behind, two of them by + * a major. Sharing the map rather than re-anchoring a second copy is what makes + * a repo-side bump reach all three generators in one edit; a generator that + * ever needs to differ can spread this and override the one key, which stays + * visible to the anchor table. + */ +export const SCAFFOLD_DEV_DEPENDENCIES: Record = { + '@tailwindcss/postcss': '^4.3.3', + '@types/react': '19.2.18', + '@types/react-dom': '19.2.4', + '@vitejs/plugin-react': '^6.0.5', + autoprefixer: '^10.5.4', + postcss: '^8.5.26', + tailwindcss: '^4.3.3', + typescript: '^6.0.3', + vite: '^8.2.0' +};