diff --git a/.changeset/25922-unpublish-app-shell.md b/.changeset/25922-unpublish-app-shell.md new file mode 100644 index 00000000000..49dcf93462c --- /dev/null +++ b/.changeset/25922-unpublish-app-shell.md @@ -0,0 +1,16 @@ +--- +'@object-ui/app-shell': patch +--- + +chore(ui)!: `@object-ui/app-shell` is no longer published to npm (#25922) + +Clause-②: `yes (narrowing)`: a published package leaves the registry; ADR-0087 marker per the lane's rules +Metadata-protocol: none + +**BREAKING** for anyone who installs `@object-ui/app-shell` from npm. The package exists to serve the ObjectOS Console, and a customisable console for a third-party host has no business demand today, so the package is now `private: true` and leaves the Changesets `fixed` group. `18.2.0` is the last published `@object-ui/app-shell`: the 18.2 Version PR versioned it before this change and the 18.2 publish ships it; no later version reaches npm, and the maintainer deprecates the package on npm after the 18.2 publish. Nothing else moves: the Console (`ui/apps/console`) and the `@object-ui/*` packages that import it are workspace consumers and keep importing it exactly as before, and its exports, views and Studio pages are unchanged. + +The host-console posture leaves the repository with it. The two examples that imported the package as a host would, `ui/examples/byo-backend-console` and `ui/examples/console-starter`, are deleted, and so is `ui/docs/app-shell-extraction-guide.md`, the guide to extracting the package into a host. The READMEs, the examples index and the contributor docs that pointed at them no longer do. + +Migration: there is no replacement package. `@object-ui/app-shell` FROM npm → TO none. A host that installs it stays on `@object-ui/app-shell@18.2.0`, the last published version, or runs the ObjectOS Console instead of embedding the shell. + + diff --git a/.changeset/config.json b/.changeset/config.json index ca7dc39bec5..b7949cd003d 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -81,7 +81,6 @@ "create-objectstack", "@objectstack/connector-openapi", "@objectstack/verify", - "@object-ui/app-shell", "@object-ui/auth", "@object-ui/cli", "@object-ui/collaboration", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6481c04a59a..8dc4a47e195 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3808,164 +3808,6 @@ importers: specifier: ^6.0.3 version: 6.0.3 - ui/examples/byo-backend-console: - dependencies: - '@object-ui/app-shell': - specifier: workspace:* - version: link:../../packages/app-shell - '@object-ui/components': - specifier: workspace:* - version: link:../../packages/components - '@object-ui/plugin-form': - specifier: workspace:* - version: link:../../packages/plugin-form - '@object-ui/plugin-view': - specifier: workspace:* - version: link:../../packages/plugin-view - '@object-ui/react': - specifier: workspace:* - version: link:../../packages/react - '@object-ui/types': - specifier: workspace:* - version: link:../../packages/types - react: - specifier: 19.3.0 - version: 19.3.0 - react-dom: - specifier: 19.3.0 - version: 19.3.0(react@19.3.0) - react-router-dom: - specifier: ^7.18.2 - version: 7.18.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - devDependencies: - '@tailwindcss/postcss': - specifier: ^4.3.3 - version: 4.3.3 - '@types/react': - specifier: 19.3.0 - version: 19.3.0 - '@types/react-dom': - specifier: 19.3.0 - version: 19.3.0(@types/react@19.3.0) - '@vitejs/plugin-react': - specifier: ^6.0.5 - version: 6.0.5(vite@8.3.5(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1)) - autoprefixer: - specifier: ^10.5.4 - version: 10.5.4(postcss@8.5.31) - postcss: - specifier: ^8.5.10 - version: 8.5.31 - tailwindcss: - specifier: ^4.3.3 - version: 4.3.3 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vite: - specifier: ^8.2.1 - version: 8.3.5(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1) - - ui/examples/console-starter: - dependencies: - '@object-ui/app-shell': - specifier: workspace:* - version: link:../../packages/app-shell - '@object-ui/auth': - specifier: workspace:* - version: link:../../packages/auth - '@object-ui/components': - specifier: workspace:* - version: link:../../packages/components - '@object-ui/i18n': - specifier: workspace:* - version: link:../../packages/i18n - '@object-ui/mobile': - specifier: workspace:* - version: link:../../packages/mobile - '@object-ui/plugin-calendar': - specifier: workspace:* - version: link:../../packages/plugin-calendar - '@object-ui/plugin-charts': - specifier: workspace:* - version: link:../../packages/plugin-charts - '@object-ui/plugin-chatbot': - specifier: workspace:* - version: link:../../packages/plugin-chatbot - '@object-ui/plugin-dashboard': - specifier: workspace:* - version: link:../../packages/plugin-dashboard - '@object-ui/plugin-designer': - specifier: workspace:* - version: link:../../packages/plugin-designer - '@object-ui/plugin-detail': - specifier: workspace:* - version: link:../../packages/plugin-detail - '@object-ui/plugin-editor': - specifier: workspace:* - version: link:../../packages/plugin-editor - '@object-ui/plugin-form': - specifier: workspace:* - version: link:../../packages/plugin-form - '@object-ui/plugin-grid': - specifier: workspace:* - version: link:../../packages/plugin-grid - '@object-ui/plugin-kanban': - specifier: workspace:* - version: link:../../packages/plugin-kanban - '@object-ui/plugin-list': - specifier: workspace:* - version: link:../../packages/plugin-list - '@object-ui/plugin-report': - specifier: workspace:* - version: link:../../packages/plugin-report - '@object-ui/plugin-view': - specifier: workspace:* - version: link:../../packages/plugin-view - '@object-ui/providers': - specifier: workspace:* - version: link:../../packages/providers - '@object-ui/react-runtime': - specifier: workspace:* - version: link:../../packages/react-runtime - '@objectstack/sdui-parser': - specifier: workspace:* - version: link:../../../packages/sdui-parser - react: - specifier: 19.3.0 - version: 19.3.0 - react-dom: - specifier: 19.3.0 - version: 19.3.0(react@19.3.0) - react-router-dom: - specifier: ^7.18.2 - version: 7.18.2(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - sonner: - specifier: ^2.0.8 - version: 2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - devDependencies: - '@tailwindcss/postcss': - specifier: ^4.3.3 - version: 4.3.3 - '@types/react': - specifier: 19.3.0 - version: 19.3.0 - '@types/react-dom': - specifier: 19.3.0 - version: 19.3.0(@types/react@19.3.0) - '@vitejs/plugin-react': - specifier: ^6.0.5 - version: 6.0.5(vite@8.3.5(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1)) - tailwindcss: - specifier: ^4.3.3 - version: 4.3.3 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vite: - specifier: ^8.2.1 - version: 8.3.5(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1) - ui/examples/hello-world: dependencies: '@object-ui/components': diff --git a/turbo.json b/turbo.json index 4b075928cb3..65bdba6619b 100644 --- a/turbo.json +++ b/turbo.json @@ -43,7 +43,6 @@ "$TURBO_ROOT$/ui/scripts/vite-*.ts", "$TURBO_ROOT$/ui/vitest.config.mts", "$TURBO_ROOT$/ui/tsconfig.json", - "$TURBO_ROOT$/ui/tsconfig.tier-base.json", "$TURBO_ROOT$/ui/apps/console/src/lib/auth-preflight.ts", "$TURBO_ROOT$/ui/eslint-rules/no-unprefixed-query-params.js", "$TURBO_ROOT$/ui/packages/cli/src/commands/generate.ts", diff --git a/ui/CONTRIBUTING.md b/ui/CONTRIBUTING.md index 7f96e932cda..fbe7ee522d0 100644 --- a/ui/CONTRIBUTING.md +++ b/ui/CONTRIBUTING.md @@ -99,10 +99,6 @@ git checkout -b feature/your-feature-name # including the visual designer, which mounts here rather than in a demo of its own) pnpm dev -# Run an example app (console-starter and byo-backend-console are the two that ship -# a dev server; hello-world has none, and schema-catalog is a data package) -pnpm --filter @object-ui/example-console-starter dev - # Run the documentation site pnpm site:dev ``` diff --git a/ui/QUICK_REFERENCE.md b/ui/QUICK_REFERENCE.md index 808180493d3..cbd2937dd0f 100644 --- a/ui/QUICK_REFERENCE.md +++ b/ui/QUICK_REFERENCE.md @@ -63,17 +63,12 @@ vitest 调用被拒绝:从包目录跑 vitest 会静默跑错测试集 (objectui Details in [AGENTS.md](./AGENTS.md) (“怎么跑测试”) and `scripts/vitest-invocation-guard.mjs`. -### Run Examples +### Examples -```bash -pnpm --filter @object-ui/example-console-starter dev # Fork-ready ObjectStack console -pnpm --filter @object-ui/example-byo-backend-console dev # ObjectUI on your own backend -``` - -Those are the only two dev servers under `examples/`: `hello-world` is a snippet to paste +No directory under `examples/` is a dev server: `hello-world` is a snippet to paste into your own app and `schema-catalog` is a data package, so neither declares a `dev` -script. [`examples/README.md`](./examples/README.md) is the index — which one to pick, -what to build first, and the port each one serves on. +script. [`examples/README.md`](./examples/README.md) is the index. The console to run is +`apps/console` (`pnpm dev`). ### Release (via changesets) @@ -87,7 +82,7 @@ pnpm changeset publish # Publish to npm (CI only) | Path | Purpose | | --- | --- | -| `packages/*` | 36 published packages (`@object-ui/*`), plus the private `vscode-extension`, `test-support` (test-only, never released) and `runner` (workspace demo app, never released) | +| `packages/*` | 35 published packages (`@object-ui/*`), plus the private `vscode-extension`, `test-support` (test-only, never released), `runner` (workspace demo app, never released) and `app-shell` (the Console's shell and views, no longer published after 18.2.0) | | `apps/console` | Full ObjectUI console app (Vite + React) | | `apps/site` | Public docs site at (fumadocs) | | `examples/*` | Runnable examples and the schema catalog — see [`examples/README.md`](./examples/README.md) | diff --git a/ui/README.md b/ui/README.md index e0c2fa33e8b..6d5b8b973ec 100644 --- a/ui/README.md +++ b/ui/README.md @@ -56,8 +56,6 @@ Pick the path that matches where you are starting from: | You want to… | Start here | Install | |---|---|---| | Render a JSON schema inside your React app | [`examples/hello-world`](examples/hello-world) | `@object-ui/react @object-ui/components` | -| Embed views in an existing app, against **your own** backend | [`examples/byo-backend-console`](examples/byo-backend-console) | `@object-ui/app-shell @object-ui/plugin-view @object-ui/react` | -| Stand up a complete console on an **ObjectStack** backend | [`examples/console-starter`](examples/console-starter) | fork the example | | Build a UI from a JSON file, no React code at all | [`@object-ui/cli`](packages/cli) | `npm install -g @object-ui/cli` | The [examples catalog](examples/README.md) explains each one in more depth. @@ -115,47 +113,6 @@ it is handed to the component the schema names — so a value passed as `data={ read nor refused, and the expression that wanted it is returned as its own source text, with nothing thrown and one line on the console. -### Bring your own backend - -Use the shell and views without the full console infrastructure — your routing, your auth, your API: - -```bash -npm install @object-ui/app-shell @object-ui/plugin-view @object-ui/react -``` - -```tsx -import type { FC } from 'react'; -import { AppShell } from '@object-ui/app-shell'; -import { ObjectView } from '@object-ui/plugin-view'; -import { ThemeProvider, SchemaRendererProvider } from '@object-ui/react'; -import type { DataSource } from '@object-ui/types'; - -// The two pieces you bring: the backend adapter you implement (see "Custom -// Data Sources" below) and your own sidebar component. -declare const myAPI: DataSource; -declare const MySidebar: FC; - -function MyConsole() { - return ( - - - }> - - - - - ); -} - -function ContactList() { - return ( - - ); -} -``` - -[`examples/byo-backend-console`](examples/byo-backend-console) is the complete working version, with a mock REST adapter. - ### No React at all ```bash @@ -167,18 +124,10 @@ objectui dev app.json # dev server on http://localhost:3000 Edit `app.json` to build your UI. -### Run the examples from this repo - -```bash -pnpm run setup # builds @objectstack/* at .objectstack-sha, then installs -pnpm -w build - -cd examples/console-starter # or examples/byo-backend-console -pnpm dev -``` +### The examples in this repo -`hello-world` ships no dev server: copy its `App.tsx` and `schema.json` into your own -Vite/Next.js app. `schema-catalog` is a data package — the canonical JSON schemas the +Neither example ships a dev server. `hello-world` is a snippet: copy its `App.tsx` and +`schema.json` into your own Vite/Next.js app. `schema-catalog` is a data package — the canonical JSON schemas the docs render, a smoke test mounts, and AI agents use as a few-shot corpus. ## Copy-Paste Schemas @@ -378,7 +327,7 @@ Grouped by dependency weight — atoms and fields stay light, heavy widgets live | Package | Description | |---------|-------------| -| **[@object-ui/app-shell](./packages/app-shell)** | Minimal application shell — the base of every console | +| **[@object-ui/app-shell](./packages/app-shell)** | The Console's shell, views and Studio; a private workspace package, last published to npm as 18.2.0 | | **[@object-ui/providers](./packages/providers)** | The upload adapter context the file and image fields upload through | | **[@object-ui/auth](./packages/auth)** | `AuthProvider`, `useAuth`, `AuthGuard`, sign-in forms | | **[@object-ui/permissions](./packages/permissions)** | Object / field / row-level permission guards and hooks | diff --git a/ui/content/docs/guide/objectos-integration.mdx b/ui/content/docs/guide/objectos-integration.mdx index 812cd2a2e3a..aedc4c8f139 100644 --- a/ui/content/docs/guide/objectos-integration.mdx +++ b/ui/content/docs/guide/objectos-integration.mdx @@ -681,7 +681,6 @@ test('create contact workflow', async ({ page }) => { - [ObjectStack Documentation](https://docs.objectstack.ai) - [ObjectUI Components Reference](/docs/components) - [ObjectQL Schemas](/docs/api/schema-reference#objectql-schemas) -- [Example: ObjectStack Console Starter](https://github.com/objectstack-ai/objectui/tree/main/examples/console-starter) - [Schema Catalog](/docs/guide/schema-catalog) - every schema rendered in these docs ## Support diff --git a/ui/docs/app-shell-extraction-guide.md b/ui/docs/app-shell-extraction-guide.md deleted file mode 100644 index 37cd4ec9c62..00000000000 --- a/ui/docs/app-shell-extraction-guide.md +++ /dev/null @@ -1,388 +0,0 @@ -# `@object-ui/app-shell` — Architecture Guide - -## Overview - -This document describes `@object-ui/app-shell` as it ships today, and the two -ways a host uses it: compose the full console from its building blocks, or take -only the layout container and bring your own routing, auth and backend. - -The package started as a minimal rendering engine for third-party hosts. It has -since become the home of the console itself: its routing tree, its auth and -organization pages, Studio, the metadata admin engine, the marketplace and the -AI chat pages all live here, and `apps/console` composes them. The sections -below describe that package, not the original plan. - -## What the package contains - -### `@object-ui/app-shell` - -**Entry**: one root entry, `.` (`src/index.ts`), plus `./styles.css`. There are -no subpath exports. - -**Size** (non-test TypeScript under `src/`): about 211,000 lines in about 600 -files, with about 1,700 test files beside them. By directory: - -| Directory | Lines | What it holds | -|:---|---:|:---| -| `views/metadata-admin` | ~97,000 | The generic metadata admin engine: directory, list, edit, history and diagnostics pages, the schema form, the per-type previews and inspectors, and its own designer string table (`i18n.ts`) | -| `views/studio-design` | ~22,000 | The Studio WYSIWYG design surface, its per-type canvases and the builder landing page | -| `views/` (the rest) | ~27,000 | The inner-SPA views: `ObjectView`, `RecordDetailView`, `RecordFormPage`, `DashboardView`, `PageView`, `ReportView`, search, flows, approvals | -| `console/` | ~22,000 | The console routing tree (`AppContent`, `ConsoleShell`) and its pages: auth, home, organizations, marketplace, AI chat | -| `hooks/`, `layout/`, `utils/`, `providers/`, `chrome/`, `context/`, the rest | ~43,000 | Shell chrome (header, sidebar, command palette, toasts), the metadata, adapter and expression providers, navigation and user-state hooks, runtime configuration and observability | - -**Exports**: the root entry publishes about 200 names (about 35 of them -type-only). They fall into these groups, in the order `src/index.ts` lists -them: - -- **The minimal shell** — `AppShell` (layout container) and its - `MinimalAppShellProps`. -- **Providers and hooks** — `AdapterProvider`, `MetadataProvider`, - `ExpressionProvider`, the settle signal (`useSettleSignal`, `whenIdle`, …), - recent items, favorites, navigation pins and sync, the AI-surface signal. -- **Console building blocks** — `ConsoleShell`, `ConnectedShell`, - `AuthenticatedRoute`, `RequireOrganization`, `SystemRedirect`, - `SetupRedirect`, and the host-app route policy (`resolveHostAppSegment`). -- **Layout chrome and top-level chrome** — `ConsoleLayout`, `AppHeader`, - `UnifiedSidebar`, `ConsolePageHeader`, `CommandPalette`, `ConsoleToaster`, - `ErrorBoundary`, `ThemeProvider`, the unsaved-edits guard. -- **Runtime configuration and observability** — `initRuntimeConfig`, - `getRuntimeConfig` and the branding / stage readers; Sentry wiring. -- **Inner-SPA views** — `ObjectView`, `RecordDetailView`, `RecordFormPage`, - `DashboardView`, `PageView`, `ReportView`, `SearchResultsPage`, - `FlowRunner`. -- **Default pages** — `DefaultAppContent`, `DefaultLoginPage`, - `DefaultRegisterPage`, `DefaultHomePage`, the organization pages, - `MarketplacePage`, `AiChatPage`, with the sign-up decision they share - (`decideSignUpOffer`). -- **Registries** — the app component registry (`registerAppComponent`), the - metadata resource, preview and inspector registries, and the Studio canvas - registry, so a plugin can override a page without forking it. -- **Metadata admin and Studio pages** — `MetadataDirectoryPage`, - `MetadataResourceRouter`, `SchemaForm`, `StudioDesignSurface`, - `BuilderLanding`, `PackagedAutomationPage`. -- **AI assistant bus** — `assistantBus`, connecting the metadata designers to - the chat. - -The [package README](../packages/app-shell/README.md) is the API reference for -these names. - -**Load-time registrations**: importing the entry registers, as a side effect, -the built-in admin components, the console's SDUI widgets (cloud connection, -product blocks, diagnostics), the record blocks (`record:attachments`, -`record:approvals`, `record:approval_decision`), the global search, -notifications, app launcher and nav menu blocks, and the metadata admin -built-ins. The built-in metadata designers load from a deferred chunk on the -first read of a designer registry, not during the entry's evaluation; the -console's first-load budget (`scripts/check-eager-closure-budget.mjs`) is sized -on that. - -**Dependencies**: `@object-ui/react`, `@object-ui/components`, -`@object-ui/fields`, `@object-ui/layout`, `@object-ui/core`, -`@object-ui/types`, `@object-ui/i18n`, `@object-ui/auth`, -`@object-ui/permissions`, `@object-ui/providers`, -`@object-ui/data-objectstack`, `@object-ui/collaboration`, -`@object-ui/plugin-editor`, `@objectstack/spec`, `@objectstack/formula`, -`@objectstack/lint`; the view plugins (`plugin-grid`, `plugin-list`, -`plugin-form`, `plugin-detail`, `plugin-view`, `plugin-dashboard`, -`plugin-calendar`, `plugin-kanban`, `plugin-charts`, `plugin-report`, -`plugin-designer`, `plugin-chatbot`) are peer dependencies. - -**Bundle size**: not one number. A host that imports only `AppShell` still -evaluates the entry and its load-time registrations; the console's eager -closure is measured and budgeted by `scripts/check-eager-closure-budget.mjs`, -not by this page. - -### `@object-ui/providers` - -**Purpose**: The upload adapter context - -**Exports**: -- `UploadProvider` / `useUpload` - The ambient upload adapter the file and image fields upload through -- `createObjectStackUploadAdapter`, `createS3Adapter`, `createAzureBlobAdapter`, `createObjectUrlAdapter` - -Its former `DataSourceProvider`, `MetadataProvider` and `ThemeProvider` retired -(objectui#13453). The data source is `SchemaRendererProvider` and the theme is -`ThemeProvider`, both from `@object-ui/react`; metadata is `MetadataProvider` from -`@object-ui/app-shell`. - -**Dependencies**: `@object-ui/types` - -## Who consumes it - -Inside this repository three workspace members depend on the package: - -- `apps/console` — the product console. It imports about 75 names from the - root entry, and composes the routing tree mostly through - `DefaultAppContent`, so components such as `ObjectView` and `ConsoleLayout` - reach it through that tree rather than by name. -- [`examples/console-starter`](../examples/console-starter/README.md) — the - minimal full console: `ConsoleShell`, `AuthenticatedRoute`, the `Default*` - pages and `DefaultAppContent`. -- [`examples/byo-backend-console`](../examples/byo-backend-console/README.md) — - the bring-your-own-backend embed. It imports `AppShell` only and renders data - through `@object-ui/plugin-view` and `@object-ui/react`. - -A name the root entry exports that nothing outside the package names is retired -from the entry, not kept for a caller that may come: the module that defines it -stays, and in-package code imports it from there. - -## Architecture Diagram - -``` -┌────────────────────────────────────────────────┐ -│ Host │ -│ apps/console · examples/console-starter │ -│ (or your own app — see byo-backend-console) │ -└───────────────────────┬────────────────────────┘ - │ -┌───────────────────────┴────────────────────────┐ -│ @object-ui/app-shell │ -│ - AppShell (minimal layout container) │ -│ - ConsoleShell / DefaultAppContent (routing) │ -│ - Default pages: auth, home, orgs, AI chat │ -│ - Inner-SPA views: ObjectView, RecordDetail… │ -│ - Metadata admin engine + Studio │ -│ - Providers, chrome, registries │ -└───────────────────────┬────────────────────────┘ - │ -┌───────────────────────┴────────────────────────┐ -│ @object-ui/react │ -│ - SchemaRenderer │ -│ - SchemaRendererProvider (data source) │ -│ - ThemeProvider │ -│ - useActionRunner │ -│ - Component Registry │ -└───────────────────────┬────────────────────────┘ - │ - ┌──────────────┬───┴──────────┬──────────────┐ - │ │ │ │ -┌────┴──────┐ ┌─────┴─────┐ ┌──────┴────┐ ┌───────┴──────┐ -│@object-ui/│ │@object-ui/│ │@object-ui/│ │ View plugins │ -│components │ │ fields │ │ layout │ │ (peer deps) │ -└───────────┘ └───────────┘ └───────────┘ └──────────────┘ -``` - -## Two ways to use it - -### The full console - -Compose the console's routing tree from the building blocks, as -`examples/console-starter` does: `ConsoleShell` around `AuthenticatedRoute`, -the `Default*` pages, and `DefaultAppContent` for the app routes. This brings -the whole package: its auth flow against the ObjectStack backend, Studio, the -metadata admin pages and the marketplace. - -### Bring your own backend - -Take `AppShell` for the layout and render data with the view plugins and -`@object-ui/react` against your own `DataSource`, as -`examples/byo-backend-console` and the examples below do. Routing, auth and the -API are yours. The view to embed is `ObjectView` from -`@object-ui/plugin-view`; app-shell's own `ObjectView` reads `objectName` from -the router (`useParams()`) and is meant for the console's route tree. - -The package's direction — what stays in a product-neutral renderer and what is -console product — is recorded in -[ADR-0060](./adr/0060-product-neutral-renderer.md) and held by -`packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.ts`. - -## Usage Examples - -### Example 1: Minimal Custom Console - -```tsx -import { AppShell } from '@object-ui/app-shell'; -import type { MinimalAppShellProps } from '@object-ui/app-shell'; -import { ObjectView } from '@object-ui/plugin-view'; -import { SchemaRendererProvider } from '@object-ui/react'; -import type { DataSource } from '@object-ui/types'; - -// The two things you bring. Both are typed from the surface these packages -// ship, so this snippet compiles on its own: `myAPI` is your backend adapter -// (the "Custom Data Source Interface" section below is the shape it needs), -// and `MySidebar` is your own component, returning whatever `AppShell` -// accepts for `sidebar`. -declare const myAPI: DataSource; -declare function MySidebar(): MinimalAppShellProps['sidebar']; - -function MyConsole() { - return ( - - }> - - - - ); -} - -function ContactList() { - return ( - - ); -} -``` - -### Example 2: Next.js Integration - -Both files below open with `'use client'`. Under the Next.js App Router every -module under `app/` is a **server** component by default, and the `@object-ui/*` -packages carry no `'use client'` banner of their own — so the client boundary has -to be declared here, in the files that import them. Without the directive the -layout's React context providers and the page's client view are both asked to -run on the server, where neither exists. - -`app/layout.tsx`: - -```tsx -'use client'; - -import { AppShell } from '@object-ui/app-shell'; -import type { MinimalAppShellProps } from '@object-ui/app-shell'; -import { ThemeProvider } from '@object-ui/react'; - -export default function RootLayout({ children }: { children: MinimalAppShellProps['children'] }) { - return ( - - {children} - - ); -} -``` - -`app/[object]/page.tsx`: - -```tsx -'use client'; - -import { ObjectView } from '@object-ui/plugin-view'; -import type { DataSource } from '@object-ui/types'; - -// Your backend adapter, imported from your own module. -declare const myAPI: DataSource; - -export default function Page({ params }: { params: { object: string } }) { - return ; -} -``` - -### Example 3: Embedded Widget - -```tsx -import { ObjectView } from '@object-ui/plugin-view'; -import { SchemaRendererProvider } from '@object-ui/react'; -import type { DataSource } from '@object-ui/types'; - -// Your backend adapter, as in Example 1 — declared here because every block on -// this page compiles on its own. -declare const myAPI: DataSource; - -function MyExistingApp() { - return ( -
-
My App Header
- - {/* Embed ObjectUI widget */} - - - - -
My App Footer
-
- ); -} - -function ContactWidget() { - return ; -} -``` - -## Custom Data Source Interface - -Third-party systems implement `DataSource`, which `@object-ui/types` **exports**. -Import it; never re-declare it locally. A local copy is a second contract that -drifts silently, and the components consume the exported one — `ObjectView`'s -`dataSource` prop is typed `DataSource`, so a stand-in that satisfies only a -hand-written look-alike is rejected at the call site. - -A minimal adapter implements the interface's six **required** members. Both -halves of that sentence are checked when this page compiles, rather than -asserted: - -```tsx -import type { DataSource } from '@object-ui/types'; - -// Which six they are. A member name that is not on the exported interface — -// `getMetadata`, say — fails this `Pick` rather than being read past. -type MinimalDataSource = Pick< - DataSource, - 'find' | 'findOne' | 'create' | 'update' | 'delete' | 'getObjectSchema' ->; - -// And that those six SUFFICE. Every other member of `DataSource` — `searchAll`, -// `bulk`, `getView`, `aggregate`, `exportDownload`, the import-job family — is optional, so -// a value with just the six is assignable to the whole interface. This line goes -// red the moment a seventh member becomes required. -declare const minimal: MinimalDataSource; -export const adapter: DataSource = minimal; -``` - -Example implementation. It is annotated `: DataSource`, which is what makes the -list above complete: leave a required member out and this block stops compiling, -so there is nothing here to elide. Parameter and return types are inferred from -the contract — read the signatures off it rather than off this page: - -```tsx -import type { DataSource } from '@object-ui/types'; - -export const myDataSource: DataSource = { - async find(resource, params) { - const res = await fetch(`/api/${resource}`, { - method: 'POST', - body: JSON.stringify(params), - }); - return res.json(); - }, - async findOne(resource, id) { - const res = await fetch(`/api/${resource}/${id}`); - return res.status === 404 ? null : res.json(); - }, - async create(resource, data) { - const res = await fetch(`/api/${resource}`, { method: 'POST', body: JSON.stringify(data) }); - return res.json(); - }, - async update(resource, id, data) { - const res = await fetch(`/api/${resource}/${id}`, { method: 'PATCH', body: JSON.stringify(data) }); - return res.json(); - }, - async delete(resource, id) { - const res = await fetch(`/api/${resource}/${id}`, { method: 'DELETE' }); - return res.ok; - }, - async getObjectSchema(objectName) { - const res = await fetch(`/api/metadata/${objectName}`); - return res.json(); - }, -}; -``` - -## Testing - -- Each package has its own suite: `pnpm --filter @object-ui/app-shell test` - runs app-shell's, from the `ui/` root (`vitest run --root ../..`). -- The code blocks on this page are type-checked by - `scripts/check-doc-snippet-types.mjs`, and every symbol a published README - imports is checked against the package's built types by - `scripts/check-readme-exports.mjs`. -- `examples/console-starter` carries its own tests under `test/`. - -## Documentation - -- [`@object-ui/app-shell` README](../packages/app-shell/README.md) — the API - reference for the root entry's names. -- [`examples/README.md`](../examples/README.md) — which example to start from. -- [ADR-0060](./adr/0060-product-neutral-renderer.md) — the product-neutral - renderer direction. - -## License - -MIT diff --git a/ui/examples/README.md b/ui/examples/README.md index d1ae4222579..655976b2b22 100644 --- a/ui/examples/README.md +++ b/ui/examples/README.md @@ -7,13 +7,10 @@ Runnable examples that show how to consume ObjectUI in different scenarios. Pick | Example | Use it when… | Backend | Bundle | What it gives you | |---|---|---|---|---| | [`hello-world/`](./hello-world) | You want the smallest possible `` demo to see the JSON → React pipeline. | None (in-memory schema) | Tiny | A single `App.tsx` rendering one schema. | -| [`byo-backend-console/`](./byo-backend-console) | You want to **embed** ObjectUI into your own app and connect it to **your own** REST/GraphQL backend. | Bring-your-own (mock REST shown) | ~50 KB | `@object-ui/app-shell` + `@object-ui/react`, custom router, custom `DataSource`. ~100 lines of integration code, no auth, no plugins. | -| [`console-starter/`](./console-starter) | You want a **fork-ready, opinionated console** with the full plugin set already wired against an **ObjectStack** backend. | ObjectStack (`/api/v1`) | Full | `@object-ui/app-shell` `ConsoleShell` + `AuthProvider` + every `@object-ui/plugin-*` (grid, kanban, calendar, charts, dashboard, designer, detail, form, list, report, view, chatbot). | -> **Rule of thumb:** -> - Learning the rendering protocol → `hello-world`. -> - Adding ObjectUI to an existing product / different backend → `byo-backend-console`. -> - Standing up a brand-new ObjectStack console → fork `console-starter`. +> The console itself is `apps/console`, built for ObjectOS. `@object-ui/app-shell`, the +> package its shell and views live in, is private and no longer published to npm, so there +> is no host-console example to fork. ### Also under `examples/` @@ -23,18 +20,7 @@ Runnable examples that show how to consume ObjectUI in different scenarios. Pick ## Running an example -`byo-backend-console` and `console-starter` are Vite apps. From the monorepo root: - -```bash -pnpm install -pnpm -w build -cd examples/console-starter # or byo-backend-console -pnpm dev -``` - -Each exposes its own dev server port (see its README). - -The other two directories are not dev servers: `hello-world` is a snippet to drop into +Neither directory is a dev server: `hello-world` is a snippet to drop into your own Vite/Next.js app, and `schema-catalog` is a data package verified with `pnpm --filter @object-ui/example-schema-catalog test`. @@ -44,4 +30,4 @@ your own Vite/Next.js app, and `schema-catalog` is a data package verified with 2. Mark it `private: true`. 3. Add an entry to the table above and link its README. 4. Reference workspace packages with `workspace:*`. -5. Keep the dependency footprint aligned with the example's purpose — heavy plugins belong in `console-starter`, not in lightweight integration demos. +5. Keep the dependency footprint aligned with the example's purpose — heavy plugins belong in `apps/console`, not in lightweight integration demos. diff --git a/ui/examples/byo-backend-console/README.md b/ui/examples/byo-backend-console/README.md deleted file mode 100644 index b4e05114bd9..00000000000 --- a/ui/examples/byo-backend-console/README.md +++ /dev/null @@ -1,232 +0,0 @@ -# Minimal Console Example - -**Building a Custom Console with ObjectUI in ~100 Lines** - -This example demonstrates how third-party systems can integrate ObjectUI components to build their own console without inheriting the full console infrastructure. - -## What This Example Shows - -- ✅ Custom routing with React Router -- ✅ Custom data adapter (mock REST API, not ObjectStack) -- ✅ Using `@object-ui/app-shell` for the page shell -- ✅ Using `@object-ui/plugin-view` for object views (`ObjectView`) -- ✅ Using `@object-ui/react` for the theme and the ambient data source -- ✅ No console dependencies -- ✅ ~100 lines of integration code - -## Key Differences from Full Console - -| Feature | Minimal Console | Full Console | -|---------|----------------|--------------| -| Lines of Code | ~100 | ~5000+ | -| Bundle Size | ~150KB | ~500KB+ | -| Auth | Mock/Custom | ObjectStack Auth | -| Routing | React Router (custom) | React Router (built-in) | -| Data Source | Mock REST API | ObjectStack API | -| Admin Pages | None | Users, Roles, Audit, etc. | -| App Management | None | Create/Edit Apps | - -## Running This Example - -```bash -# From monorepo root -pnpm install -pnpm build - -# Run the example -cd examples/byo-backend-console -pnpm dev - -# Open http://localhost:5174 -``` - -## Code Walkthrough - -### 1. Custom Data Adapter (`src/mockDataSource.ts`) - -Implements the `DataSource` interface to connect to your backend: - -```tsx -const mockDataSource = { - async find(objectName: string) { - // Call your API - return fetch(`/api/${objectName}`).then(r => r.json()); - }, - // ... other methods -}; -``` - -### 2. App Shell (`src/App.tsx`) - -Uses `@object-ui/app-shell` for the shell and `@object-ui/plugin-view` for the -object view: - -```tsx -import { Routes, Route, useParams } from 'react-router-dom'; -import { AppShell } from '@object-ui/app-shell'; -import { ObjectView } from '@object-ui/plugin-view'; -import { ThemeProvider, SchemaRendererProvider } from '@object-ui/react'; - -function App() { - return ( - - - }> - - } /> - - - - - ); -} - -// `ObjectView` takes the object name in its schema, not from the router, and -// its `dataSource` prop is required by the type. So a route renders a small -// component of your own, not the view directly. -function ObjectPage() { - const { objectName } = useParams<{ objectName: string }>(); - - return ( - - ); -} -``` - -### 3. Custom Routing (`src/router.tsx`) - -Full control over routes - no predefined structure: - -```tsx -import { mockDataSource } from './mockDataSource'; - -function AppRoutes() { - return ( - - } /> - } /> - } /> - {/* Your custom routes */} - - ); -} - -// One small component per object. `ObjectView` requires its `dataSource` -// prop, so pass your adapter in. -function ObjectListPage({ objectName }: { objectName: string }) { - return ( - - ); -} -``` - -## Customization Examples - -### Use Next.js Instead of React Router - -```tsx -// app/layout.tsx -import { AppShell } from '@object-ui/app-shell'; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return }>{children}; -} - -// app/[object]/page.tsx -'use client'; -import { ObjectView } from '@object-ui/plugin-view'; -import { dataSource } from '../../lib/dataSource'; // your adapter - -export default function ObjectPage({ params }: { params: { object: string } }) { - return ( - - ); -} -``` - -### Connect to Your Own API - -```tsx -const myDataSource = { - async find(objectName, params) { - return axios.get(`https://my-api.com/${objectName}`, { params }); - }, - async create(objectName, data) { - return axios.post(`https://my-api.com/${objectName}`, data); - }, - // ... implement other methods -}; -``` - -### Add Custom Authentication - -```tsx -function App() { - const { user, login, logout } = useMyAuth(); - - if (!user) { - return ; - } - - return ( - - }> - {/* ... */} - - - ); -} -``` - -## What You Get - -- **Full ObjectUI Rendering**: All view types (Grid, Kanban, List, Calendar, etc.) -- **All Field Types**: Text, Number, Select, Date, Lookup, etc. -- **Form Handling**: Modal and inline forms -- **Dashboard Support**: Widget-based dashboards -- **Custom Pages**: JSON-driven page layouts -- **Zero Console Bloat**: No admin pages, no app management - -## What You Don't Get - -- App management UI (create/edit apps) -- User/role/permission management pages -- Audit log viewer -- ObjectStack-specific features -- Pre-built authentication - -## When to Use This - -✅ **Use minimal console when:** -- You have your own backend API -- You want full control over routing and layout -- You need custom authentication -- You want to embed ObjectUI in an existing app -- Bundle size matters - -❌ **Use full console when:** -- You're using ObjectStack backend -- You need complete admin functionality -- You want everything pre-configured -- You prefer convention over configuration - -## Next Steps - -1. Customize the data adapter for your API -2. Add your own authentication -3. Customize the sidebar and navigation -4. Add custom pages and routes -5. Deploy to your infrastructure - -## License - -MIT diff --git a/ui/examples/byo-backend-console/index.html b/ui/examples/byo-backend-console/index.html deleted file mode 100644 index d8da12651df..00000000000 --- a/ui/examples/byo-backend-console/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Minimal Console - ObjectUI - - -
- - - diff --git a/ui/examples/byo-backend-console/package.json b/ui/examples/byo-backend-console/package.json deleted file mode 100644 index 09c59c92b62..00000000000 --- a/ui/examples/byo-backend-console/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@object-ui/example-byo-backend-console", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "Minimal console example demonstrating third-party ObjectUI integration", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint .", - "type-check": "tsc --noEmit", - "preview": "vite preview" - }, - "dependencies": { - "@object-ui/app-shell": "workspace:*", - "@object-ui/components": "workspace:*", - "@object-ui/plugin-form": "workspace:*", - "@object-ui/plugin-view": "workspace:*", - "@object-ui/react": "workspace:*", - "@object-ui/types": "workspace:*", - "react": "19.3.0", - "react-dom": "19.3.0", - "react-router-dom": "^7.18.2" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.3.3", - "@types/react": "19.3.0", - "@types/react-dom": "19.3.0", - "@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.1" - } -} diff --git a/ui/examples/byo-backend-console/postcss.config.js b/ui/examples/byo-backend-console/postcss.config.js deleted file mode 100644 index fc9500d2d3c..00000000000 --- a/ui/examples/byo-backend-console/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import tailwindcss from '@tailwindcss/postcss'; - -export default { - plugins: [tailwindcss()], -}; diff --git a/ui/examples/byo-backend-console/src/App.tsx b/ui/examples/byo-backend-console/src/App.tsx deleted file mode 100644 index 6c011dc7aef..00000000000 --- a/ui/examples/byo-backend-console/src/App.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { BrowserRouter, Routes, Route, Link, useParams } from 'react-router-dom'; -import { AppShell } from '@object-ui/app-shell'; -import { ObjectView } from '@object-ui/plugin-view'; -import { ObjectForm } from '@object-ui/plugin-form'; -import { ThemeProvider, SchemaRendererProvider } from '@object-ui/react'; -import { mockDataSource } from './mockDataSource'; - -/** - * Minimal Console Example - * - * Demonstrates third-party ObjectUI integration in ~100 lines of code. - * No console dependencies, full control over routing and layout. - */ - -function App() { - return ( - - - - }> - - } /> - } /> - } /> - } /> - - - - - - ); -} - -function Sidebar() { - return ( - - ); -} - -function Home() { - return ( -
-

Welcome to Minimal Console

-

- This is a demonstration of third-party ObjectUI integration using - @object-ui/app-shell and @object-ui/react. -

- -
-
-

Key Features

-
    -
  • No console dependencies
  • -
  • Custom routing with React Router
  • -
  • Mock data source (replace with your API)
  • -
  • ~100 lines of integration code
  • -
  • Full control over layout and navigation
  • -
-
- -
-

Try It Out

-
    -
  • - - View Contacts - -
  • -
  • - - View Accounts - -
  • -
-
-
- -
-

What This Demonstrates

-

- This example shows how to build a custom console using ObjectUI - components without inheriting the full console infrastructure. You can: -

-
    -
  • Use your own backend API (not ObjectStack)
  • -
  • Implement custom authentication
  • -
  • Define your own routes and navigation
  • -
  • Customize the layout and styling
  • -
  • Cherry-pick only the components you need
  • -
-
-
- ); -} - -/** - * The grid columns of each object's named view. A named view on an - * `object-view` node is the protocol's `ObjectListViewSchema`, which requires - * `columns` (objectui#7928), so each object lists the fields it declares in - * `mockDataSource.ts`. - */ -const LIST_COLUMNS: Record = { - contact: ['name', 'email', 'phone', 'notes'], - account: ['name', 'industry', 'website'], -}; - -function ObjectPage() { - const { objectName } = useParams<{ objectName: string }>(); - - return ( -
- -
- ); -} - -/** - * Standalone simple ObjectForm. Unlike the grid's modal create/view dialogs, - * this is the default `formType: 'simple'` form — so when the object declares - * `fieldGroups` (see mockDataSource), the form renders grouped sections. - */ -function ObjectFormPage() { - const { objectName } = useParams<{ objectName: string }>(); - - return ( -
-

- New {objectName} (simple form) -

- -
- ); -} - -/** - * Modal ObjectForm with explicit `sections` — exercises the Card-wrapped - * `FormSectionContainer` path (formType: 'modal'/'split'/'tabbed'/'wizard' all share - * it), as opposed to `ObjectFormPage` above which renders the flat - * divider-only path used by the default `formType: 'simple'` + `fieldGroups`. - */ -function ObjectFormModalPage() { - const { objectName } = useParams<{ objectName: string }>(); - - return ( - - ); -} - -export default App; diff --git a/ui/examples/byo-backend-console/src/index.css b/ui/examples/byo-backend-console/src/index.css deleted file mode 100644 index c1f26780e3d..00000000000 --- a/ui/examples/byo-backend-console/src/index.css +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Tailwind v4 setup for the example. - * - * This project depends on Tailwind v4 (`@tailwindcss/postcss`), so it MUST use - * the v4 entry syntax — NOT the legacy v3 `@tailwind base/components/utilities` - * directives (v4 ignores them, which is why the form/grid/app-shell rendered - * completely unstyled). - * - * Three pieces are required, mirroring apps/console/src/index.css: - * 1. `@import 'tailwindcss'` — Tailwind v4 engine + preflight. - * 2. `@import '@object-ui/app-shell/styles.css'` — shared variant/theming contract. - * 3. `@theme { --color-*: hsl(var(--*)) }` + `:root`/`.dark` token blocks — - * registers `bg-card` / `text-muted-foreground` / `border` etc. as real - * utilities AND gives them values. Without the `@theme` mapping those - * class names don't exist in v4. - * 4. `@source` globs — Tailwind only generates classes it can see; the class - * names live inside the `@object-ui/*` package sources, so those dirs must - * be scanned or the components stay unstyled. - */ - -@import 'tailwindcss'; -@import '@object-ui/app-shell/styles.css'; - -/* Scan this app + every ObjectUI package whose components we render. */ -@source '../src/**/*.{ts,tsx}'; -@source '../../../packages/app-shell/src/**/*.{ts,tsx}'; -@source '../../../packages/components/src/**/*.{ts,tsx}'; -@source '../../../packages/layout/src/**/*.{ts,tsx}'; -@source '../../../packages/fields/src/**/*.{ts,tsx}'; -@source '../../../packages/react/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-form/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-view/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-grid/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-list/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-detail/src/**/*.{ts,tsx}'; - -/* Map the shadcn-style HSL design tokens to Tailwind v4 color utilities. */ -@theme { - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 4px); - - --color-border: hsl(var(--border)); - --color-input: hsl(var(--input)); - --color-ring: hsl(var(--ring)); - --color-background: hsl(var(--background)); - --color-foreground: hsl(var(--foreground)); - --color-primary: hsl(var(--primary)); - --color-primary-foreground: hsl(var(--primary-foreground)); - --color-secondary: hsl(var(--secondary)); - --color-secondary-foreground: hsl(var(--secondary-foreground)); - --color-destructive: hsl(var(--destructive)); - --color-destructive-foreground: hsl(var(--destructive-foreground)); - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-foreground)); - --color-accent: hsl(var(--accent)); - --color-accent-foreground: hsl(var(--accent-foreground)); - --color-popover: hsl(var(--popover)); - --color-popover-foreground: hsl(var(--popover-foreground)); - --color-card: hsl(var(--card)); - --color-card-foreground: hsl(var(--card-foreground)); - --color-sidebar: hsl(var(--sidebar)); - --color-sidebar-foreground: hsl(var(--sidebar-foreground)); - --color-sidebar-primary: hsl(var(--sidebar-primary)); - --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); - --color-sidebar-accent: hsl(var(--sidebar-accent)); - --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); - --color-sidebar-border: hsl(var(--sidebar-border)); - --color-sidebar-ring: hsl(var(--sidebar-ring)); - - --color-success: hsl(var(--success)); - --color-success-foreground: hsl(var(--success-foreground)); - --color-warning: hsl(var(--warning)); - --color-warning-foreground: hsl(var(--warning-foreground)); -} - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - - --primary: 243 75% 59%; - --primary-foreground: 0 0% 100%; - - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 243 75% 59%; - - --radius: 0.5rem; - - --sidebar: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 243 75% 59%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 243 75% 59%; - - --success: 142 71% 45%; - --success-foreground: 0 0% 100%; - --warning: 38 92% 50%; - --warning-foreground: 0 0% 100%; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - - --primary: 239 84% 67%; - --primary-foreground: 0 0% 100%; - - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 239 84% 67%; - - --sidebar: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 239 84% 67%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 239 84% 67%; - - --success: 142 65% 50%; - --success-foreground: 0 0% 100%; - --warning: 38 92% 55%; - --warning-foreground: 0 0% 10%; - } - - * { - border-color: hsl(var(--border)); - } - - body { - background-color: hsl(var(--background)); - color: hsl(var(--foreground)); - } -} diff --git a/ui/examples/byo-backend-console/src/main.tsx b/ui/examples/byo-backend-console/src/main.tsx deleted file mode 100644 index a46835a4fc2..00000000000 --- a/ui/examples/byo-backend-console/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App'; -import './index.css'; - -createRoot(document.getElementById('root')!).render( - - - -); diff --git a/ui/examples/byo-backend-console/src/mockDataSource.ts b/ui/examples/byo-backend-console/src/mockDataSource.ts deleted file mode 100644 index fa98c73f80b..00000000000 --- a/ui/examples/byo-backend-console/src/mockDataSource.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Mock Data Source - * - * A simple in-memory data source that demonstrates the DataSource interface. - * In a real application, replace this with calls to your REST API, GraphQL, etc. - * - * The runtime (list / form / detail) drives every schema-aware component off - * `getObjectSchema(objectName)`, which must return the object's `fields` as a - * **map** (`{ [fieldName]: fieldDef }`) plus optional top-level `fieldGroups`. - * The `contact` object below declares `fieldGroups` so the runtime ObjectForm - * renders grouped sections — matching the designer. - */ - -interface DataSource { - find(objectName: string, params?: any): Promise; - findOne(objectName: string, id: string): Promise; - create(objectName: string, data: any): Promise; - update(objectName: string, id: string, data: any): Promise; - delete(objectName: string, id: string): Promise; - getObjectSchema(objectName: string): Promise; - getMetadata(): Promise; -} - -// Mock data storage -const mockData: Record = { - contact: [ - { id: '1', name: 'John Doe', email: 'john@example.com', phone: '555-1234', notes: 'VIP customer' }, - { id: '2', name: 'Jane Smith', email: 'jane@example.com', phone: '555-5678', notes: '' }, - ], - account: [ - { id: '1', name: 'Acme Corp', industry: 'Technology', website: 'acme.com' }, - { id: '2', name: 'Global Inc', industry: 'Manufacturing', website: 'global.com' }, - ], -}; - -/** - * Per-object schemas keyed by object name. `fields` is a map (not an array) and - * `contact` carries `fieldGroups` + per-field `group` to exercise grouped form - * sections at runtime. - */ -const objectSchemas: Record = { - contact: { - name: 'contact', - label: 'Contacts', - fieldGroups: [ - // `collapsible` renders a chevron toggle on the section header; `collapsed` - // would start the group closed. Each group spans the full form width. - { key: 'identity', label: 'Identity', collapsible: true }, - { key: 'contact_info', label: 'Contact Info', collapsible: true }, - ], - fields: { - name: { name: 'name', label: 'Name', type: 'text', required: true, group: 'identity' }, - email: { name: 'email', label: 'Email', type: 'email', group: 'contact_info' }, - phone: { name: 'phone', label: 'Phone', type: 'text', group: 'contact_info' }, - // No `group` → renders in the trailing ungrouped section. - notes: { name: 'notes', label: 'Notes', type: 'textarea' }, - }, - views: [{ id: 'grid', name: 'All Contacts', type: 'grid' }], - }, - account: { - name: 'account', - label: 'Accounts', - fields: { - name: { name: 'name', label: 'Name', type: 'text', required: true }, - industry: { name: 'industry', label: 'Industry', type: 'text' }, - website: { name: 'website', label: 'Website', type: 'url' }, - }, - views: [{ id: 'grid', name: 'All Accounts', type: 'grid' }], - }, -}; - -export const mockDataSource: DataSource = { - async find(objectName: string, _params?: any) { - await delay(300); // Simulate network delay - const data = mockData[objectName] || []; - return { - data, - total: data.length, - }; - }, - - async findOne(objectName: string, id: string) { - await delay(200); - const data = mockData[objectName] || []; - const record = data.find((r) => r.id === id); - if (!record) { - throw new Error(`Record not found: ${objectName}/${id}`); - } - return record; - }, - - async create(objectName: string, data: any) { - await delay(300); - const newId = String(mockData[objectName]?.length ?? 0) + '-' + (data.name || 'rec'); - const newRecord = { ...data, id: newId }; - - if (!mockData[objectName]) { - mockData[objectName] = []; - } - mockData[objectName].push(newRecord); - - return newRecord; - }, - - async update(objectName: string, id: string, data: any) { - await delay(300); - const records = mockData[objectName] || []; - const index = records.findIndex((r) => r.id === id); - - if (index === -1) { - throw new Error(`Record not found: ${objectName}/${id}`); - } - - const updatedRecord = { ...records[index], ...data }; - mockData[objectName][index] = updatedRecord; - - return updatedRecord; - }, - - async delete(objectName: string, id: string) { - await delay(300); - const records = mockData[objectName] || []; - const index = records.findIndex((r) => r.id === id); - - if (index === -1) { - throw new Error(`Record not found: ${objectName}/${id}`); - } - - mockData[objectName].splice(index, 1); - return true; - }, - - async getObjectSchema(objectName: string) { - await delay(150); - const schema = objectSchemas[objectName]; - if (!schema) { - throw new Error(`Unknown object: ${objectName}`); - } - return schema; - }, - - async getMetadata() { - await delay(200); - return { objects: Object.values(objectSchemas) }; - }, -}; - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/ui/examples/byo-backend-console/src/vite-env.d.ts b/ui/examples/byo-backend-console/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a00..00000000000 --- a/ui/examples/byo-backend-console/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/ui/examples/byo-backend-console/tsconfig.json b/ui/examples/byo-backend-console/tsconfig.json deleted file mode 100644 index 347989debed..00000000000 --- a/ui/examples/byo-backend-console/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "../../tsconfig.tier-base.json", - "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [ - { "path": "../../packages/app-shell" }, - { "path": "../../packages/providers" }, - { "path": "../../packages/react" }, - { "path": "../../packages/types" }, - { "path": "../../packages/components" } - ] -} diff --git a/ui/examples/byo-backend-console/vite.config.ts b/ui/examples/byo-backend-console/vite.config.ts deleted file mode 100644 index a4732d77cfd..00000000000 --- a/ui/examples/byo-backend-console/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - port: 5174, - }, -}); diff --git a/ui/examples/console-starter/.env.development b/ui/examples/console-starter/.env.development deleted file mode 100644 index 5d3cf0f7585..00000000000 --- a/ui/examples/console-starter/.env.development +++ /dev/null @@ -1,18 +0,0 @@ -# Development: the starter SPA and the ObjectStack API share ONE origin. -# -# Leave VITE_SERVER_URL EMPTY. `src/App.tsx`, `src/main.tsx` and everything -# inside @object-ui/app-shell coalesce an unset value to '' and then build a -# RELATIVE `/api/...` URL, which the Vite dev server proxies to the backend -# (`vite.config.ts` -> server.proxy['/api'], target `DEV_PROXY_TARGET` or -# http://localhost:3000). So the page at :5173 fetches :5173 and the proxy -# does the split-host hop. -# -# Why empty rather than `http://localhost:3000`: a non-empty value makes every -# relative-target `type: 'api'` action resolve CROSS-origin, and the action -# runtime does not attach Authorization / X-Tenant-ID / Accept-Language to a -# cross-origin fetch — a 401 on the standard `pnpm dev` stack. -# -# Pointing at a DIFFERENT backend: set DEV_PROXY_TARGET (keeps one origin) — -# DEV_PROXY_TARGET=https://demo.objectstack.ai pnpm dev -VITE_SERVER_URL= -VITE_USE_MOCK_SERVER=false diff --git a/ui/examples/console-starter/.env.production b/ui/examples/console-starter/.env.production deleted file mode 100644 index f93a7febbd3..00000000000 --- a/ui/examples/console-starter/.env.production +++ /dev/null @@ -1,18 +0,0 @@ -# Production: connect to the ObjectStack server. -# -# Defaults to same-origin (empty), matching `apps/console/.env.production`. -# This is a FORK-READY SCAFFOLD: whatever is committed here is the production -# origin every fork inherits, so it must not name one specific deployment. -# A committed `https://demo.objectstack.ai` was cleared from the sibling -# console for exactly that reason — it CORS-blocked the auth / i18n / -# discovery calls and left a blank page on every host but the demo one. -# -# Deployments that genuinely need a split origin inject VITE_SERVER_URL from -# their own deploy environment at build time, rather than committing it: -# -# VITE_SERVER_URL=https://your-objectstack-host pnpm build -# -# A split origin also requires the backend to allow CORS from the SPA origin -# and to issue auth cookies as `SameSite=None; Secure`. -VITE_SERVER_URL= -VITE_USE_MOCK_SERVER=false diff --git a/ui/examples/console-starter/README.md b/ui/examples/console-starter/README.md deleted file mode 100644 index 45f399287d9..00000000000 --- a/ui/examples/console-starter/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Console Starter — ObjectUI - -A fork-ready **console scaffold** wired against a real ObjectStack backend. -`src/App.tsx` owns the routing tree and is the file you edit; everything the user -then sees inside it — objects, fields, views, relationships, dashboards — is -served by the backend, not defined here. - -## What it demonstrates - -The whole app is ~70 lines of JSX in [`src/App.tsx`](./src/App.tsx), assembled from -building blocks exported by `@object-ui/app-shell` -([`ConsoleShell.tsx`](../../packages/app-shell/src/console/ConsoleShell.tsx)): - -| Piece | What it gives you | -|---|---| -| `ConsoleShell` | Top-level provider stack: theme, navigation, favorites, notifications, `Suspense`. Goes inside `BrowserRouter`, around `Routes`. | -| `AuthenticatedRoute` | `AuthGuard` + `ConnectedShell` + `RequireOrganization`, the guard for protected routes. `requireOrganization={false}` opts out (the `/organizations` route shows this). | -| `ConnectedShell` | The data layer — `AdapterProvider` (an `ObjectStackAdapter` at `VITE_SERVER_URL`) + `MetadataProvider`. | -| `SystemRedirect` | Legacy `/system/*` → `/apps/setup/*`. (`/` is routed by this file's own `` — the package publishes no `/` element, objectui#10042.) | -| `Default*` pages | Drop-in login / register / forgot-password / home / organizations screens — replace any one with your own component. | -| `DefaultAppContent` | Mounted at `/apps/:appName/*`. This is the console proper: layout, command palette, and the object / record / dashboard / report / page routes. | - -Auth is `AuthProvider` from `@object-ui/auth` pointed at -`${VITE_SERVER_URL}/api/v1/auth`. [`src/main.tsx`](./src/main.tsx) registers ten view -plugins by side-effect import (grid, kanban, calendar, charts, list, detail, view, -form, dashboard, report) and hands `I18nProvider` the loader in -[`src/loadLanguage.ts`](./src/loadLanguage.ts), which fetches the app's translations -from `${VITE_SERVER_URL}/api/v1/i18n/translations/:lang`. The server answers with a -spec `TranslationData` document; the loader namespaces it with `@object-ui/i18n`'s -`transformSpecTranslations` — the same branch `apps/console` takes — so -`useObjectLabel` reads your object and field labels. A payload that is already an -i18next namespace tree passes through unchanged. - -[`vite.config.ts`](./vite.config.ts) aliases 29 `@object-ui/*` specifiers to -`packages/*/src` so plugin registration hits one `ComponentRegistry` singleton — a -monorepo detail. Drop the aliases when you consume published packages. The list is -**closed under its own import graph** — aliased source imports only aliased -packages — and [`test/vite-alias-closure.test.ts`](./test/vite-alias-closure.test.ts) -re-derives that graph on every run so the list cannot silently drift out of date -again. - -## What it is **not** - -- **Not a schema or relationship modelling example.** Nothing in this directory - declares an object, a field or a relationship — there is no schema JSON here at - all; those live on the server. If you came here to see how relationships are - modelled, you want [`../schema-catalog/`](../schema-catalog/) and the docs - ([`content/docs/fields/lookup.mdx`](../../content/docs/fields/lookup.mdx) for - lookup / master-detail). -- **Not a bring-your-own-backend example.** The data layer is hardwired to - `ObjectStackAdapter`. For your own REST/GraphQL API see - [`../byo-backend-console/`](../byo-backend-console/). -- **Not an offline demo.** There is no mock server here; nothing past the login - screen renders without a live backend. - -## How to run - -```bash -# from the monorepo root -pnpm install - -cd examples/console-starter -pnpm dev # Vite; no server.port is set, so the default 5173 -``` - -**`pnpm dev` needs no prior build.** Every workspace package the app reaches is on -the alias list, so Vite serves all of them from `src`. This was not always true: -five packages the aliased sources import — `@object-ui/mobile`, -`@object-ui/providers`, `@objectstack/sdui-parser`, `@object-ui/plugin-editor`, -`@object-ui/react-runtime` — were missing from the list, fell back to node -resolution onto `packages/*/dist`, and produced 500s behind an empty `#root` with -nothing on the page to say why (objectui#3528). - -**`pnpm build` and `pnpm type-check` still need `pnpm -w build` first.** Those run -`tsc`, which resolves `@object-ui/*` through `node_modules` rather than through the -Vite aliases — and each package's `types` entry points at `dist/index.d.ts`, which -exists only after a build. Skip it and `tsc` reports `TS2307: Cannot find module -'@object-ui/app-shell' or its corresponding type declarations`: - -```bash -# from the monorepo root -pnpm -w build - -cd examples/console-starter -pnpm build -``` - -### Backend - -`VITE_SERVER_URL` is the one setting that matters — the adapter, auth, i18n and -action endpoints all hang off it. An empty value means same-origin, for when the -ObjectStack server serves the console itself. - -| File | Value | -|---|---| -| [`.env.development`](./.env.development) | empty (same-origin) | -| [`.env.production`](./.env.production) | empty (same-origin) | - -Both files ship empty. In dev, the Vite server proxies `/api/*` to -`http://localhost:3000` by default; to point dev at a different backend while -keeping one origin, set `DEV_PROXY_TARGET`, e.g. -`DEV_PROXY_TARGET=https://demo.objectstack.ai pnpm dev`. - -Point it at any ObjectStack server. If you need one locally, this repo's live-e2e -helper [`e2e/live/ci/start-backend.sh`](../../e2e/live/ci/start-backend.sh) boots a -real `objectstack dev` on port 4010 with a seeded admin — it is the CI lane's -script, so expect it to fetch the showcase app metadata and `npm install` -published `@objectstack/*` packages first. - -(`VITE_USE_MOCK_SERVER` appears in both `.env` files, copied from `apps/console`. -Nothing in this repo reads it. There is no mock mode.) - -### Without a backend - -Measured with nothing listening on `:3000`, `pnpm dev` on a free port, headless -Chromium on `/`: - -**`/` redirects to `/login`.** The root route is wrapped in `AuthenticatedRoute` -(objectui#4042), so the guard resolves the session before the landing redirect — -and the `/meta/*` reads behind it — ever mount. The branded boot screen (the product name -over "Opening your console…") still appears, but only as a *transient* frame -while `GET /api/v1/auth/get-session` is in flight; the redirect lands within a few -hundred milliseconds of that request being refused. `/login` then renders in full -but cannot sign in. The browser console shows `ERR_CONNECTION_REFUSED` for -`/api/v1/auth/get-session`, `/api/v1/auth/config` and -`/api/v1/i18n/translations/en`. - -**A stored credential does not change this.** `isAuthenticated` is -`user !== null && session !== null`, and both come only from the server's -`get-session` answer — `localStorage` holds a bearer *token*, never a session — so -seeding `auth-session-token` and reloading `/` still lands on `/login`. With the -backend down no cold load reaches an authenticated state, so nothing below the -guard renders. (Not measured: a session signed in against a live backend that is -then killed under it; until the next reload that one still holds its in-memory -session.) - -## Which example do I want? - -Full table in the [examples catalog](../README.md). Short version: - -- [`hello-world/`](../hello-world/) — the JSON → UI pipeline, one schema, no backend. -- [`byo-backend-console/`](../byo-backend-console/) — embed ObjectUI in your own app, against your own API. -- **`console-starter/`** (this one) — stand up a new ObjectStack console: fork it and edit `src/App.tsx`. -- [`schema-catalog/`](../schema-catalog/) — not an app; the canonical schema corpus used by the docs and tests. - -`apps/console/src/App.tsx` is the same composition with more routes — read it when -you outgrow this one. diff --git a/ui/examples/console-starter/index.html b/ui/examples/console-starter/index.html deleted file mode 100644 index 80b9aa443b9..00000000000 --- a/ui/examples/console-starter/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Console Starter - - -
- - - diff --git a/ui/examples/console-starter/package.json b/ui/examples/console-starter/package.json deleted file mode 100644 index aefd146dc7c..00000000000 --- a/ui/examples/console-starter/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@object-ui/example-console-starter", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "Opinionated, fork-ready console template built on @object-ui/app-shell with the full plugin set wired up.", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint .", - "test": "vitest run --root ../.. examples/console-starter/", - "type-check": "tsc --noEmit && tsc -p tsconfig.test.json", - "preview": "vite preview" - }, - "dependencies": { - "@object-ui/app-shell": "workspace:*", - "@object-ui/auth": "workspace:*", - "@object-ui/components": "workspace:*", - "@object-ui/i18n": "workspace:*", - "@object-ui/mobile": "workspace:*", - "@object-ui/plugin-calendar": "workspace:*", - "@object-ui/plugin-charts": "workspace:*", - "@object-ui/plugin-chatbot": "workspace:*", - "@object-ui/plugin-dashboard": "workspace:*", - "@object-ui/plugin-designer": "workspace:*", - "@object-ui/plugin-detail": "workspace:*", - "@object-ui/plugin-editor": "workspace:*", - "@object-ui/plugin-form": "workspace:*", - "@object-ui/plugin-grid": "workspace:*", - "@object-ui/plugin-kanban": "workspace:*", - "@object-ui/plugin-list": "workspace:*", - "@object-ui/plugin-report": "workspace:*", - "@object-ui/plugin-view": "workspace:*", - "@object-ui/providers": "workspace:*", - "@object-ui/react-runtime": "workspace:*", - "@objectstack/sdui-parser": "workspace:*", - "react": "19.3.0", - "react-dom": "19.3.0", - "react-router-dom": "^7.18.2", - "sonner": "^2.0.8" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.3.3", - "@types/react": "19.3.0", - "@types/react-dom": "19.3.0", - "@vitejs/plugin-react": "^6.0.5", - "tailwindcss": "^4.3.3", - "typescript": "^6.0.3", - "vite": "^8.2.1" - } -} diff --git a/ui/examples/console-starter/postcss.config.js b/ui/examples/console-starter/postcss.config.js deleted file mode 100644 index a34a3d560dc..00000000000 --- a/ui/examples/console-starter/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; diff --git a/ui/examples/console-starter/src/App.tsx b/ui/examples/console-starter/src/App.tsx deleted file mode 100644 index ceeb8bf41a7..00000000000 --- a/ui/examples/console-starter/src/App.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Minimal third-party console template. - * - * This file owns the routing tree — fork and edit it. The building blocks - * imported from @object-ui/app-shell (ConsoleShell, AuthenticatedRoute, - * Default* pages) encapsulate the provider stack and auth guard - * so you only write JSX. - * - * Common customisations: - * - add routes: drop a inside - * - swap auth: replace with your own component - * - skip orgs: - */ - -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider, useAuth } from '@object-ui/auth'; -import { Toaster } from 'sonner'; -import { - ConsoleShell, - AuthenticatedRoute, - SystemRedirect, - DefaultLoginPage, - DefaultRegisterPage, - DefaultForgotPasswordPage, - DefaultHomeLayout, - DefaultHomePage, - DefaultOrganizationsLayout, - DefaultOrganizationsPage, - DefaultAppContent, -} from '@object-ui/app-shell'; - -const AUTH_URL = `${import.meta.env.VITE_SERVER_URL || ''}/api/v1/auth`; - -/** Wraps `DefaultHomeLayout` so the FAB picks up the signed-in user id. */ -function HomeRoute() { - const { user } = useAuth(); - return ( - - - - ); -} - -export function App() { - return ( - - - - - - } /> - } /> - } /> - - - - } /> - - - - } /> - } /> - - - - } /> - {/* This starter lands on the launcher, and routes `/` there - * itself: objectui#10042 removed the published `/` element, so - * `/` has exactly one resolver in this repo and it is the - * console's own. Still guarded (not a bare `ConnectedShell`) so an - * unauthenticated visitor goes to /login instead of firing a round - * of doomed 401 `/meta/*` reads first — objectui#4042. */} - - - - } /> - } /> - - - - - ); -} diff --git a/ui/examples/console-starter/src/index.css b/ui/examples/console-starter/src/index.css deleted file mode 100644 index 8e9709fd5be..00000000000 --- a/ui/examples/console-starter/src/index.css +++ /dev/null @@ -1,128 +0,0 @@ -@import 'tailwindcss'; -@import '@object-ui/app-shell/styles.css'; - -@source '../src/**/*.{ts,tsx}'; -@source '../../../packages/app-shell/src/**/*.{ts,tsx}'; -@source '../../../packages/auth/src/**/*.{ts,tsx}'; -@source '../../../packages/components/src/**/*.{ts,tsx}'; -@source '../../../packages/layout/src/**/*.{ts,tsx}'; -@source '../../../packages/fields/src/**/*.{ts,tsx}'; -@source '../../../packages/react/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-dashboard/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-report/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-form/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-grid/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-calendar/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-kanban/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-charts/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-chatbot/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-list/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-detail/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-view/src/**/*.{ts,tsx}'; -@source '../../../packages/plugin-designer/src/**/*.{ts,tsx}'; - -@theme { - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 4px); - - --color-border: hsl(var(--border)); - --color-input: hsl(var(--input)); - --color-ring: hsl(var(--ring)); - --color-background: hsl(var(--background)); - --color-foreground: hsl(var(--foreground)); - --color-primary: hsl(var(--primary)); - --color-primary-foreground: hsl(var(--primary-foreground)); - --color-secondary: hsl(var(--secondary)); - --color-secondary-foreground: hsl(var(--secondary-foreground)); - --color-destructive: hsl(var(--destructive)); - --color-destructive-foreground: hsl(var(--destructive-foreground)); - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-foreground)); - --color-accent: hsl(var(--accent)); - --color-accent-foreground: hsl(var(--accent-foreground)); - --color-popover: hsl(var(--popover)); - --color-popover-foreground: hsl(var(--popover-foreground)); - --color-card: hsl(var(--card)); - --color-card-foreground: hsl(var(--card-foreground)); - --color-sidebar: hsl(var(--sidebar)); - --color-sidebar-foreground: hsl(var(--sidebar-foreground)); - --color-sidebar-primary: hsl(var(--sidebar-primary)); - --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); - --color-sidebar-accent: hsl(var(--sidebar-accent)); - --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); - --color-sidebar-border: hsl(var(--sidebar-border)); - --color-sidebar-ring: hsl(var(--sidebar-ring)); -} - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - --radius: 0.5rem; - --sidebar: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --sidebar: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - * { - border-color: hsl(var(--border)); - } - - body { - background-color: hsl(var(--background)); - color: hsl(var(--foreground)); - } -} diff --git a/ui/examples/console-starter/src/loadLanguage.ts b/ui/examples/console-starter/src/loadLanguage.ts deleted file mode 100644 index 9dbe6072efd..00000000000 --- a/ui/examples/console-starter/src/loadLanguage.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Load the app's translations for a language from - * `${VITE_SERVER_URL}/api/v1/i18n/translations/:lang`, in the shape - * `I18nProvider` merges into i18next. - * - * The server answers with a `@objectstack/spec` `TranslationData` document - * inside the REST envelope (`{ data: { locale, translations } }`). That - * document is NOT an i18next resource tree: `useObjectLabel` and the - * screen-flow runner read an app NAMESPACE (`app.objects.*`, `app.fields.*`, - * …), so the spec payload goes through `transformSpecTranslations` first. A - * payload that is already a namespaced tree (a mock or hand-rolled server) - * is returned as-is. - * - * Both halves of that branch come from `@object-ui/i18n` — the same two calls - * the reference console makes — so this template never carries its own copy of - * the transform and picks up new spec scopes with the package. Returning the - * spec payload untransformed put every authored label at the root of the - * resource tree, where nothing reads it (objectui#10349). - */ -import { isSpecTranslationData, transformSpecTranslations } from '@object-ui/i18n'; - -export async function loadLanguage(lang: string): Promise> { - try { - const serverUrl = import.meta.env.VITE_SERVER_URL || ''; - const res = await fetch(`${serverUrl}/api/v1/i18n/translations/${lang}`); - if (!res.ok) return {}; - const json = await res.json(); - const translations: Record = json?.data?.translations ?? json ?? {}; - return isSpecTranslationData(translations) - ? transformSpecTranslations(translations) - : translations; - } catch { - return {}; - } -} diff --git a/ui/examples/console-starter/src/main.tsx b/ui/examples/console-starter/src/main.tsx deleted file mode 100644 index 57fe3c1362a..00000000000 --- a/ui/examples/console-starter/src/main.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { I18nProvider } from '@object-ui/i18n'; -import './index.css'; -import { App } from './App'; -import { loadLanguage } from './loadLanguage'; - -import '@object-ui/plugin-grid'; -import '@object-ui/plugin-kanban'; -import '@object-ui/plugin-calendar'; -import '@object-ui/plugin-charts'; -import '@object-ui/plugin-list'; -import '@object-ui/plugin-detail'; -import '@object-ui/plugin-view'; -import '@object-ui/plugin-form'; -import '@object-ui/plugin-dashboard'; -import '@object-ui/plugin-report'; - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - , -); diff --git a/ui/examples/console-starter/test/loadLanguage.test.tsx b/ui/examples/console-starter/test/loadLanguage.test.tsx deleted file mode 100644 index d420937c76b..00000000000 --- a/ui/examples/console-starter/test/loadLanguage.test.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 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 starter's `loadLanguage` — the loader `src/main.tsx` hands to - * `I18nProvider` — must namespace a spec `TranslationData` payload where the - * readers look (objectui#10349). - * - * The server answers `/api/v1/i18n/translations/:lang` with a spec document - * inside the REST envelope. The loader used to return that document as-is, so - * it landed at the root of the i18next resource tree; `useObjectLabel` only - * reads app namespaces one level down, and an author starting from this - * template saw their own source labels in every locale, with no error. The - * loader now takes the reference console's branch — - * `isSpecTranslationData` ⇒ `transformSpecTranslations`, both from - * `@object-ui/i18n` — and these tests go through the real loader, then hand - * its answer to i18next with the `addResourceBundle` call `I18nProvider` makes - * and draw the reader the card names. - */ -import { describe, it, expect, vi, afterEach } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { createI18n, I18nProvider, useObjectLabel } from '@object-ui/i18n'; -import { loadLanguage } from '../src/loadLanguage'; - -/** Serve `body` as the endpoint's JSON answer. */ -function respond(body: unknown) { - vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, json: async () => body }))); -} - -/** Serve `translations` inside the spec REST envelope, as the server does. */ -function serve(translations: unknown) { - respond({ data: { locale: 'zh-CN', translations } }); -} - -/** The card's payload: an object label AND a field label. */ -const SPEC_PAYLOAD = { - objects: { crm_lead: { label: '线索', fields: { name: { label: '名称' } } } }, -}; - -afterEach(() => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); -}); - -describe('loadLanguage — a spec payload lands where the readers look (objectui#10349)', () => { - it('namespaces the spec payload under `app`, flattening its field labels', async () => { - serve(SPEC_PAYLOAD); - const resources = await loadLanguage('zh-CN'); - - expect(resources).toEqual({ - app: { - objects: { crm_lead: { label: '线索' } }, - fields: { crm_lead: { name: '名称' } }, - }, - }); - }); - - it('renders the translated object and field labels through `useObjectLabel`', async () => { - serve(SPEC_PAYLOAD); - const resources = await loadLanguage('zh-CN'); - const instance = createI18n({ defaultLanguage: 'zh-CN', detectBrowserLanguage: false }); - instance.addResourceBundle('zh-CN', 'translation', resources, true, true); - - function LeadHeader() { - const { objectLabel, fieldLabel } = useObjectLabel(); - return ( - <> -

{objectLabel({ name: 'crm_lead', label: 'Lead' })}

-

{fieldLabel('crm_lead', 'name', 'Name')}

- - ); - } - render( - - - , - ); - - expect(screen.getByRole('heading', { level: 1 }).textContent).toBe('线索'); - expect(screen.getByRole('heading', { level: 2 }).textContent).toBe('名称'); - }); -}); - -describe('loadLanguage — anything else passes through unchanged', () => { - it('an already-namespaced tree inside the envelope', async () => { - const flat = { - crm: { - objects: { crm_lead: { label: '线索' } }, - fields: { crm_lead: { name: '名称' } }, - }, - }; - serve(flat); - await expect(loadLanguage('zh-CN')).resolves.toEqual(flat); - }); - - it('a built-in override with no envelope, as a mock server may answer', async () => { - const flat = { common: { save: '保存' } }; - respond(flat); - await expect(loadLanguage('zh-CN')).resolves.toEqual(flat); - }); -}); - -describe('loadLanguage — an empty or failed fetch still yields `{}`', () => { - it('an empty translations document', async () => { - serve({}); - await expect(loadLanguage('zh-CN')).resolves.toEqual({}); - }); - - it('a null body', async () => { - respond(null); - await expect(loadLanguage('zh-CN')).resolves.toEqual({}); - }); - - it('an HTTP error', async () => { - vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, status: 500, json: async () => ({}) }))); - await expect(loadLanguage('zh-CN')).resolves.toEqual({}); - }); - - it('a network failure', async () => { - vi.stubGlobal('fetch', vi.fn(async () => { - throw new TypeError('Failed to fetch'); - })); - await expect(loadLanguage('zh-CN')).resolves.toEqual({}); - }); -}); diff --git a/ui/examples/console-starter/test/vite-alias-closure.test.ts b/ui/examples/console-starter/test/vite-alias-closure.test.ts deleted file mode 100644 index 591057b8dcd..00000000000 --- a/ui/examples/console-starter/test/vite-alias-closure.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -/** - * 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. - */ - -/** - * `vite.config.ts`'s workspace alias table is a hand-written list standing in - * for a real import graph, and the two drifted apart: five packages the aliased - * `src` files import were missing from it, so Vite fell back to node resolution - * and landed on each package's `dist` directory — which exists only after - * `pnpm -w build`, so a plain `pnpm dev` served 500s behind a blank `#root` with - * no on-page diagnosis (objectui#3528). - * - * This re-derives the graph instead of re-reading the list. It walks the - * example's own `src/` and follows every relative import and every - * `@object-ui/*` import into that package's `src`, transitively, and asserts the - * alias table covers the fixed point. Because it traverses into packages - * regardless of whether they are aliased today, a missing alias is reported - * together with everything that alias would have pulled in — adding one entry - * can never leave a second hole behind. - * - * Specifier extraction uses TypeScript's own preprocessor, not a regex. A - * hand-rolled import regex was tried first and silently under-reported: a lazy - * middle between `import` and `from` scans across statement boundaries, so the - * word "import" appearing in ordinary comment prose swallowed the next two real - * import statements on its way to a later `export ... from`, and two of the five - * missing packages disappeared from the answer. - */ - -import { describe, it, expect } from 'vitest'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; - -const exampleDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const repoRoot = path.resolve(exampleDir, '../..'); -const viteConfigPath = path.join(exampleDir, 'vite.config.ts'); - -/** `'@object-ui/x': path.resolve(__dirname, '../../packages/x/src'),` */ -const ALIAS_ENTRY = /'(@object-ui\/[^']+)':\s*path\.resolve\(__dirname,\s*'([^']+)'\)/g; - -function readAliasTable(): Map { - const source = fs.readFileSync(viteConfigPath, 'utf8'); - const table = new Map(); - for (const m of source.matchAll(ALIAS_ENTRY)) table.set(m[1], path.resolve(exampleDir, m[2])); - return table; -} - -const RESOLVABLE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']; - -function resolveModule(target: string): string | null { - if (fs.existsSync(target) && fs.statSync(target).isFile()) return target; - for (const ext of RESOLVABLE) { - if (fs.existsSync(target + ext)) return target + ext; - } - for (const ext of RESOLVABLE) { - const indexFile = path.join(target, `index${ext}`); - if (fs.existsSync(indexFile)) return indexFile; - } - // An ESM-correct specifier addresses the EMITTED file: `./Foo.js` is how a - // package whose build preserves specifiers has to spell `Foo.tsx`, because - // Node's resolver does not extension-search relative specifiers (objectui#4538, - // enforced per pull request by `pnpm check:esm-specifiers`). Tried only after - // every candidate above has failed, so a package that genuinely ships a `.js` - // next to its TypeScript still resolves to that file and nothing that already - // resolved changes answer. - // - // Load-bearing, and it degraded SILENTLY: the relative branch of the walk - // drops an unresolvable specifier with no record, so as packages converted to - // explicit extensions the walk kept shrinking while every assertion stayed - // green. Measured on `main` before objectui#5357: 236 relative specifiers - // dropped and 890 files walked, against 1242 once they resolve. Only the - // `filesWalked` floor could see it, and only once app-shell — the largest - // package — converted too and took the count to 401. - const asSource = target.replace(/\.(js|jsx|mjs|cjs)$/, ''); - if (asSource !== target) return resolveModule(asSource); - return null; -} - -/** - * The spellings a specifier that is *meant to be a module* can end with. Derived - * from `RESOLVABLE` so the two cannot drift, plus the two TypeScript module - * extensions that are legal to write but that `resolveModule` never has to - * probe for (a `.mts` source is imported as `.mjs`). - */ -const MODULE_EXTENSIONS = [...RESOLVABLE, '.mts', '.cts']; - -/** - * Does this relative specifier denote a module the walk is supposed to follow? - * - * `ts.preProcessFile` reports EVERY import specifier, including `./styles.css`, - * `./data.json` and `./logo.svg` — which `resolveModule` deliberately cannot - * resolve, because its candidate list is JS and TS only. Those are not walk - * failures, and recording them as such would fail this test for a reason that - * is not a defect. The closure contains two `.css` imports today, both of which - * happen to resolve only because `resolveModule` returns any path that exists - * on disk verbatim; a stylesheet that is virtual, generated, or shipped only in - * `dist` would land in the miss branch the moment it appeared. - * - * So the boundary is drawn on *intent*, not on whether resolution happened to - * succeed: no extension, or one of the JS/TS emitted extensions, is a module. - * Anything else is an asset and is skipped EXPLICITLY — recorded in - * `nonModuleSkipped` rather than dropped — so that the skip stays auditable and - * this branch can never again go quiet by accident. - * - * A specifier carrying a `?query` or `#hash` is a Vite resource specifier - * (`?raw`, `?url`, `?inline`, `?worker`) rather than a plain module path, and - * the walk cannot follow it meaningfully — it counts as a non-module skip. The - * closure contains none today. - */ -function isModuleSpecifier(specifier: string): boolean { - if (specifier.includes('?') || specifier.includes('#')) return false; - // `.` and `..` are extensionless directory imports, not an `.`-extension. - const ext = /\.[a-zA-Z0-9]+$/.exec(specifier)?.[0]; - if (!ext) return true; - return MODULE_EXTENSIONS.includes(ext.toLowerCase()); -} - -const packageSrc = (pkg: string) => - path.join(repoRoot, 'packages', pkg.slice('@object-ui/'.length), 'src'); - -const isWorkspacePackage = (pkg: string) => { - const src = packageSrc(pkg); - return fs.existsSync(src) && fs.statSync(src).isDirectory(); -}; - -/** `@object-ui/plugin-list/foo` -> `@object-ui/plugin-list` */ -const packageOf = (specifier: string) => specifier.split('/').slice(0, 2).join('/'); - -interface Closure { - /** every workspace package reached, mapped to the files that import it */ - packages: Map>; - /** the subset imported directly by the example's own src/ */ - direct: Set; - filesWalked: number; - unresolvable: string[]; - /** - * Relative specifiers the walk skipped on purpose because they are assets, - * not modules. Never asserted empty — this exists so the skip is observable - * instead of implicit, and so a fixture can prove the boundary is drawn where - * it is documented to be. - */ - nonModuleSkipped: string[]; -} - -/** - * Where Vite sends a workspace specifier: the FIRST alias entry, in table - * order, whose key equals the specifier or is a prefix of it ending at a `/`, - * with that key replaced by its target — the matching `@rollup/plugin-alias` - * applies to `resolve.alias`. With no matching entry it is the package-root - * guess (`packages//src` plus the subpath), so the walk still traverses a - * package that is not aliased today and the closure assertion names it. - * - * A bare package specifier lands where the guess always put it. The case this - * exists for is a SUBPATH whose module is not `src//index.*`: - * `@object-ui/types/zod` is `src/zod/index.zod.ts`, which the guess cannot - * reach and the dev server reaches only through the table's own subpath entry, - * listed BEFORE the bare package for exactly that reason. Resolving through the - * table models that and nothing looser: drop the subpath entry, or list it after - * the bare package, and Vite would land on `src/zod` — which this walk then - * reports as unresolvable, as before (pinned by the fixtures at the bottom). - */ -function viteAliasTarget(specifier: string, aliases: ReadonlyMap): string { - for (const [key, target] of aliases) { - if (specifier === key || specifier.startsWith(`${key}/`)) return target + specifier.slice(key.length); - } - const pkg = packageOf(specifier); - return packageSrc(pkg) + specifier.slice(pkg.length); -} - -function computeClosure( - entryDir: string = path.join(exampleDir, 'src'), - aliases: ReadonlyMap = readAliasTable(), -): Closure { - const packages = new Map>(); - const direct = new Set(); - const unresolvable: string[] = []; - const nonModuleSkipped: string[] = []; - const seen = new Set(); - - const exampleSrc = entryDir; - - function walk(file: string): void { - if (seen.has(file)) return; - seen.add(file); - if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file)) return; - // Tests are not part of what the dev server serves. - if (/\.(test|spec)\.[tj]sx?$/.test(file)) return; - - const code = fs.readFileSync(file, 'utf8'); - const specifiers = new Set(ts.preProcessFile(code, true, true).importedFiles.map((r) => r.fileName)); - - for (const specifier of specifiers) { - if (specifier.startsWith('.')) { - const resolved = resolveModule(path.resolve(path.dirname(file), specifier)); - if (resolved) { - walk(resolved); - continue; - } - // A miss used to vanish here with no record, which made - // `unresolvable` structurally empty for this entire specifier class: - // `expect(closure.unresolvable).toEqual([])` could not fail for a - // relative import no matter how many the walk failed to follow. That is - // how objectui#4538's and objectui#5214's conversions each truncated - // this walk while landing green — the `filesWalked` floor was the only - // signal there was, and it had enough slack to hide two pull requests. - if (isModuleSpecifier(specifier)) { - unresolvable.push( - `${specifier} (unresolvable relative import, imported by ${path.relative(repoRoot, file)})`, - ); - } else { - nonModuleSkipped.push(`${specifier} (imported by ${path.relative(repoRoot, file)})`); - } - continue; - } - // Third-party and @objectstack/* are real registry dependencies; only - // workspace packages are aliased to source. - if (!specifier.startsWith('@object-ui/')) continue; - - const pkg = packageOf(specifier); - if (!isWorkspacePackage(pkg)) { - unresolvable.push(`${specifier} (no packages/*/src, imported by ${path.relative(repoRoot, file)})`); - continue; - } - - if (!packages.has(pkg)) packages.set(pkg, new Set()); - packages.get(pkg)!.add(path.relative(repoRoot, file)); - if (file.startsWith(exampleSrc + path.sep)) direct.add(pkg); - - const resolved = resolveModule(viteAliasTarget(specifier, aliases)); - if (resolved) walk(resolved); - else unresolvable.push(`${specifier} (unresolvable under src, imported by ${path.relative(repoRoot, file)})`); - } - } - - for (const entry of fs.readdirSync(exampleSrc)) { - if (/\.(ts|tsx|js|jsx)$/.test(entry)) walk(path.join(exampleSrc, entry)); - } - - return { packages, direct, filesWalked: seen.size, unresolvable, nonModuleSkipped }; -} - -describe('console-starter vite alias table', () => { - const aliases = readAliasTable(); - const closure = computeClosure(); - - it('parses the alias table out of vite.config.ts', () => { - // Guards the regex above: if the config is reformatted so the entries stop - // matching, every other assertion here would pass vacuously. - expect(aliases.size).toBeGreaterThan(20); - expect(aliases.get('@object-ui/app-shell')).toBe( - path.join(repoRoot, 'packages/app-shell/src'), - ); - }); - - it('reaches the workspace graph it is meant to cover', () => { - // Same guard for the walker: a resolution regression that walked nothing - // would make the closure assertion trivially true. - // - // `unresolvable` now covers BOTH walk branches — bare `@object-ui/*` and - // relative — so a resolution regression is reported as itself, by name and - // importer, instead of only as a file count that drifted toward a floor. - expect(closure.filesWalked).toBeGreaterThan(500); - expect(closure.unresolvable).toEqual([]); - }); - - it('points every alias at a directory that exists', () => { - const broken = [...aliases.entries()].filter(([, target]) => !fs.existsSync(target)); - expect(broken.map(([k, v]) => `${k} -> ${path.relative(repoRoot, v)}`)).toEqual([]); - }); - - it('is closed under the import graph — no aliased source imports an unaliased workspace package', () => { - const missing = [...closure.packages.keys()] - .filter((pkg) => !aliases.has(pkg)) - .sort() - .map((pkg) => `${pkg} (imported by ${[...closure.packages.get(pkg)!].sort().join(', ')})`); - - // Anything listed here falls back to packages/*/dist at dev time, so - // `pnpm dev` without a prior `pnpm -w build` renders a blank page. - expect(missing).toEqual([]); - }); - - it('declares every workspace package the example imports directly', () => { - const manifest = JSON.parse( - fs.readFileSync(path.join(exampleDir, 'package.json'), 'utf8'), - ) as { dependencies?: Record }; - const declared = new Set(Object.keys(manifest.dependencies ?? {})); - const undeclared = [...closure.direct].filter((pkg) => !declared.has(pkg)).sort(); - expect(undeclared).toEqual([]); - }); -}); - -/** - * `closure.unresolvable` above is asserted empty, and on a healthy tree it IS - * empty — which is exactly the reading that cannot be trusted on its own. An - * assertion that receives nothing looks identical to one that cannot receive - * anything, and for the relative branch it *was* the second of those for three - * pull requests. - * - * So the empty reading is only worth what these fixtures are worth: the same - * walker, over a tree built on purpose, must report the misses that genuinely - * are misses and stay silent about the assets that are not. Pinning the class - * here means a future edit cannot re-blind the branch without turning this red. - */ -describe('the closure walker records relative misses instead of dropping them', () => { - /** Builds a throwaway source tree and walks it with the real `computeClosure`. */ - function withFixture(files: Record, run: (dir: string) => T): T { - const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'alias-closure-'))); - try { - for (const [name, content] of Object.entries(files)) { - const target = path.join(dir, name); - fs.mkdirSync(path.dirname(target), { recursive: true }); - fs.writeFileSync(target, content); - } - return run(dir); - } finally { - fs.rmSync(dir, { recursive: true, force: true }); - } - } - - it('walks `./Foo.js` through to the `Foo.tsx` on disk', () => { - // The case that produced every row of objectui#5386's table: a package whose - // build preserves specifiers must spell `Foo.tsx` as `./Foo.js`, and the - // walk has to follow it rather than lose the subtree behind it. - const closure = withFixture( - { - 'entry.ts': `import './Foo.js';\n`, - 'Foo.tsx': `import './Bar';\nexport const Foo = 1;\n`, - 'Bar.ts': `export const Bar = 2;\n`, - }, - (dir) => computeClosure(dir), - ); - - // entry.ts + Foo.tsx + Bar.ts — the subtree behind the `.js` spelling is - // reached, not dropped at the first re-export. - expect(closure.filesWalked).toBe(3); - expect(closure.unresolvable).toEqual([]); - }); - - it('reports a relative module specifier that does not resolve', () => { - // The counter-probe for the empty reading above: plant misses of both - // module spellings and require this walker to name them. - const closure = withFixture( - { - 'entry.ts': `import './missing-module.js';\nimport './gone';\n`, - }, - (dir) => computeClosure(dir), - ); - - expect(closure.unresolvable).toHaveLength(2); - expect(closure.unresolvable.join('\n')).toContain('./missing-module.js'); - expect(closure.unresolvable.join('\n')).toContain('./gone'); - }); - - it('skips an unresolvable asset specifier explicitly rather than reporting it', () => { - // The boundary condition: a stylesheet, image or font import is not a - // missing module. `ts.preProcessFile` reports them all, and `resolveModule` - // cannot resolve any of them, so recording them alongside real misses would - // manufacture failures that are not defects. - const closure = withFixture( - { - 'entry.ts': - `import './theme.css';\n` + - `import './logo.svg';\n` + - `import './font.woff2';\n` + - `import './data.json';\n` + - `import './raw.css?inline';\n` + - `import './real.js';\n`, - 'real.ts': `export const real = 1;\n`, - }, - (dir) => computeClosure(dir), - ); - - // None of the five assets is a defect... - expect(closure.unresolvable).toEqual([]); - // ...but each is accounted for, so the skip is a decision and not a drop. - expect(closure.nonModuleSkipped).toHaveLength(5); - expect(closure.nonModuleSkipped.join('\n')).toContain('./theme.css'); - expect(closure.nonModuleSkipped.join('\n')).toContain('./raw.css?inline'); - // The module alongside them still resolves and is walked. - expect(closure.filesWalked).toBe(2); - }); - - // objectui#8894 — the first workspace SUBPATH import inside this closure - // (`@object-ui/types/zod`, from plugin-dashboard and plugin-designer). Its - // module is `src/zod/index.zod.ts`, so it resolves only through the alias - // table's subpath entry; these three pin that the walk follows the table the - // way Vite does and is not loosened by it. - const TYPES_SRC = path.join(repoRoot, 'packages/types/src'); - const TYPES_ZOD = path.join(TYPES_SRC, 'zod/index.zod.ts'); - const subpathEntry = { 'entry.ts': `import { DashboardWidgetSchema } from '@object-ui/types/zod';\nexport { DashboardWidgetSchema };\n` }; - - it('resolves a workspace subpath through the alias entry Vite applies', () => { - const closure = withFixture(subpathEntry, (dir) => - computeClosure(dir, new Map([['@object-ui/types/zod', TYPES_ZOD], ['@object-ui/types', TYPES_SRC]])), - ); - expect(closure.unresolvable).toEqual([]); - // The validators' module and what it imports were walked, not just named. - expect(closure.filesWalked).toBeGreaterThan(2); - }); - - it('reports the subpath when the table has no entry for it — the package-root guess cannot reach it', () => { - const closure = withFixture(subpathEntry, (dir) => - computeClosure(dir, new Map([['@object-ui/types', TYPES_SRC]])), - ); - expect(closure.unresolvable.join('\n')).toContain('@object-ui/types/zod (unresolvable under src'); - }); - - it('reports the subpath when its entry comes AFTER the bare package — the first match wins, as in Vite', () => { - const closure = withFixture(subpathEntry, (dir) => - computeClosure(dir, new Map([['@object-ui/types', TYPES_SRC], ['@object-ui/types/zod', TYPES_ZOD]])), - ); - expect(closure.unresolvable.join('\n')).toContain('@object-ui/types/zod (unresolvable under src'); - }); - - it('classifies the specifier spellings that decide the boundary', () => { - // Directly pins `isModuleSpecifier`, so the line stays where it is - // documented even if the walk around it is rewritten. - for (const module of ['./Foo', '.', '..', '../pkg/src', './Foo.js', './Foo.ts', './Foo.mts']) { - expect(isModuleSpecifier(module)).toBe(true); - } - for (const asset of ['./a.css', './a.svg', './a.json', './a.woff2', './a.js?worker']) { - expect(isModuleSpecifier(asset)).toBe(false); - } - }); -}); diff --git a/ui/examples/console-starter/tsconfig.json b/ui/examples/console-starter/tsconfig.json deleted file mode 100644 index 6ed3fad1747..00000000000 --- a/ui/examples/console-starter/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - "types": ["vite/client"] - }, - "include": ["src"] -} diff --git a/ui/examples/console-starter/tsconfig.test.json b/ui/examples/console-starter/tsconfig.test.json deleted file mode 100644 index e37df7e90b5..00000000000 --- a/ui/examples/console-starter/tsconfig.test.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - // Type-checks this example's TESTS, which `tsconfig.json` does not read. - // - // That is correct of `tsconfig.json` — it is the app build config, its - // `include` is `["src"]`, and `test/` sits beside `src` rather than inside it. - // But it also means no `tsc` invocation would read the closure test at all, - // which is exactly the hole `scripts/check-type-check-coverage.mjs` exists to - // close (objectstack#4118, objectui#3968): a test the compiler never reads can - // assert a contract that was never checked, and then read as evidence that the - // contract holds. Chained off the `type-check` script in `package.json`. - // - // No `exclude` override is needed here, unlike - // `examples/schema-catalog/tsconfig.test.json`: this example's - // `tsconfig.json` has no `exclude` at all, so there is no inherited - // directory-form exclusion to undo. - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": true, - // The test walks the repository off disk and drives TypeScript's own - // preprocessor, so it needs the Node built-in declarations that the app's - // `["vite/client"]` alone does not provide. - "types": ["node", "vite/client"] - }, - "include": ["test/**/*.test.ts", "test/**/*.test.tsx"] -} diff --git a/ui/examples/console-starter/vite.config.ts b/ui/examples/console-starter/vite.config.ts deleted file mode 100644 index fd2fb1b042f..00000000000 --- a/ui/examples/console-starter/vite.config.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; - -// Workspace src/ aliases ensure plugin side-effect imports -// (ComponentRegistry.register) resolve to the same singleton instance, mirroring -// what apps/console does. Customers shipping a real app can drop these aliases -// once the @object-ui/* packages are published to a registry. -// -// The table must be CLOSED under the import graph it creates: every alias points -// at a package's `src`, and that source imports further @object-ui/* packages, -// which must be aliased too. A package left out falls back to node resolution -// and lands on that package's `dist`, which only exists after `pnpm -w build` — -// so `pnpm dev` on a fresh clone served 500s for those modules and rendered a -// blank `#root` with nothing on the page to say why (objectui#3528). Five -// packages had drifted out of the list that way. -// `test/vite-alias-closure.test.ts` re-derives the closure from the real import -// graph and fails when it drifts again — do not hand-maintain this list against -// memory. -const workspaceAliases: Record = { - '@object-ui/app-shell': path.resolve(__dirname, '../../packages/app-shell/src'), - '@object-ui/auth': path.resolve(__dirname, '../../packages/auth/src'), - '@object-ui/collaboration': path.resolve(__dirname, '../../packages/collaboration/src'), - '@object-ui/components': path.resolve(__dirname, '../../packages/components/src'), - '@object-ui/core': path.resolve(__dirname, '../../packages/core/src'), - '@object-ui/data-objectstack': path.resolve(__dirname, '../../packages/data-objectstack/src'), - '@object-ui/fields': path.resolve(__dirname, '../../packages/fields/src'), - '@object-ui/i18n': path.resolve(__dirname, '../../packages/i18n/src'), - '@object-ui/layout': path.resolve(__dirname, '../../packages/layout/src'), - '@object-ui/mobile': path.resolve(__dirname, '../../packages/mobile/src'), - '@object-ui/permissions': path.resolve(__dirname, '../../packages/permissions/src'), - '@object-ui/plugin-calendar': path.resolve(__dirname, '../../packages/plugin-calendar/src'), - '@object-ui/plugin-charts': path.resolve(__dirname, '../../packages/plugin-charts/src'), - '@object-ui/plugin-chatbot': path.resolve(__dirname, '../../packages/plugin-chatbot/src'), - '@object-ui/plugin-dashboard': path.resolve(__dirname, '../../packages/plugin-dashboard/src'), - '@object-ui/plugin-designer': path.resolve(__dirname, '../../packages/plugin-designer/src'), - '@object-ui/plugin-detail': path.resolve(__dirname, '../../packages/plugin-detail/src'), - '@object-ui/plugin-editor': path.resolve(__dirname, '../../packages/plugin-editor/src'), - '@object-ui/plugin-form': path.resolve(__dirname, '../../packages/plugin-form/src'), - '@object-ui/plugin-grid': path.resolve(__dirname, '../../packages/plugin-grid/src'), - '@object-ui/plugin-kanban': path.resolve(__dirname, '../../packages/plugin-kanban/src'), - '@object-ui/plugin-list': path.resolve(__dirname, '../../packages/plugin-list/src'), - '@object-ui/plugin-report': path.resolve(__dirname, '../../packages/plugin-report/src'), - '@object-ui/plugin-view': path.resolve(__dirname, '../../packages/plugin-view/src'), - '@object-ui/providers': path.resolve(__dirname, '../../packages/providers/src'), - // Note that the `react` key does NOT shadow the `react-runtime` one below — - // an alias key matches an importee only when it equals it or is a prefix - // ending at a '/' boundary, so the order of these keys is presentation, not - // precedence. (Spelled without the full quoted specifiers on purpose: a - // quoted `@object-ui/...` followed by a colon reads as a 30th table entry to - // anything scanning this file, and that decoy already fooled one scan.) - '@object-ui/react': path.resolve(__dirname, '../../packages/react/src'), - '@object-ui/react-runtime': path.resolve(__dirname, '../../packages/react-runtime/src'), - '@objectstack/sdui-parser': path.resolve(__dirname, '../../../packages/sdui-parser/src'), - // Subpath before bare package — see the note in `apps/console/vite.config.ts`. - '@object-ui/types/zod': path.resolve(__dirname, '../../packages/types/src/zod/index.zod.ts'), - '@object-ui/types': path.resolve(__dirname, '../../packages/types/src'), -}; - -export default defineConfig({ - plugins: [react()], - resolve: { alias: workspaceAliases }, - // ONE ORIGIN in dev, proxy for the split host. `.env.development` leaves - // VITE_SERVER_URL empty, so every client here builds a RELATIVE `/api/...` - // URL and this stanza forwards it to the backend. Without it an empty - // VITE_SERVER_URL would make those fetches hit the Vite dev server, which - // has no `/api` route and answers with the SPA's index.html fallback. - // - // Why not just point VITE_SERVER_URL at the backend: a cross-origin value - // makes every relative-target `type: 'api'` action resolve off-origin, and - // the action runtime does not attach Authorization / X-Tenant-ID / - // Accept-Language across origins — a 401 on the standard `pnpm dev` stack. - // - // `DEV_PROXY_TARGET` and the `/api` prefix mirror `apps/console/vite.config.ts` - // deliberately: the two dev stacks are meant to be configured the same way. - // No `port` is set — the README documents this example on Vite's default - // 5173, and pinning one would only collide with a real fork's choice. - server: { - proxy: { - '/api': { target: process.env.DEV_PROXY_TARGET || 'http://localhost:3000', changeOrigin: true }, - }, - }, -}); diff --git a/ui/examples/hello-world/README.md b/ui/examples/hello-world/README.md index 1a9cc7284e6..d240579a0dc 100644 --- a/ui/examples/hello-world/README.md +++ b/ui/examples/hello-world/README.md @@ -28,6 +28,4 @@ pnpm install ## Learn more -- [BYO Backend Console](../byo-backend-console/) — add ObjectUI to an existing product with your own REST/GraphQL backend (~100 lines: custom `DataSource` + router) -- [Console Starter](../console-starter/) — fork it to stand up a brand-new ObjectStack console, with the full plugin set already wired - [Schema Catalog](../schema-catalog/) — canonical JSON schemas used across docs & tests diff --git a/ui/examples/schema-catalog/README.md b/ui/examples/schema-catalog/README.md index dba603d29b5..6bdf148a57a 100644 --- a/ui/examples/schema-catalog/README.md +++ b/ui/examples/schema-catalog/README.md @@ -4,8 +4,8 @@ Canonical JSON schema catalog for ObjectUI. **Private** (not published to npm). > Naming note: this package lives under `examples/` (alongside the runnable > example apps), but it is **not** itself a runnable app — it is a data -> package + smoke tests. The runnable apps are `examples/hello-world`, -> `examples/console-starter`, etc. +> package + smoke tests. Its sibling `examples/hello-world` is a snippet to +> paste into your own app. This package is the **single source of truth** for every schema example we ship. It is consumed by: diff --git a/ui/packages/app-shell/README.md b/ui/packages/app-shell/README.md index 166d140aba3..2a8d11e7bbf 100644 --- a/ui/packages/app-shell/README.md +++ b/ui/packages/app-shell/README.md @@ -1,8 +1,12 @@ # @object-ui/app-shell -**Minimal Application Shell for ObjectUI** +**The ObjectOS Console's shell, views and Studio — a private workspace package** -A lightweight, framework-agnostic rendering engine that enables third-party systems to integrate ObjectUI components without inheriting the full console infrastructure. +> **Not published after 18.2.0.** This package exists to serve the ObjectOS Console +> (`apps/console`), and a customisable console for a third-party host has no business +> demand today, so it is `private: true` and no longer released to npm. The last published +> version is 18.2.0, which the maintainer deprecates on npm after the 18.2 publish. Inside this +> workspace `apps/console` and the `@object-ui/*` packages keep importing it as before. ## Purpose @@ -15,9 +19,8 @@ This package provides the essential building blocks for rendering ObjectUI schem ## Installation -```bash -pnpm add @object-ui/app-shell -``` +There is nothing to install from npm: the package is private (see above). Workspace +consumers declare it as `"@object-ui/app-shell": "workspace:*"`. ## Requires a bundler — plain Node cannot import this package @@ -196,7 +199,7 @@ reads as off, so a self-hosted admin's page load issues no request and logs no ## Default auth pages and the sign-up offer `DefaultLoginPage` and `DefaultRegisterPage` are the sign-in and sign-up pages -a host mounts at `/login` and `/register` (`examples/console-starter` does). +a host mounts at `/login` and `/register` (`apps/console` mounts its own pages there). They offer a generic sign-up only where the server would accept one. The server states that rule in two keys of `GET /api/v1/auth/config`: `emailPassword.disableSignUp` (the hard off switch) and @@ -1096,15 +1099,6 @@ Third-Party App | Data Source | Any | ObjectStack | | Customization | Full control | Limited | -## Examples - -See `examples/byo-backend-console` for a complete working example that demonstrates: -- Custom routing with React Router -- Custom data adapter (not ObjectStack) -- Custom authentication -- Cherry-picking only needed components -- Building a console in ~100 lines of code - ## Record create/edit modes The default `` shell mounts a global `` for diff --git a/ui/packages/app-shell/package.json b/ui/packages/app-shell/package.json index cf935e165a1..da685140f55 100644 --- a/ui/packages/app-shell/package.json +++ b/ui/packages/app-shell/package.json @@ -1,6 +1,7 @@ { "name": "@object-ui/app-shell", "version": "18.2.0", + "private": true, "type": "module", "sideEffects": [ "./dist/index.js", diff --git a/ui/packages/app-shell/src/console/ConsoleShell.tsx b/ui/packages/app-shell/src/console/ConsoleShell.tsx index 99e3961489c..04e0e9fc224 100644 --- a/ui/packages/app-shell/src/console/ConsoleShell.tsx +++ b/ui/packages/app-shell/src/console/ConsoleShell.tsx @@ -6,8 +6,8 @@ * routing tree behind a config object. In practice every real project wants to * edit the routes directly (add /billing, tweak AuthGuard behaviour, reorder * providers), so we now export the pieces and let consumers write ~40 lines of - * JSX in App.tsx. See examples/console-starter/src/App.tsx for a minimal example, - * apps/console/src/App.tsx for one with custom system routes + CreateApp. + * JSX in App.tsx. See apps/console/src/App.tsx, with custom system routes + + * CreateApp. */ import { Suspense, useEffect, useRef, useState, type ReactNode } from 'react'; diff --git a/ui/packages/app-shell/src/console/auth/signUpOffer.ts b/ui/packages/app-shell/src/console/auth/signUpOffer.ts index 0d6ffcbb775..4f1f72fd25c 100644 --- a/ui/packages/app-shell/src/console/auth/signUpOffer.ts +++ b/ui/packages/app-shell/src/console/auth/signUpOffer.ts @@ -13,7 +13,7 @@ * ONE decision for every such page (objectui#11705): the console's own * `/login` and `/register` (`apps/console/src/pages/auth/`) and this package's * exported `DefaultLoginPage` / `DefaultRegisterPage` (`./LoginPage`, - * `./RegisterPage`, which `examples/console-starter` mounts) all call + * `./RegisterPage`) all call * {@link decideSignUpOffer}. It was written for the console's pages and moved * here unchanged, so that a package page can call it too; there is no second * copy of the rule. diff --git a/ui/packages/app-shell/src/index.ts b/ui/packages/app-shell/src/index.ts index 20adb37f977..633056f19d9 100644 --- a/ui/packages/app-shell/src/index.ts +++ b/ui/packages/app-shell/src/index.ts @@ -68,7 +68,7 @@ export type { } from './hooks/useRecentItems.js'; // Console building blocks — compose these in your App.tsx to build the console -// routing tree. See examples/console-starter/src/App.tsx for a minimal example. +// routing tree. See apps/console/src/App.tsx. export { ConsoleShell, ConnectedShell, diff --git a/ui/scripts/__tests__/check-changeset-fixed.test.ts b/ui/scripts/__tests__/check-changeset-fixed.test.ts index c41e20d3616..f04bdcb461e 100644 --- a/ui/scripts/__tests__/check-changeset-fixed.test.ts +++ b/ui/scripts/__tests__/check-changeset-fixed.test.ts @@ -118,6 +118,17 @@ describe('analyze', () => { expect(result.privateInFixed).toEqual([]); }); + it('classifies a private package outside both lists only while private packages are versioned', () => { + // `@object-ui/app-shell` since it left npm: versioned on its own, published + // nowhere. Under `version: false` the same package would be treated as + // ignored, so it stays unclassified there. + const outside = [...manifests, pkg('@object-ui/private-shell', true)]; + expect(analyze({ ...config, privatePackages: { version: true, tag: false } }, outside).unclassified).toEqual([]); + expect( + analyze({ ...config, privatePackages: { version: false, tag: false } }, outside).unclassified.map((p) => p.name) + ).toEqual(['@object-ui/private-shell']); + }); + it('reports a package that is in neither `fixed` nor `ignore`', () => { const result = analyze({ ...config, privatePackages: { version: true, tag: false } }, [ ...manifests, diff --git a/ui/scripts/__tests__/check-doc-fence-languages.test.ts b/ui/scripts/__tests__/check-doc-fence-languages.test.ts index b4a8e046f64..ad57639d009 100644 --- a/ui/scripts/__tests__/check-doc-fence-languages.test.ts +++ b/ui/scripts/__tests__/check-doc-fence-languages.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parse as parseYaml } from 'yaml'; @@ -215,9 +216,18 @@ describe('check-doc-fence-languages: the scan surface is check-doc-snippet-types it('…and that subtraction is non-empty, so it is not silently subtracting nothing', () => { // Each leg separately: a union that is non-empty overall would stay green - // with one of its four members returning nothing at all. + // with one of its four members returning nothing at all. The root `docs/*.md` + // leg is read on a fixture page: the tree's one root page, the app-shell + // extraction guide, was retired with the package's publishing (#25922). + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'check-doc-fence-languages-rootdocs-')); + try { + fs.mkdirSync(path.join(fixture, 'docs')); + fs.writeFileSync(path.join(fixture, 'docs', 'PAGE.md'), '# top level\n'); + expect(snippetRootDocsPages(fixture)).toEqual(['docs/PAGE.md']); + } finally { + fs.rmSync(fixture, { recursive: true, force: true }); + } for (const leg of [ - snippetRootDocsPages(ROOT), snippetAdrDocsPages(ROOT), snippetAuditDocsPages(ROOT), snippetNestedPackageReadmes(ROOT), diff --git a/ui/scripts/__tests__/check-doc-links.test.ts b/ui/scripts/__tests__/check-doc-links.test.ts index 1e7776d21dd..98061a4e1cf 100644 --- a/ui/scripts/__tests__/check-doc-links.test.ts +++ b/ui/scripts/__tests__/check-doc-links.test.ts @@ -514,7 +514,9 @@ describe('the repo it guards', () => { expect(scanned['README.md']).toBe(1); expect(scanned['CONTRIBUTING.md']).toBe(1); expect(scanned['ROADMAP.md']).toBe(1); - expect(scanned['examples']).toBeGreaterThanOrEqual(4); + // Three READMEs since the two host-console examples were deleted (#25922): + // the index, hello-world and schema-catalog. + expect(scanned['examples']).toBeGreaterThanOrEqual(3); expect(scanned['docs']).toBeGreaterThanOrEqual(15); expect(scanned['content/docs']).toBeGreaterThanOrEqual(100); // objectui#3622. 38 of the 39 package directories carry a README today diff --git a/ui/scripts/__tests__/check-doc-snippet-types.test.ts b/ui/scripts/__tests__/check-doc-snippet-types.test.ts index 9c2e0d15535..7ed58a2de8b 100644 --- a/ui/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/ui/scripts/__tests__/check-doc-snippet-types.test.ts @@ -670,20 +670,37 @@ describe('objectui#7856 — the root docs/*.md pages are in the scan set, and on it('listDocuments reaches them', () => { const documents = listDocuments(repoRoot); for (const doc of rootDocsPages(repoRoot)) expect(documents).toContain(doc); - // Non-vacuous: the leg reaches a real page, not an empty directory. - expect(rootDocsPages(repoRoot)).toContain('docs/app-shell-extraction-guide.md'); + // Non-vacuous on a fixture: the tree's one root page, the app-shell + // extraction guide, was retired with the package's publishing (#25922), + // so the real `docs/` holds no top-level page today and the loop above + // has nothing to iterate. The leg still has to reach one when it exists. + const root = tempTree({ 'docs/PAGE.md': '# top level\n' }); + try { + expect(rootDocsPages(root)).toEqual(['docs/PAGE.md']); + expect(listDocuments(root)).toContain('docs/PAGE.md'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } }); it('the widening judges something — the leg contributes blocks to the compiled tier', () => { - // Being IN the walk is one fact; being compiled is the other, and this card - // delivered both (no `UNGATED_DOCS` entry was needed — the blocks were - // repaired). A leg whose pages all sat on the ledger would be visible to the - // accounting and judged by nothing, which is a weaker claim than this test - // makes. - const state = analyze({}); - const leg = new Set(rootDocsPages(repoRoot)); - expect((state.covered as string[]).filter((d) => leg.has(d)).sort()).toEqual([...leg].sort()); - expect((state.compiled as { doc: string }[]).some((b) => leg.has(b.doc))).toBe(true); + // Being IN the walk is one fact; being compiled is the other. A leg whose + // pages all sat on the ledger would be visible to the accounting and judged + // by nothing, which is a weaker claim than this test makes. Measured on a + // fixture page because the real `docs/` has no top-level page since #25922; + // every real page the leg reaches must still be covered. + const real = new Set(rootDocsPages(repoRoot)); + expect((analyze({}).covered as string[]).filter((d) => real.has(d)).sort()).toEqual([...real].sort()); + const root = tempTree({ + 'docs/PAGE.md': ['# T', '', `${FENCE}ts`, 'export const a = 1;', FENCE].join('\n'), + }); + try { + const state = analyze({ root, ungated: {} }); + expect(state.covered as string[]).toContain('docs/PAGE.md'); + expect((state.compiled as { doc: string }[]).some((b) => b.doc === 'docs/PAGE.md')).toBe(true); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } }); it('stops at the top level: a page in a subdirectory is NOT collected BY THIS LEG', () => { diff --git a/ui/scripts/__tests__/check-type-check-coverage.test.ts b/ui/scripts/__tests__/check-type-check-coverage.test.ts index f7f7483814b..ae9239f2125 100644 --- a/ui/scripts/__tests__/check-type-check-coverage.test.ts +++ b/ui/scripts/__tests__/check-type-check-coverage.test.ts @@ -769,20 +769,13 @@ describe('examples/schema-catalog is wired up, in this repository', () => { // layout is fine — but it must arrive wired up, and this list is where the // next reader finds out. // - // `@object-ui/example-console-starter` is that second package - // (objectui#3528). Its vite-alias closure test lives in `test/` for the same - // structural reason schema-catalog's do: that example's `tsconfig.json` IS - // the app build (`tsc && vite build`, `include: ["src"]`), and a fork-ready - // template should not hand the customer a test file inside the `src/` they - // are meant to edit. + // `@object-ui/example-console-starter` was the second (objectui#3528), until + // that example was deleted with the host-console posture (#25922). const outsideSrc = packages .filter((p) => p.testFilePaths.some((rel: string) => !rel.startsWith('src/'))) .map((p) => p.name) .sort(); - expect(outsideSrc).toEqual([ - '@object-ui/example-console-starter', - '@object-ui/example-schema-catalog', - ]); + expect(outsideSrc).toEqual(['@object-ui/example-schema-catalog']); // "It must arrive wired up" is now enforced here rather than left to the // reader: appending a name above is not enough on its own, because a package diff --git a/ui/scripts/__tests__/package-files-exist.test.ts b/ui/scripts/__tests__/package-files-exist.test.ts index dd382071c31..76ca33bae89 100644 --- a/ui/scripts/__tests__/package-files-exist.test.ts +++ b/ui/scripts/__tests__/package-files-exist.test.ts @@ -445,7 +445,9 @@ describe('published packages that declare a license ship its text (objectui#3696 // flipped — would otherwise report success over an empty set. Lowered by // one when `@object-ui/sdui-parser` was retired for the root // `@objectstack/sdui-parser` (objectstack#25712 item 3): 37 measured then. - expect(owingPackages.length).toBeGreaterThanOrEqual(37); + // Lowered by one again when `@object-ui/app-shell` went private + // (objectstack#25922): 36 measured then. + expect(owingPackages.length).toBeGreaterThanOrEqual(36); // The package objectui#3696 fixed must be in scope BY NAME. If it drops // out of the population, this guard has stopped watching the exact spot diff --git a/ui/scripts/__tests__/quick-reference-commands-4149.test.ts b/ui/scripts/__tests__/quick-reference-commands-4149.test.ts index b10d551252b..33dedfb7b85 100644 --- a/ui/scripts/__tests__/quick-reference-commands-4149.test.ts +++ b/ui/scripts/__tests__/quick-reference-commands-4149.test.ts @@ -234,7 +234,7 @@ describe('QUICK_REFERENCE.md — every documented `pnpm` command exists', () => }); }); -describe('QUICK_REFERENCE.md — the "Run Examples" section covers the examples that run', () => { +describe('QUICK_REFERENCE.md — the "Examples" section covers the examples that run', () => { /** * The reverse direction, and the one that would have caught this card before it was * filed. The forward test above only says the documented examples exist; it stays @@ -245,16 +245,25 @@ describe('QUICK_REFERENCE.md — the "Run Examples" section covers the examples * when its manifest declares a `dev` script. `hello-world` (a snippet) and * `schema-catalog` (a data package) declare none, so they are correctly absent from * the command block — and the day either grows one, this goes red. + * + * Since the two host-console examples were deleted (#25922) no `examples/*` package + * is a dev server, and the section says so in words instead of listing commands. That + * sentence is pinned for as long as it is true, so neither half can go stale alone: + * an example that grows a `dev` script reds the sentence, and the command check + * below takes over. */ it('documents a dev command for every `examples/*` package that declares one', () => { const runnable = workspacePackages.filter((p) => p.dir.startsWith('examples/') && p.scripts.has('dev')); - expect( - runnable.length, - 'no `examples/*` package declares a `dev` script any more — the "Run Examples" section ' + - 'is now describing something that does not exist and needs rewriting, not re-pinning', - ).toBeGreaterThan(0); - - const block = section('### Run Examples'); + const block = section('### Examples'); + const noneRuns = 'No directory under `examples/` is a dev server'; + if (runnable.length === 0) { + expect(block, `${docRel}'s "Examples" section must say that no example is a dev server`).toContain(noneRuns); + expect(block, `${docRel}'s "Examples" section documents a dev command for an example that has none`).not.toMatch( + /--filter @object-ui\/example-[\w-]+ dev/, + ); + return; + } + expect(block, `${docRel} says no example is a dev server, but ${JSON.stringify(runnable.map((p) => p.dir))} declare one`).not.toContain(noneRuns); const missing = runnable.filter((p) => !block.includes(`--filter ${p.name} dev`)); expect( diff --git a/ui/scripts/__tests__/side-effects-declaration-consistency.test.ts b/ui/scripts/__tests__/side-effects-declaration-consistency.test.ts index 4977a39b071..02ca9845998 100644 --- a/ui/scripts/__tests__/side-effects-declaration-consistency.test.ts +++ b/ui/scripts/__tests__/side-effects-declaration-consistency.test.ts @@ -1172,10 +1172,7 @@ describe('`sideEffects` declarations match load-time behaviour (objectui#3943)', // the entry form that made the console's bundle 0 bytes while every // published path was declared. const layoutAliases = aliasEntries.filter((a) => a.specifier === '@object-ui/layout'); - expect(layoutAliases.map((a) => a.config).sort()).toEqual([ - 'apps/console/vite.config.ts', - 'examples/console-starter/vite.config.ts', - ]); + expect(layoutAliases.map((a) => a.config).sort()).toEqual(['apps/console/vite.config.ts']); for (const alias of layoutAliases) { expect(alias.target).toBe(path.join(repoRoot, 'packages/layout/src')); } diff --git a/ui/scripts/__tests__/zero-test-member-exclusion-9106.test.ts b/ui/scripts/__tests__/zero-test-member-exclusion-9106.test.ts index 2a107a1a273..862c39ba63d 100644 --- a/ui/scripts/__tests__/zero-test-member-exclusion-9106.test.ts +++ b/ui/scripts/__tests__/zero-test-member-exclusion-9106.test.ts @@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url'; * other. It then landed NOTHING that records the derivation — measured on * `1e0e46af9`, `git grep -ln 8991 -- scripts packages apps examples` returns two * incidental files, a vector fixture and a screenshot. So the next person to - * derive this population re-discovers these three and re-asks the same question, + * derive this population re-discovers these members and re-asks the same question, * which is exactly what produced objectui#9106. * * A prose exclusion is the thing this repo has already proved does not hold, in @@ -44,7 +44,7 @@ import { fileURLToPath } from 'node:url'; * `passWithNoTests` to a constant, it sets it to the negation of whether the CLI * named test filters, so a positional filter collecting nothing is precisely the * case it rejects loudly. That premise is pinned below, because the whole reason - * these three get no `test` script depends on it. + * these members get no `test` script depends on it. * * ⇒ giving them a `test` script would need `--passWithNoTests`, which would make * the package entry exit 0 while the root-config comparison exits 1 — @@ -92,11 +92,6 @@ const ZERO_TEST_MEMBERS = [ // construction and could never notice a test file appearing. why: 'the docs site; no test files, and no vitest project covers apps/site', }, - { - dir: 'examples/byo-backend-console', - pkgName: '@object-ui/example-byo-backend-console', - why: 'a runnable example app; no test files', - }, { dir: 'examples/hello-world', pkgName: '@object-ui/example-hello-world', @@ -298,7 +293,7 @@ describe('objectui#9106 — the two buckets either side of the exclusion', () => describe('objectui#9106 — the premises the exclusion rests on', () => { it('the root config still derives passWithNoTests from the CLI, so a named path that collects nothing is loud', () => { // If this ever becomes a constant `true`, the measured exit 1 above stops - // being true and the reason these three have no `test` script needs + // being true and the reason these members have no `test` script needs // re-reading rather than re-spelling. expect(rootConfigText).toMatch(/passWithNoTests:\s*!cliHasTestFilters\(process\.argv\)/); }); diff --git a/ui/scripts/check-changeset-fixed.mjs b/ui/scripts/check-changeset-fixed.mjs index 94cf5365463..7a603c62160 100644 --- a/ui/scripts/check-changeset-fixed.mjs +++ b/ui/scripts/check-changeset-fixed.mjs @@ -4,7 +4,9 @@ * release. Three rules, all decided from the config file plus the manifests: * * 1. every workspace package under `packages/` and `apps/` is either in the - * `fixed` group or matched by `ignore`; + * `fixed` group or matched by `ignore` -- or is PRIVATE while + * `privatePackages.version` is true, which versions it on its own and + * publishes it nowhere (`@object-ui/app-shell` since it left npm, #25922); * 2. `privatePackages` is declared explicitly; * 3. no PRIVATE package sits in the `fixed` group while * `privatePackages.version` is false. @@ -125,7 +127,16 @@ export function analyze(config, manifests) { const isIgnored = globMatcher(config.ignore ?? []); const privatePackages = normalizePrivatePackages(config.privatePackages); - const unclassified = manifests.filter((pkg) => !isFixed(pkg.name) && !isIgnored(pkg.name)); + // A private package outside both lists is classified only while private + // packages are versioned: under `version: false` changesets treats it as + // ignored, and a changeset naming it beside a published package is then the + // mixed-changeset error rule 3 describes. + const unclassified = manifests.filter( + (pkg) => + !isFixed(pkg.name) && + !isIgnored(pkg.name) && + !(pkg.private && privatePackages?.version === true) + ); const privateInFixed = privatePackages?.version === true ? [] @@ -183,7 +194,9 @@ function main() { console.error(` • ${name} (${path})`); } console.error( - "\nAdd them to the `fixed` array in .changeset/config.json to keep versions in sync." + "\nAdd them to the `fixed` array in .changeset/config.json to keep versions in sync, or\n" + + "mark a package that publishes nowhere `private: true` (private packages version on\n" + + "their own while `privatePackages.version` is true)." ); } @@ -223,7 +236,7 @@ function main() { if (failed) return 1; - console.log("✅ All workspace packages are in the changeset fixed group."); + console.log("✅ Every workspace package is classified: in the fixed group, ignored, or private."); console.log( `✅ privatePackages declared: version=${privatePackages.version}, tag=${privatePackages.tag}.` ); diff --git a/ui/scripts/markdown-test-inputs.mjs b/ui/scripts/markdown-test-inputs.mjs index adce3f02eac..ae5eb466ad9 100644 --- a/ui/scripts/markdown-test-inputs.mjs +++ b/ui/scripts/markdown-test-inputs.mjs @@ -827,19 +827,19 @@ export const ADJUDICATED = new Map([ [ 'scripts/__tests__/check-doc-links.test.ts', { - reads: ['./*', 'apps/**', 'content/docs/**', 'docs/app-shell-extraction-guide.md', 'docs/adr/**', 'docs/audits/**', 'examples/**', 'packages/**'], + reads: ['./*', 'apps/**', 'content/docs/**', 'docs/adr/**', 'docs/audits/**', 'examples/**', 'packages/**'], }, ], [ 'scripts/__tests__/check-doc-snippet-emitted-census.test.ts', { - reads: ['README.md', 'apps/console/docs/UI_IMPROVEMENT_PROPOSAL.md', 'apps/console/docs/deployment.md', 'apps/console/docs/error-tracking.md', 'content/docs/**', 'docs/app-shell-extraction-guide.md', 'docs/adr/**', 'docs/audits/**', 'packages/**'], + reads: ['README.md', 'apps/console/docs/UI_IMPROVEMENT_PROPOSAL.md', 'apps/console/docs/deployment.md', 'apps/console/docs/error-tracking.md', 'content/docs/**', 'docs/adr/**', 'docs/audits/**', 'packages/**'], }, ], [ 'scripts/__tests__/check-doc-snippet-types.test.ts', { - reads: ['README.md', 'apps/console/docs/UI_IMPROVEMENT_PROPOSAL.md', 'apps/console/docs/deployment.md', 'apps/console/docs/error-tracking.md', 'content/docs/**', 'docs/app-shell-extraction-guide.md', 'docs/adr/**', 'docs/audits/**', 'packages/**'], + reads: ['README.md', 'apps/console/docs/UI_IMPROVEMENT_PROPOSAL.md', 'apps/console/docs/deployment.md', 'apps/console/docs/error-tracking.md', 'content/docs/**', 'docs/adr/**', 'docs/audits/**', 'packages/**'], walker: 'markdown-tree', }, ], @@ -973,7 +973,7 @@ export const ADJUDICATED = new Map([ [ 'scripts/__tests__/dollar-dialect-alias-census.test.ts', { - reads: ['./*', '.changeset/**', '.claude/skills/**', 'apps/**', 'content/docs/**', 'docs/app-shell-extraction-guide.md', 'docs/adr/**', 'docs/audits/**', 'examples/**', 'packages/**', 'patches/README.md', 'skills/objectui/**'], + reads: ['./*', '.changeset/**', '.claude/skills/**', 'apps/**', 'content/docs/**', 'docs/adr/**', 'docs/audits/**', 'examples/**', 'packages/**', 'patches/README.md', 'skills/objectui/**'], }, ], [ diff --git a/ui/tsconfig.json b/ui/tsconfig.json index f7c3839497c..f7274094be6 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -22,8 +22,8 @@ // `packages/types/tsconfig.examples.json` // Nothing under `apps/` or `examples/` extends it. // -// `tsconfig.tier-base.json` is extended by 3: `tsconfig.node.json`, -// `tsconfig.react.json` and `examples/byo-backend-console/tsconfig.json`. +// `tsconfig.tier-base.json` is extended by 2: `tsconfig.node.json` and +// `tsconfig.react.json`. // Its full header carries the rest, including why `tsconfig.scripts.json` is // NOT among them although objectui#9330's ruling names it. // diff --git a/ui/tsconfig.tier-base.json b/ui/tsconfig.tier-base.json index e60bfb20231..7f920e70d32 100644 --- a/ui/tsconfig.tier-base.json +++ b/ui/tsconfig.tier-base.json @@ -20,14 +20,16 @@ // cb725e78f. Re-derive it rather than trusting this block if it matters — the // numbers below are a reading with a date on it, not a contract. // -// THIS FILE is extended by 3 files and by nothing else: +// THIS FILE is extended by 2 files and by nothing else: // tsconfig.node.json editor / reference only; NO script // runs it (see its own header) // tsconfig.react.json no in-repo extender at all; named as // a template for plugin authors in // skills/objectui/guides/plugin-development.md -// examples/byo-backend-console/tsconfig.json the one real compile that reaches -// here (`tsc --noEmit`, `tsc && vite build`) +// +// No package's build or type-check program reaches this file since +// `examples/byo-backend-console`, the one real compile that did, was deleted +// (#25922); turbo's task inputs no longer name it for that reason. // // The root `tsconfig.json` is extended by 75 of the 94, every one of them // under `packages/`: 38 of the 40 `packages/*/tsconfig.json`, 36 of the 37 diff --git a/ui/turbo.objectui.json b/ui/turbo.objectui.json index 92073c0762c..589f5fce5ac 100644 --- a/ui/turbo.objectui.json +++ b/ui/turbo.objectui.json @@ -15,7 +15,6 @@ "inputs": [ "$TURBO_DEFAULT$", "$TURBO_ROOT$/tsconfig.json", - "$TURBO_ROOT$/tsconfig.tier-base.json", "$TURBO_ROOT$/scripts/vite-*.ts", "$TURBO_ROOT$/scripts/build-plugin-stylesheet.mjs", "$TURBO_ROOT$/scripts/check-dist-completeness.mjs", @@ -90,7 +89,6 @@ "$TURBO_ROOT$/scripts/vite-*.ts", "$TURBO_ROOT$/vitest.config.mts", "$TURBO_ROOT$/tsconfig.json", - "$TURBO_ROOT$/tsconfig.tier-base.json", "$TURBO_ROOT$/apps/console/src/lib/auth-preflight.ts", "$TURBO_ROOT$/eslint-rules/no-unprefixed-query-params.js", "$TURBO_ROOT$/packages/cli/src/commands/generate.ts",