Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/components-widgets-subpath.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@json-edit-react/components': minor
---

Move the editor-slot widgets (`ReactSelect`, `CodeEditor`) to their own subpath, `@json-edit-react/components/widgets`. They're a different kind of thing from the rest of the package — they satisfy JsonEditor's `Select` / `TextEditor` prop contracts to replace a built-in UI control, rather than ship a `CustomNodeDefinition` for `customNodeDefinitions` — so they're kept off the package root, leaving it uniformly node-definition components.

Import them from the subpath: `import { ReactSelect, CodeEditor } from '@json-edit-react/components/widgets'`. Everything else continues to import from the package root.
2 changes: 1 addition & 1 deletion .changeset/utils-filter-toolkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
'@json-edit-react/utils': minor
---

Add a filter-function toolkit — composable predicate builders for the `allow*` props (`allowEdit`, `allowDelete`, `allowAdd`, `allowTypeSelection`, …) and `searchFilter`.
Add a filter-function toolkit — composable predicate builders for the `allow*` props (`allowEdit`, `allowDelete`, `allowAdd`, `allowTypeSelection`, …) and `searchFilter`. It ships under its own subpath, `@json-edit-react/utils/filters`, so its generic builder names (`and`, `or`, `not`, `root`, `collections`, …) stay off the package root.

Every builder returns the same `FilterPredicate` shape — `(node, searchText?) => boolean` — whose optional second argument makes it assignable to both `FilterFunction` (the `allow*` props) and `SearchFilterFunction` (`searchFilter`), so one set of builders serves every filter prop. Property builders: `byKey`, `byPath` (glob / RegExp / segment-array paths), `byLevel`, `bySize`, `byType`, `byValue`. Position constants: `root`, `collections`, `primitives`, `inArray`, `inObject`. Combinators: `and` / `or` / `not` (they thread `searchText`, so search bridges compose with structural builders). Search bridges: `matchesSearch(mode?)` (wraps core's own matchers) and `matchRecord({ fields, path? })` (reveals a whole record when one of its fields matches, instead of collapsing it to the single matching field).

Expand Down
2 changes: 1 addition & 1 deletion demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import './style.css'
import { getLineHeight, truncate } from './helpers'
import { RenderProfiler } from './RenderProfiler'
import { Loading } from '../../packages/components/src/_common/Loading'
import { CodeEditor } from '@json-edit-react/components'
import { CodeEditor } from '@json-edit-react/components/widgets'
const SourceIndicator = lazy(() => import('./SourceIndicator'))
const JsonEditor = lazy(() =>
import('@json-edit-react').then((m) => ({ default: m.JsonEditor }))
Expand Down
20 changes: 11 additions & 9 deletions demo/src/demoData/dataDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
not,
primitives,
root,
} from '@json-edit-react/utils'
} from '@json-edit-react/utils/filters'
import jsonSchema from './jsonSchema.json'
import customNodesSchema from './customNodesSchema.json'
import Ajv from 'ajv'
Expand Down Expand Up @@ -580,9 +580,11 @@ export const demoDataDefinitions: Record<string, DemoData> = {
// Borrow the pre-built definition's ISO-date condition; the component
// here is a custom read-only display, not the date picker
condition: datePickerDefinition().condition,
component: ({ data, getStyles, nodeData }) => {
component: ({ value, getStyles, nodeData }) => {
return (
<p style={getStyles('string', nodeData)}>{new Date(data as string).toLocaleString()}</p>
<p style={getStyles('string', nodeData)}>
{new Date(value as string).toLocaleString()}
</p>
)
},
},
Expand Down Expand Up @@ -717,22 +719,22 @@ export const demoDataDefinitions: Record<string, DemoData> = {
typeof value === 'string' &&
value.startsWith('http') &&
value.endsWith('.png'),
component: ({ data }) => {
component: ({ value }) => {
const truncate = (string: string, length = 50) =>
string.length < length ? string : `${string.slice(0, length - 2).trim()}...`
return (
<div style={{ maxWidth: 250 }}>
<a href={data as string} target="_blank" rel="noreferrer">
<img src={data as string} style={{ maxHeight: 75 }} alt="logo" />
<p style={{ fontSize: '0.75em' }}>{truncate(data as string)}</p>{' '}
<a href={value as string} target="_blank" rel="noreferrer">
<img src={value as string} style={{ maxHeight: 75 }} alt="logo" />
<p style={{ fontSize: '0.75em' }}>{truncate(value as string)}</p>{' '}
</a>
</div>
)
},
},
{
condition: ({ key }) => key === 'publisher',
component: ({ data }) => {
component: ({ value }) => {
return (
<p
style={{
Expand All @@ -746,7 +748,7 @@ export const demoDataDefinitions: Record<string, DemoData> = {
color: 'black',
}}
>
Presented by: <strong>{String(data)}</strong>
Presented by: <strong>{String(value)}</strong>
</p>
)
},
Expand Down
2 changes: 1 addition & 1 deletion demo/src/examples/filter-toolkit/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from '@chakra-ui/react'
import { InfoIcon, SmallCloseIcon } from '@chakra-ui/icons'
import { JsonEditor, type JsonData, type NodeData, type ThemeStyles } from '@json-edit-react'
import { type FilterPredicate } from '@json-edit-react/utils'
import { type FilterPredicate } from '@json-edit-react/utils/filters'
import { useExampleProps, useExampleTheme } from '../kit/exampleProps'
import { useExamplePalette } from '../kit/useThemePalette'
import { SplitPane } from '../kit/SplitPane'
Expand Down
2 changes: 1 addition & 1 deletion demo/src/examples/filter-toolkit/recipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
primitives,
root,
type FilterPredicate,
} from '@json-edit-react/utils'
} from '@json-edit-react/utils/filters'

export interface Recipe {
id: string
Expand Down
2 changes: 1 addition & 1 deletion demo/src/examples/static/swap-the-built-ins/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type SelectProps,
type TypeOptions,
} from '@json-edit-react'
import { CodeEditor, ReactSelect } from '@json-edit-react/components'
import { CodeEditor, ReactSelect } from '@json-edit-react/components/widgets'
import { buildSelectStyles } from './utils'
import { useExampleTheme } from '../../kit/exampleProps'
import { useExampleProps } from '../../kit/exampleProps' // ---cut---
Expand Down
1 change: 0 additions & 1 deletion demo/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@json-edit-react/themes": ["../packages/themes/src"],
"@json-edit-react/themes/*": ["../packages/themes/src/*"],
Expand Down
49 changes: 41 additions & 8 deletions demo/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@ const coreSrcMap: Record<PackageOption, { pkgJson: string; src: string }> = {
pkgJson: path.join('..', 'package.json'),
src: path.resolve(__dirname, '../src'),
},
// `build` mode reads core's raw rollup output (`build/`) directly, so a plain
// `pnpm build` is enough — no need to re-stage `build_package/`, which exists
// only to assemble the npm-publish tree (short README + trimmed package.json).
// `pack` mode below is the true publish dress-rehearsal. Point at the ESM file,
// not the dir: `build/` has no package.json / index.js for Vite to resolve.
// Version comes from the root package.json (build/ carries no version).
build: {
pkgJson: path.join('..', 'build_package', 'package.json'),
src: path.resolve(__dirname, '../build_package'),
pkgJson: path.join('..', 'package.json'),
src: path.resolve(__dirname, '../build/index.esm.js'),
},
pack: {
pkgJson: path.resolve(__dirname, '../pack-output/json-edit-react/package/package.json'),
Expand All @@ -63,13 +69,34 @@ const componentsSrcMap: Record<PackageOption, string> = {
pack: path.resolve(__dirname, '../pack-output/components/package'),
}

// The editor-slot widgets (`ReactSelect`, `CodeEditor`) ship under their own
// subpath (`@json-edit-react/components/widgets`), so they need their own
// resolution target — the bare `components` alias is anchored and won't match
// the subpath.
const componentsWidgetsSrcMap: Record<PackageOption, string> = {
npm: '@json-edit-react/components/widgets',
local: path.resolve(__dirname, '../packages/components/src/widgets'),
build: path.resolve(__dirname, '../packages/components/build/widgets.esm.js'),
pack: path.resolve(__dirname, '../pack-output/components/package/build/widgets.esm.js'),
}

const utilsSrcMap: Record<PackageOption, string> = {
npm: '@json-edit-react/utils',
local: path.resolve(__dirname, '../packages/utils/src'),
build: path.resolve(__dirname, '../packages/utils/build/index.esm.js'),
pack: path.resolve(__dirname, '../pack-output/utils/package'),
}

// The filter toolkit ships under its own subpath (`@json-edit-react/utils/filters`),
// so it needs its own resolution target — the bare `utils` alias is anchored and
// won't match the subpath.
const utilsFiltersSrcMap: Record<PackageOption, string> = {
npm: '@json-edit-react/utils/filters',
local: path.resolve(__dirname, '../packages/utils/src/filters'),
build: path.resolve(__dirname, '../packages/utils/build/filters.esm.js'),
pack: path.resolve(__dirname, '../pack-output/utils/package/build/filters.esm.js'),
}

const packageFile = coreSrcMap[provider].pkgJson
const jsonEditReactPath = coreSrcMap[provider].src
const pkg = fs.readJsonSync(packageFile)
Expand Down Expand Up @@ -114,19 +141,25 @@ export default defineConfig({
// `../../../../../data/...` relative path.
{ find: /^@test-data\//, replacement: path.resolve(__dirname, '../data') + '/' },
{ find: /^@json-edit-react\/themes$/, replacement: themesSrcMap[provider] },
{ find: /^@json-edit-react\/components\/widgets$/, replacement: componentsWidgetsSrcMap[provider] },
{ find: /^@json-edit-react\/components$/, replacement: componentsSrcMap[provider] },
{ find: /^@json-edit-react\/utils\/filters$/, replacement: utilsFiltersSrcMap[provider] },
{ find: /^@json-edit-react\/utils$/, replacement: utilsSrcMap[provider] },
{ find: /^@json-edit-react$/, replacement: jsonEditReactPath },
{ find: /^json-edit-react$/, replacement: jsonEditReactPath },
],
// In `pack` and `build` modes the packed/built sub-packages live outside
// demo/node_modules. Without dedupe, vite's walk-up resolution from those
// files can pick up a second copy of React (from the workspace root's
// node_modules, or wherever else it finds one first) — different on-disk
// path = different React instance = hooks/context broken at runtime.
// Forcing react/react-dom to always resolve from the demo's own deps
// guarantees a single instance.
dedupe: ['react', 'react-dom'],
// files can pick up a second copy of a package (from the workspace root's
// pnpm store, or wherever else it finds one first) — a different on-disk
// path means a different module instance:
// - react / react-dom: a second React breaks hooks/context at runtime.
// - @codemirror/state, @codemirror/view: CodeMirror keys its extension
// system on `instanceof` against these singletons, so a second copy
// makes `@json-edit-react/components`' CodeEditor throw "Unrecognized
// extension value … multiple instances of @codemirror/state".
// Forcing each to resolve from the demo's own deps guarantees one instance.
dedupe: ['react', 'react-dom', '@codemirror/state', '@codemirror/view'],
},
server: {
port: 5175,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"demo:pack": "pnpm pack-all && cd demo && yarn && yarn start:pack",
"prebuild": "pnpm lint && node scripts/run-prebuild-tests.mjs",
"build": "rollup -c && rm -R build/dts",
"build-all": "pnpm -r build",
"build-package": "pnpm build && node scripts/stage-package.mjs",
"lint": "eslint",
"compile": "tsc --noEmit && ts-prune",
Expand Down
2 changes: 2 additions & 0 deletions packages/components/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ A published package of pre-built custom node components for [`json-edit-react`](

Re-exported from [src/index.ts](src/index.ts). Each component lives in its own folder under `src/` with the structure `{component.tsx, definition.ts, index.ts}` (plus `style.css` for some).

**Exception — the editor-slot widgets ship under their own subpath**, `@json-edit-react/components/widgets` (`src/widgets/`), and are deliberately NOT re-exported from the root. `ReactSelect` and `CodeEditor` are a different *kind* of thing from the node-definition components: they have no `definition.ts` — they satisfy a props contract (`SelectProps`, `TextEditorProps`) and get passed to JsonEditor's top-level `Select` / `TextEditor` props to replace a built-in UI control, rather than render a node type. Splitting them out keeps the root barrel uniformly node-definition components. This is purely a **conceptual** grouping — NOT a tree-shaking measure (the widgets' heavy libs are already `React.lazy`-loaded and the wrapper code is tiny); don't conflate it with the bundle-bloat escape hatch below. The wiring lives in the same three places as a future sub-path group: `package.json` `exports` (+ a `typesVersions` fallback for classic `moduleResolution: node`), a second `jsBundle`/`dtsBundle` pair in [rollup.config.mjs](rollup.config.mjs), and the demo's Vite alias (`componentsWidgetsSrcMap`).

## Conventions

### Third-party deps strategy: Option B+
Expand Down
13 changes: 10 additions & 3 deletions packages/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,22 @@ Each component ships a React component plus a definition factory that produces a
| `Undefined` | `undefined` value display |
| `ErrorIndicator` | Wraps a node with a glyph (default ⚠️) to flag the nodes you target via `condition` — e.g. validation errors |

### Editor slot components
### Editor slot widgets

Standalone components that plug into JsonEditor's `Select` and `TextEditor` props (not into `customNodeDefinitions`).
Standalone components that plug into JsonEditor's `Select` and `TextEditor` props (not into `customNodeDefinitions`) to replace a built-in UI control. They ship under their own subpath — **`@json-edit-react/components/widgets`** — kept off the package root because they're a different mechanism from the node-definition components above.

| Component | Use case |
| Widget | Use case |
|---|---|
| `ReactSelect` | Drop-in replacement for the built-in `<select>`, wrapping [`react-select`](https://react-select.com). Pass to `JsonEditor`'s `Select` prop. |
| `CodeEditor` | CodeMirror-based editor with JSON syntax highlighting. Pass to `JsonEditor`'s `TextEditor` prop to upgrade the raw-JSON text editor. Accepts an optional `theme` prop matching the built-in theme names. |

```tsx
import { JsonEditor } from 'json-edit-react'
import { ReactSelect, CodeEditor } from '@json-edit-react/components/widgets'

<JsonEditor data={data} setData={setData} Select={ReactSelect} TextEditor={CodeEditor} />
```

## Usage

```tsx
Expand Down
10 changes: 10 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
"types": "./build/index.d.ts",
"import": "./build/index.esm.js",
"require": "./build/index.cjs.js"
},
"./widgets": {
"types": "./build/widgets.d.ts",
"import": "./build/widgets.esm.js",
"require": "./build/widgets.cjs.js"
}
},
"typesVersions": {
"*": {
"widgets": ["build/widgets.d.ts"]
}
},
"files": [
Expand Down
109 changes: 61 additions & 48 deletions packages/components/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,66 @@ import nodeResolve from '@rollup/plugin-node-resolve'
import bundleSize from 'rollup-plugin-bundle-size'
import sizes from 'rollup-plugin-sizes'

// Mark all dependencies (peer + regular) as external so they aren't bundled.
// Consumers' bundlers (Vite, Webpack 4+, etc.) then tree-shake unused
// components AND skip pulling in transitive deps of components they don't
// import. Heavy deps (react-datepicker, react-markdown, react-colorful) are
// lazy-loaded inside their components, so they only hit the consumer's runtime
// when actually rendered.
const external = (id) =>
id === 'react' ||
id === 'react/jsx-runtime' ||
id.startsWith('react/') ||
id === 'json-edit-react' ||
id === 'react-datepicker' ||
id.startsWith('react-datepicker/') ||
id === 'react-markdown' ||
id === 'react-colorful' ||
id === 'colord' ||
id.startsWith('colord/') ||
id === 'use-debounce' ||
id === 'react-select' ||
id.startsWith('@codemirror/') ||
id.startsWith('@uiw/')

// One JS bundle per entry point. `name` is the output basename — `index` is
// the package root; `widgets` is the `@json-edit-react/components/widgets`
// subpath. Both run the TS plugin, which emits declarations into build/dts for
// the dts bundles below to flatten.
const jsBundle = (input, name) => ({
input,
output: [
{ file: `build/${name}.cjs.js`, format: 'cjs' },
{ file: `build/${name}.esm.js`, format: 'esm' },
],
external,
plugins: [
peerDepsExternal({ includeDependencies: true }),
nodeResolve(),
styles({ minimize: true }),
typescript({
module: 'ESNext',
target: 'es2020',
declaration: true,
declarationDir: 'build/dts',
}),
terser(),
bundleSize(),
sizes(),
],
})

// Flatten the per-entry declarations emitted above into a single .d.ts.
const dtsBundle = (input, name) => ({
input,
output: [{ file: `build/${name}.d.ts`, format: 'es' }],
external: [/\.css$/],
plugins: [dts()],
})

export default [
{
input: 'src/index.ts',
output: [
{ file: 'build/index.cjs.js', format: 'cjs' },
{ file: 'build/index.esm.js', format: 'esm' },
],
// Mark all dependencies (peer + regular) as external so they aren't
// bundled. Consumers' bundlers (Vite, Webpack 4+, etc.) then tree-shake
// unused components AND skip pulling in transitive deps of components
// they don't import. Heavy deps (react-datepicker, react-markdown,
// react-colorful) are lazy-loaded inside their components, so they only
// hit the consumer's runtime when actually rendered.
external: (id) =>
id === 'react' ||
id === 'react/jsx-runtime' ||
id.startsWith('react/') ||
id === 'json-edit-react' ||
id === 'react-datepicker' ||
id.startsWith('react-datepicker/') ||
id === 'react-markdown' ||
id === 'react-colorful' ||
id === 'colord' ||
id.startsWith('colord/') ||
id === 'use-debounce' ||
id === 'react-select' ||
id.startsWith('@codemirror/') ||
id.startsWith('@uiw/'),
plugins: [
peerDepsExternal({ includeDependencies: true }),
nodeResolve(),
styles({ minimize: true }),
typescript({
module: 'ESNext',
target: 'es2020',
declaration: true,
declarationDir: 'build/dts',
}),
terser(),
bundleSize(),
sizes(),
],
},
{
input: 'build/dts/index.d.ts',
output: [{ file: 'build/index.d.ts', format: 'es' }],
external: [/\.css$/],
plugins: [dts()],
},
jsBundle('src/index.ts', 'index'),
jsBundle('src/widgets/index.ts', 'widgets'),
dtsBundle('build/dts/index.d.ts', 'index'),
dtsBundle('build/dts/widgets/index.d.ts', 'widgets'),
]
7 changes: 5 additions & 2 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ export * from './BigInt'
export * from './Markdown'
export * from './Image'
export * from './ColorPicker'
export * from './ReactSelect'
export * from './CodeEditor'
export * from './ErrorIndicator'

// The definition factories' override surface; the factory builder itself
// (`createDefinitionFactory`) stays internal
export { type DefinitionOverrides } from './_common/createDefinitionFactory'

// Editor-slot widgets (`ReactSelect`, `CodeEditor`) are NOT re-exported here.
// They replace JsonEditor's built-in UI controls (`Select` / `TextEditor`)
// rather than render a node type, so they ship under their own subpath,
// `@json-edit-react/components/widgets` (see ./widgets).
Loading
Loading