diff --git a/apps/blocks/registry.json b/apps/blocks/registry.json index f169df0..720f7ca 100644 --- a/apps/blocks/registry.json +++ b/apps/blocks/registry.json @@ -269,7 +269,7 @@ ], "docs": "## Usage\n\nCreate one registry for the application, adapt navigation at the host boundary, and render the palette near the root layout. Pages can register commands while mounted with `usePageCommands`.\n\n```tsx\nimport { createCommandRegistry, kbd } from '@constructive-io/command-palette';\nimport { CommandPalette } from '@/blocks/command-palette/command-palette';\n\nconst registry = createCommandRegistry({\n groups: [{ id: 'navigation', label: 'Navigation', priority: 1 }],\n commands: [{\n id: 'settings',\n label: 'Open settings',\n type: 'navigation',\n group: 'navigation',\n href: '/settings',\n shortcut: kbd(',', 'mod')\n }]\n});\n\n router.push(href)} />;\n```\n\nThe installed block owns presentation and keyboard interaction. The headless package owns command registration, execution, multi-step state, and background-task lifecycle. Route authorization, action permissions, errors, and business workflows remain the host application's responsibility.", "dependencies": [ - "@constructive-io/command-palette@^0.6.0", + "@constructive-io/command-palette", "lucide-react", "motion" ], @@ -436,7 +436,7 @@ "description": "The leaf-independent Console Kit shell, runtime, discovery, and single modular Zustand store.", "docs": "`console-kit-core` installed the shell, runtime, semantic routing, callback boundary, and one per-instance modular Zustand store. Core intentionally includes no feature view, so add selected `console-module-*` items.\n\nDegraded states: explicit endpoint, current `_meta`, introspection, capability, and adapter evidence fail closed independently; installation never grants authority.\n\nGuide: https://constructive-io.github.io/blocks/blocks/console-kit/", "dependencies": [ - "@constructive-io/data@^0.8.0", + "@constructive-io/data", "@tanstack/react-query", "graphql", "lucide-react", @@ -566,7 +566,7 @@ "description": "A current-_meta-only application data explorer with application-table filtering and spreadsheet CRUD.", "docs": "`feature-pack-data` installed a provider-neutral view and `.constructive/feature-packs/data.json`. Import from `@/blocks/feature-packs/data/data-feature-pack`. The host must supply the view resource, policy, and actions; add `console-module-data` when Console Kit should own discovery and Constructive integration.\n\nDegraded states: The host owns Sheets state and endpoint binding; incompatible metadata stays explicit, and PostgreSQL privileges and RLS decide every query and mutation.\n\nGuide: https://constructive-io.github.io/blocks/blocks/features/data/", "dependencies": [ - "@constructive-io/data@^0.8.0", + "@constructive-io/data", "lucide-react" ], "registryDependencies": [ diff --git a/apps/registry/scripts/build.ts b/apps/registry/scripts/build.ts index b436fe7..e6953d6 100644 --- a/apps/registry/scripts/build.ts +++ b/apps/registry/scripts/build.ts @@ -33,6 +33,7 @@ import { createRegistryModuleOwnership, deriveAliasDependencies, deriveOwnedRegistryDependencies, + pinWorkspaceDependencies, portableTargetForUiFile, rewriteConstructiveUiImports, type Registry, @@ -284,6 +285,9 @@ for (const { source, item } of preparedItems) { compiledSource.set(file.path, rewritten.source); } + if (item.dependencies) item.dependencies = pinWorkspaceDependencies(item.dependencies); + if (item.devDependencies) item.devDependencies = pinWorkspaceDependencies(item.devDependencies); + const derivedDependencies = new Set( deriveOwnedRegistryDependencies(item, compiledSource, moduleOwnership), ); diff --git a/apps/registry/scripts/compiler.test.ts b/apps/registry/scripts/compiler.test.ts index 94de12f..23dcf74 100644 --- a/apps/registry/scripts/compiler.test.ts +++ b/apps/registry/scripts/compiler.test.ts @@ -9,8 +9,8 @@ import { CONSOLE_KIT_ITEM_NAME, CONSOLE_MODULE_ITEM_NAME_PREFIX, CONSOLE_INSTALL_ROOT_DEPENDENCIES, - CONSTRUCTIVE_COMMAND_PALETTE_DEPENDENCY, - CONSTRUCTIVE_DATA_DEPENDENCY, + CONSTRUCTIVE_COMMAND_PALETTE_PACKAGE, + CONSTRUCTIVE_DATA_PACKAGE, CONSTRUCTIVE_UI_PACKAGE, CONSTRUCTIVE_SHEETS_PACKAGE, CONSTRUCTIVE_THEME_DEPENDENCY, @@ -26,8 +26,10 @@ import { createRegistryModuleOwnership, deriveAliasDependencies, deriveOwnedRegistryDependencies, + pinWorkspaceDependencies, portableTargetForUiFile, rewriteConstructiveUiImports, + workspaceDependencyRange, type Registry, type RegistryItem, } from './compiler'; @@ -349,13 +351,28 @@ test('rejects duplicate item names and install targets', () => { ); }); +test('derives pinned workspace ranges from the manifests Lerna bumps', () => { + const { version } = JSON.parse( + fs.readFileSync(path.join(repositoryRoot, 'packages/data/package.json'), 'utf8'), + ) as { version: string }; + + assert.equal(workspaceDependencyRange(CONSTRUCTIVE_DATA_PACKAGE), `@constructive-io/data@^${version}`); + assert.deepEqual( + pinWorkspaceDependencies([CONSTRUCTIVE_DATA_PACKAGE, '@constructive-io/data@^0.1.0', 'zod']), + [`@constructive-io/data@^${version}`, `@constructive-io/data@^${version}`, 'zod'], + ); +}); + function distributionContractFixture(): RegistryItem[] { return [ { name: 'constructive-theme', type: 'registry:style' }, { name: 'consumer', type: 'registry:block', - dependencies: [CONSTRUCTIVE_DATA_DEPENDENCY, CONSTRUCTIVE_COMMAND_PALETTE_DEPENDENCY], + dependencies: pinWorkspaceDependencies([ + CONSTRUCTIVE_DATA_PACKAGE, + CONSTRUCTIVE_COMMAND_PALETTE_PACKAGE, + ]), registryDependencies: [CONSTRUCTIVE_THEME_DEPENDENCY], }, { @@ -399,7 +416,9 @@ test('enforces the compiled registry distribution contract', () => { wrongRange.find((item) => item.name === 'consumer')!.dependencies = ['@constructive-io/data@^1.0.0']; assert.throws( () => assertRegistryDistributionContract(wrongRange), - new RegExp(`must depend on ${CONSTRUCTIVE_DATA_DEPENDENCY.replace(/[.^]/g, '\\$&')}`), + new RegExp( + `must depend on ${workspaceDependencyRange(CONSTRUCTIVE_DATA_PACKAGE).replace(/[.^]/g, '\\$&')}`, + ), ); const sourceOwnedPackage = structuredClone(distributionContractFixture()); diff --git a/apps/registry/scripts/compiler.ts b/apps/registry/scripts/compiler.ts index d579ce5..c6cb073 100644 --- a/apps/registry/scripts/compiler.ts +++ b/apps/registry/scripts/compiler.ts @@ -1,3 +1,6 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { isDeepStrictEqual } from 'node:util'; import ts from 'typescript'; @@ -5,10 +8,39 @@ export const CONSTRUCTIVE_UI_PACKAGE = '@constructive-io/ui'; export const CONSTRUCTIVE_SHEETS_PACKAGE = '@constructive-io/sheets'; export const CONSTRUCTIVE_NAMESPACE = '@constructive/'; export const CONSTRUCTIVE_THEME_DEPENDENCY = '@constructive/constructive-theme'; -export const CONSTRUCTIVE_DATA_DEPENDENCY = '@constructive-io/data@^0.8.0'; -export const CONSTRUCTIVE_COMMAND_PALETTE_DEPENDENCY = '@constructive-io/command-palette@^0.6.0'; +export const CONSTRUCTIVE_DATA_PACKAGE = '@constructive-io/data'; +export const CONSTRUCTIVE_COMMAND_PALETTE_PACKAGE = '@constructive-io/command-palette'; export const NODE_TYPE_REGISTRY_DEPENDENCY = 'node-type-registry@^1.11.0'; +/** Workspace packages whose registry dependency range follows their published version. */ +export const WORKSPACE_PINNED_PACKAGES = [ + CONSTRUCTIVE_DATA_PACKAGE, + CONSTRUCTIVE_COMMAND_PALETTE_PACKAGE, +] as const; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'); + +/** Caret range for a workspace package, read from the manifest Lerna bumps. */ +export function workspaceDependencyRange(packageName: string): string { + const directory = packageName.split('/').at(-1); + const manifestPath = path.join(repositoryRoot, 'packages', String(directory), 'package.json'); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name: string; version: string }; + if (manifest.name !== packageName) { + throw new Error(`${manifestPath} declares ${manifest.name}, expected ${packageName}.`); + } + return `${packageName}@^${manifest.version}`; +} + +/** Stamps the current workspace version onto every pinned dependency reference. */ +export function pinWorkspaceDependencies(dependencies: readonly string[]): string[] { + return dependencies.map((dependency) => { + const packageName = WORKSPACE_PINNED_PACKAGES.find( + (candidate) => dependency === candidate || dependency.startsWith(`${candidate}@`), + ); + return packageName ? workspaceDependencyRange(packageName) : dependency; + }); +} + export const FEATURE_PACK_IDS = [ 'data', 'auth', @@ -788,10 +820,9 @@ function referencesPackage(dependency: string, packageName: string): boolean { export function assertRegistryDistributionContract(items: readonly RegistryItem[]): void { const ownNames = new Set(items.map((item) => item.name)); - const requiredPackageRanges = new Map([ - ['@constructive-io/data', CONSTRUCTIVE_DATA_DEPENDENCY], - ['@constructive-io/command-palette', CONSTRUCTIVE_COMMAND_PALETTE_DEPENDENCY], - ]); + const requiredPackageRanges = new Map( + WORKSPACE_PINNED_PACKAGES.map((packageName) => [packageName, workspaceDependencyRange(packageName)]), + ); for (const item of items) { const dependencies = [ diff --git a/packages/schema-builder/registry.json b/packages/schema-builder/registry.json index 75e94e0..a6a78c2 100644 --- a/packages/schema-builder/registry.json +++ b/packages/schema-builder/registry.json @@ -14,7 +14,7 @@ "schema" ], "dependencies": [ - "@constructive-io/data@^0.8.0", + "@constructive-io/data", "@dnd-kit/core", "@dnd-kit/utilities", "@fluentui/react-context-selector", diff --git a/packages/schema-builder/scripts/build-registry.ts b/packages/schema-builder/scripts/build-registry.ts index 4884345..3e41aab 100644 --- a/packages/schema-builder/scripts/build-registry.ts +++ b/packages/schema-builder/scripts/build-registry.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { - CONSTRUCTIVE_DATA_DEPENDENCY, + CONSTRUCTIVE_DATA_PACKAGE, NODE_TYPE_REGISTRY_DEPENDENCY, collectModuleSpecifiers, } from '../../../apps/registry/scripts/compiler'; @@ -62,7 +62,7 @@ buildSourceRegistry({ registrySubdirectory: 'blocks/schema-builder', targetPrefix: '@components/schema-builder', dependencies: [ - CONSTRUCTIVE_DATA_DEPENDENCY, + CONSTRUCTIVE_DATA_PACKAGE, '@dnd-kit/core', '@dnd-kit/utilities', '@fluentui/react-context-selector', diff --git a/packages/sheets/registry.json b/packages/sheets/registry.json index e0f5ba3..43ef01f 100644 --- a/packages/sheets/registry.json +++ b/packages/sheets/registry.json @@ -14,7 +14,7 @@ "data" ], "dependencies": [ - "@constructive-io/data@^0.8.0", + "@constructive-io/data", "@internationalized/date", "@remixicon/react", "@tanstack/react-form", diff --git a/packages/sheets/scripts/build-registry.ts b/packages/sheets/scripts/build-registry.ts index 4baf8f2..0616046 100644 --- a/packages/sheets/scripts/build-registry.ts +++ b/packages/sheets/scripts/build-registry.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { CONSTRUCTIVE_DATA_DEPENDENCY } from '../../../apps/registry/scripts/compiler'; +import { CONSTRUCTIVE_DATA_PACKAGE } from '../../../apps/registry/scripts/compiler'; import { buildSourceRegistry } from '../../../apps/registry/scripts/build-source-registry'; const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); @@ -21,7 +21,7 @@ buildSourceRegistry({ registrySubdirectory: 'blocks/sheets', targetPrefix: '@ui/sheets', dependencies: [ - CONSTRUCTIVE_DATA_DEPENDENCY, + CONSTRUCTIVE_DATA_PACKAGE, '@internationalized/date', '@remixicon/react', '@tanstack/react-form',