diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d2cdd748..426623caf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -543,5 +543,4 @@ Our versioning strategy is as follows: ### 🧹 Chores -* `[template/nextjs]` Clean package.json scripts ([#75](https://github.com/Sitecore/content-sdk/pull/75)) -* Upgrade 3rd party dependencies ([#88](https://github.com/Sitecore/content-sdk/pull/88)) ([#92](https://github.com/Sitecore/content-sdk/pull/92)) +* `[template/nextjs]` Clean package.json scripts ([#75](https://github.com/Sitecore/content-sdk/pull/75)) \ No newline at end of file diff --git a/packages/analytics-core/package.json b/packages/analytics-core/package.json index c4cd9d290a..10ae3eada5 100644 --- a/packages/analytics-core/package.json +++ b/packages/analytics-core/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/sitecore/content-sdk/issues" }, "dependencies": { - "@sitecore-content-sdk/core": "^2.1.0", + "@sitecore-content-sdk/core": "2.1.0-beta.1", "debug": "^4.4.3", "isbot": "^5.1.39" }, @@ -75,5 +75,5 @@ "api-extractor": "npm run build && api-extractor run --local --verbose", "api-extractor:verify": "api-extractor run" }, - "version": "2.1.0" + "version": "2.1.0-beta.1" } diff --git a/packages/cli/package.json b/packages/cli/package.json index 5b827b0641..bf7e20920c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-content-sdk/cli", - "version": "2.1.0", + "version": "2.1.0-beta.1", "description": "Sitecore Content SDK CLI", "main": "dist/cjs/cli.js", "module": "dist/esm/cli.js", @@ -34,8 +34,8 @@ "url": "https://github.com/sitecore/content-sdk/issues" }, "dependencies": { - "@sitecore-content-sdk/content": "^2.1.0", - "@sitecore-content-sdk/core": "^2.1.0", + "@sitecore-content-sdk/content": "2.1.0-beta.1", + "@sitecore-content-sdk/core": "2.1.0-beta.1", "chokidar": "^4.0.3", "dotenv": "^16.5.0", "dotenv-expand": "^12.0.2", diff --git a/packages/cli/src/scripts/project/atoms/constants.ts b/packages/cli/src/scripts/project/atoms/constants.ts new file mode 100644 index 0000000000..cd18a35df5 --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/constants.ts @@ -0,0 +1,4 @@ +export const ATOMS_MODULE_PATH = 'src/atoms/index'; +export const LOCK_FILE_DIR = '.sitecore'; +export const LOCK_FILE_NAME = 'atoms.lock.json'; + diff --git a/packages/cli/src/scripts/project/atoms/index.ts b/packages/cli/src/scripts/project/atoms/index.ts new file mode 100644 index 0000000000..dc3a08f9ae --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/index.ts @@ -0,0 +1,17 @@ +import { Argv } from 'yargs'; +import * as update from './update'; +import * as validate from './validate'; + +export const command = ['atoms', 'a']; + +export const describe = 'Manage atom version locks and validate atom contracts'; + +/** + * @param {Argv} yargs + */ +export function builder(yargs: Argv) { + return yargs + .command([update, validate] as any) + .strict() + .demandCommand(1, 'You need to specify an atoms subcommand (update, validate)'); +} diff --git a/packages/cli/src/scripts/project/atoms/types.ts b/packages/cli/src/scripts/project/atoms/types.ts new file mode 100644 index 0000000000..d7ec96b188 --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/types.ts @@ -0,0 +1,61 @@ +/** + * Shape of a single atom entry in the lock file. + * @internal + */ +export interface AtomLockEntry { + /** Semver version of this component. Absent when not declared on the component. */ + version?: string; + /** Hash of the component's schema, used to detect changes in the schema. */ + hash: string; +} + +/** + * Shape of the lock file. + * @internal + */ +export interface AtomVersionsLock { + /** Catalog root version from `defineAtomsCatalog`. Absent when not declared. */ + version?: string; + /** Timestamp of when the lock file was generated. */ + generated: string; + /** Map of atom name to its lock entry. */ + atoms: Record; +} + +/** + * Atoms info extracted from the catalog, used for lock file generation and validation. + * @internal + */ +export interface AtomInfo { + /** Semver version of this component. Absent when not declared on the component. */ + version: string; + /** Hash of the component's schema, used to detect changes in the schema. */ + schemaHash: string; +} + +/** + * Map of atom name to its info (version and schema hash) extracted from the catalog. + * @internal + */ +export type AtomsInfoMap = Record; + +/** + * Load the raw catalog object from the project's atoms module. + * @returns The raw catalog export from the atoms module, which should include component definitions and optionally a version. + * @internal + */ +export interface CatalogLoadResult { + data?: { components?: Record>; version?: string }; + componentNames?: string[]; +} + +/** + * Result of a lock file validation. + * @internal + */ +export interface ValidateResult { + /** Whether the lock file is valid (all hashes and versions match). */ + valid: boolean; + /** List of issues found during validation, if any. */ + issues: string[]; +} diff --git a/packages/cli/src/scripts/project/atoms/update.test.ts b/packages/cli/src/scripts/project/atoms/update.test.ts new file mode 100644 index 0000000000..da449d2b35 --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/update.test.ts @@ -0,0 +1,140 @@ +/* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as utilsModule from './utils'; +import { handler } from './update'; + +describe('atoms/update handler', () => { + let loadCurrentAtomsStub: sinon.SinonStub; + let loadCatalogStub: sinon.SinonStub; + let writeLockFileStub: sinon.SinonStub; + let consoleLogStub: sinon.SinonStub; + + beforeEach(() => { + loadCurrentAtomsStub = sinon.stub(utilsModule, 'loadCurrentAtoms'); + loadCatalogStub = sinon.stub(utilsModule, 'loadCatalog'); + writeLockFileStub = sinon.stub(utilsModule, 'writeLockFile'); + consoleLogStub = sinon.stub(console, 'log'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should call loadCurrentAtoms and loadCatalog', async () => { + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: {} }); + + await handler(); + + expect(loadCurrentAtomsStub.calledOnce).to.be.true; + expect(loadCatalogStub.calledOnce).to.be.true; + }); + + it('should write lock file with correct atom entries', async () => { + loadCurrentAtomsStub.resolves({ + Button: { version: '1.0.0', schemaHash: 'abc123' }, + }); + loadCatalogStub.returns({ data: {} }); + + await handler(); + + expect(writeLockFileStub.calledOnce).to.be.true; + const lock = writeLockFileStub.firstCall.args[0]; + expect(lock.atoms.Button).to.deep.equal({ version: '1.0.0', hash: 'abc123' }); + }); + + it('should include catalog version in lock when catalog declares one', async () => { + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: { version: '2.0.0' } }); + + await handler(); + + const lock = writeLockFileStub.firstCall.args[0]; + expect(lock.version).to.equal('2.0.0'); + }); + + it('should not include version in lock when catalog has no version', async () => { + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: {} }); + + await handler(); + + const lock = writeLockFileStub.firstCall.args[0]; + expect(lock.version).to.be.undefined; + }); + + it('should not include version in lock when catalog.data is undefined', async () => { + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({}); + + await handler(); + + const lock = writeLockFileStub.firstCall.args[0]; + expect(lock.version).to.be.undefined; + }); + + it('should omit atom version entry when atom has no version', async () => { + loadCurrentAtomsStub.resolves({ + Card: { version: undefined, schemaHash: 'def456' }, + }); + loadCatalogStub.returns({ data: {} }); + + await handler(); + + const lock = writeLockFileStub.firstCall.args[0]; + expect(lock.atoms.Card).to.deep.equal({ hash: 'def456' }); + expect(lock.atoms.Card.version).to.be.undefined; + }); + + it('should omit atom version entry when atom version is empty string', async () => { + loadCurrentAtomsStub.resolves({ + Card: { version: '', schemaHash: 'def456' }, + }); + loadCatalogStub.returns({ data: {} }); + + await handler(); + + const lock = writeLockFileStub.firstCall.args[0]; + expect(lock.atoms.Card.version).to.be.undefined; + }); + + it('should set generated to a valid ISO timestamp', async () => { + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: {} }); + + await handler(); + + const lock = writeLockFileStub.firstCall.args[0]; + expect(lock.generated).to.be.a('string'); + expect(new Date(lock.generated).toISOString()).to.equal(lock.generated); + }); + + it('should write a lock with multiple atoms', async () => { + loadCurrentAtomsStub.resolves({ + Button: { version: '1.0.0', schemaHash: 'hash1' }, + Card: { version: undefined, schemaHash: 'hash2' }, + Banner: { version: '0.5.0', schemaHash: 'hash3' }, + }); + loadCatalogStub.returns({ data: { version: '3.0.0' } }); + + await handler(); + + const lock = writeLockFileStub.firstCall.args[0]; + expect(Object.keys(lock.atoms)).to.have.length(3); + expect(lock.atoms.Button).to.deep.equal({ version: '1.0.0', hash: 'hash1' }); + expect(lock.atoms.Card).to.deep.equal({ hash: 'hash2' }); + expect(lock.atoms.Banner).to.deep.equal({ version: '0.5.0', hash: 'hash3' }); + expect(lock.version).to.equal('3.0.0'); + }); + + it('should log success message after writing', async () => { + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: {} }); + + await handler(); + + expect(consoleLogStub.calledWith('[atoms update] Lock file updated successfully.')).to.be.true; + }); +}); + diff --git a/packages/cli/src/scripts/project/atoms/update.ts b/packages/cli/src/scripts/project/atoms/update.ts new file mode 100644 index 0000000000..4583e4a2fa --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/update.ts @@ -0,0 +1,49 @@ +import { AtomLockEntry, AtomVersionsLock } from './types'; +import { loadCatalog, loadCurrentAtoms, writeLockFile } from './utils'; + +export const command = ['update', 'u']; + +export const describe = + 'Regenerate the atom versions lock file from the current atom definitions. Run after intentional schema changes.'; + +export const builder = { + config: { + requiresArg: false, + type: 'string', + describe: 'Path to the `sitecore.cli.config` file.', + alias: 'c', + }, +}; + +export type UpdateArgs = { + config?: string; +}; + +/** + * Handler for `sitecore-tools project atoms update`. + * Regenerates `.sitecore/atoms.lock.json` from current atom definitions. + */ +export async function handler() { + const catalog = loadCatalog(); + const currentAtoms = await loadCurrentAtoms(catalog); + const catalogData = catalog.data as Record; + const catalogVersion = typeof catalogData?.version === 'string' ? catalogData.version : undefined; + + const atoms: Record = {}; + for (const [name, def] of Object.entries(currentAtoms)) { + atoms[name] = { + ...(def.version && { version: def.version }), + hash: def.schemaHash, + }; + } + + const lock: AtomVersionsLock = { + ...(catalogVersion !== undefined && { version: catalogVersion }), + generated: new Date().toISOString(), + atoms, + }; + + writeLockFile(lock); + + console.log('[atoms update] Lock file updated successfully.'); +} diff --git a/packages/cli/src/scripts/project/atoms/utils.test.ts b/packages/cli/src/scripts/project/atoms/utils.test.ts new file mode 100644 index 0000000000..262acf33f0 --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/utils.test.ts @@ -0,0 +1,349 @@ +/* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import proxyquire from 'proxyquire'; +import * as ensureDirModule from '../../../utils/ensure-sitecore-directory'; +import { + getLockFilePath, + hashSchema, + readLockFile, + writeLockFile, + resolveAtomsModulePath, +} from './utils'; + +describe('atoms/utils', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getLockFilePath', () => { + it('should return a path combining cwd, lock dir and lock file name', () => { + sinon.stub(process, 'cwd').returns('/project'); + const result = getLockFilePath(); + expect(result).to.equal(path.resolve('/project', '.sitecore', 'atoms.lock.json')); + }); + }); + + describe('hashSchema', () => { + it('should return a sha256 hex string for a given object', () => { + const result = hashSchema({ foo: 'bar' }); + const expected = crypto.createHash('sha256').update('{"foo":"bar"}').digest('hex'); + expect(result).to.equal(expected); + }); + + it('should produce the same hash for identical inputs', () => { + expect(hashSchema({ x: 1, y: 2 })).to.equal(hashSchema({ x: 1, y: 2 })); + }); + + it('should produce different hashes for different inputs', () => { + expect(hashSchema({ x: 1 })).to.not.equal(hashSchema({ x: 2 })); + }); + + it('should handle nested objects', () => { + const result = hashSchema({ props: { title: 'string' } }); + expect(result).to.be.a('string').with.length(64); + }); + }); + + describe('readLockFile', () => { + let ensureDirStub: sinon.SinonStub; + + beforeEach(() => { + ensureDirStub = sinon.stub(ensureDirModule, 'ensureSitecoreDirectory'); + }); + + it('should call ensureSitecoreDirectory', () => { + sinon.stub(fs, 'existsSync').returns(false); + readLockFile(); + expect(ensureDirStub.calledOnce).to.be.true; + }); + + it('should return null when the lock file does not exist', () => { + sinon.stub(fs, 'existsSync').returns(false); + const result = readLockFile(); + expect(result).to.be.null; + }); + + it('should return parsed lock file content when it exists', () => { + const lockData = { version: '1.0.0', generated: '2024-01-01T00:00:00Z', atoms: {} }; + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns(JSON.stringify(lockData)); + const result = readLockFile(); + expect(result).to.deep.equal(lockData); + }); + }); + + describe('writeLockFile', () => { + it('should create directory when it does not exist and write the file', () => { + const lockData = { generated: '2024-01-01T00:00:00Z', atoms: {} }; + sinon.stub(fs, 'existsSync').returns(false); + const mkdirStub = sinon.stub(fs, 'mkdirSync'); + const writeStub = sinon.stub(fs, 'writeFileSync'); + + writeLockFile(lockData as any); + + expect(mkdirStub.calledOnce).to.be.true; + expect(writeStub.calledOnce).to.be.true; + const [, content] = writeStub.firstCall.args; + expect(content).to.include('"atoms"'); + expect(content).to.include('"generated"'); + }); + + it('should not create directory when it already exists', () => { + const lockData = { generated: '2024-01-01T00:00:00Z', atoms: {} }; + sinon.stub(fs, 'existsSync').returns(true); + const mkdirStub = sinon.stub(fs, 'mkdirSync'); + sinon.stub(fs, 'writeFileSync'); + + writeLockFile(lockData as any); + + expect(mkdirStub.notCalled).to.be.true; + }); + + it('should write JSON followed by a newline', () => { + const lockData = { generated: '2024-01-01T00:00:00Z', atoms: {} }; + sinon.stub(fs, 'existsSync').returns(true); + const writeStub = sinon.stub(fs, 'writeFileSync'); + + writeLockFile(lockData as any); + + const [, content] = writeStub.firstCall.args; + expect((content as string).endsWith('\n')).to.be.true; + }); + }); + + describe('resolveAtomsModulePath', () => { + it('should return the .ts path when it exists', () => { + sinon.stub(fs, 'existsSync').callsFake((p: any) => String(p).endsWith('.ts')); + const result = resolveAtomsModulePath(); + expect(result).to.not.be.null; + expect(result).to.match(/\.ts$/); + }); + + it('should return the .tsx path when .ts does not exist but .tsx does', () => { + sinon.stub(fs, 'existsSync').callsFake((p: any) => String(p).endsWith('.tsx')); + const result = resolveAtomsModulePath(); + expect(result).to.not.be.null; + expect(result).to.match(/\.tsx$/); + }); + + it('should return null when neither .ts nor .tsx exists', () => { + sinon.stub(fs, 'existsSync').returns(false); + const result = resolveAtomsModulePath(); + expect(result).to.be.null; + }); + + it('should prefer .ts over .tsx', () => { + sinon.stub(fs, 'existsSync').returns(true); + const result = resolveAtomsModulePath(); + expect(result).to.match(/\.ts$/); + expect(result).to.not.match(/\.tsx$/); + }); + }); + + describe('loadCatalog', () => { + let fsExistsSyncStub: sinon.SinonStub; + let tsxRequireStub: sinon.SinonStub; + let utilsProxied: any; + + beforeEach(() => { + fsExistsSyncStub = sinon.stub(fs, 'existsSync'); + tsxRequireStub = sinon.stub(); + utilsProxied = proxyquire('./utils', { + 'tsx/cjs/api': { require: tsxRequireStub }, + }); + }); + + it('should throw when atoms module is not found', () => { + fsExistsSyncStub.returns(false); + expect(() => utilsProxied.loadCatalog()).to.throw('Atoms module not found'); + }); + + it('should throw when module does not export catalog', () => { + fsExistsSyncStub.returns(true); + tsxRequireStub.returns({}); + expect(() => utilsProxied.loadCatalog()).to.throw('does not export "catalog"'); + }); + + it('should return catalog from module.catalog', () => { + const mockCatalog = { data: { components: {} }, componentNames: [] }; + fsExistsSyncStub.returns(true); + tsxRequireStub.returns({ catalog: mockCatalog }); + const result = utilsProxied.loadCatalog(); + expect(result).to.deep.equal(mockCatalog); + }); + + it('should return catalog from module.default.catalog', () => { + const mockCatalog = { data: { components: {} }, componentNames: [] }; + fsExistsSyncStub.returns(true); + tsxRequireStub.returns({ default: { catalog: mockCatalog } }); + const result = utilsProxied.loadCatalog(); + expect(result).to.deep.equal(mockCatalog); + }); + }); + + describe('loadCurrentAtoms', () => { + let fsExistsSyncStub: sinon.SinonStub; + let tsxRequireStub: sinon.SinonStub; + let utilsProxied: any; + + beforeEach(() => { + fsExistsSyncStub = sinon.stub(fs, 'existsSync'); + tsxRequireStub = sinon.stub(); + utilsProxied = proxyquire('./utils', { + 'tsx/cjs/api': { require: tsxRequireStub }, + }); + }); + + it('should throw when atoms module is not found', async () => { + fsExistsSyncStub.returns(false); + try { + await utilsProxied.loadCurrentAtoms(); + expect.fail('expected to throw'); + } catch (err: any) { + expect(err.message).to.include('Atoms module not found'); + } + }); + + it('should return a map of atom name to version and schemaHash', async () => { + fsExistsSyncStub.returns(true); + const catalog = { + componentNames: ['Button', 'Card'], + data: { + components: { + Button: { version: '1.2.0', label: 'Primary Button' }, + Card: { props: { title: 'string' } }, + }, + }, + }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + expect(result.Button).to.exist; + expect(result.Button.version).to.equal('1.2.0'); + expect(result.Button.schemaHash).to.be.a('string').with.length(64); + expect(result.Card).to.exist; + expect(result.Card.version).to.be.undefined; + expect(result.Card.schemaHash).to.be.a('string').with.length(64); + }); + + it('should exclude version from schema hash computation', async () => { + fsExistsSyncStub.returns(true); + const catalog = { + componentNames: ['Button'], + data: { + components: { + Button: { version: '1.0.0', label: 'My Button' }, + }, + }, + }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + const expectedHash = crypto + .createHash('sha256') + .update(JSON.stringify({ label: 'My Button' }, null, 0)) + .digest('hex'); + expect(result.Button.schemaHash).to.equal(expectedHash); + }); + + it('should handle atoms with no properties besides version', async () => { + fsExistsSyncStub.returns(true); + const catalog = { + componentNames: ['Empty'], + data: { + components: { + Empty: { version: '1.0.0' }, + }, + }, + }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + expect(result.Empty.version).to.equal('1.0.0'); + // Hash is computed over {} (empty after removing version) + const expectedHash = crypto + .createHash('sha256') + .update(JSON.stringify({}, null, 0)) + .digest('hex'); + expect(result.Empty.schemaHash).to.equal(expectedHash); + }); + + it('should return empty map when componentNames is empty', async () => { + fsExistsSyncStub.returns(true); + const catalog = { componentNames: [], data: { components: {} } }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + expect(result).to.deep.equal({}); + }); + + it('should default componentNames to [] when catalog does not provide it', async () => { + fsExistsSyncStub.returns(true); + // catalog has no componentNames property + const catalog = { data: { components: { Button: { version: '1.0.0' } } } }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + expect(result).to.deep.equal({}); + }); + + it('should default components to {} when catalog.data.components is absent', async () => { + fsExistsSyncStub.returns(true); + // componentNames lists an atom but data.components is missing + const catalog = { componentNames: ['Button'], data: {} }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + expect(result.Button).to.exist; + expect(result.Button.version).to.be.undefined; + const expectedHash = crypto + .createHash('sha256') + .update(JSON.stringify({}, null, 0)) + .digest('hex'); + expect(result.Button.schemaHash).to.equal(expectedHash); + }); + + it('should default components to {} when catalog.data is absent', async () => { + fsExistsSyncStub.returns(true); + // componentNames lists an atom but data is missing entirely + const catalog = { componentNames: ['Card'] }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + expect(result.Card).to.exist; + expect(result.Card.version).to.be.undefined; + }); + + it('should use empty object for component data when name is not in components map', async () => { + fsExistsSyncStub.returns(true); + // componentNames has 'Ghost' but components map does not + const catalog = { + componentNames: ['Ghost'], + data: { components: {} }, + }; + tsxRequireStub.returns({ catalog }); + + const result = await utilsProxied.loadCurrentAtoms(); + + expect(result.Ghost).to.exist; + expect(result.Ghost.version).to.be.undefined; + const expectedHash = crypto + .createHash('sha256') + .update(JSON.stringify({}, null, 0)) + .digest('hex'); + expect(result.Ghost.schemaHash).to.equal(expectedHash); + }); + }); +}); + diff --git a/packages/cli/src/scripts/project/atoms/utils.ts b/packages/cli/src/scripts/project/atoms/utils.ts new file mode 100644 index 0000000000..2a98bfe872 --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/utils.ts @@ -0,0 +1,116 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import { constants } from '@sitecore-content-sdk/core'; +import { ensureSitecoreDirectory } from '../../../utils/ensure-sitecore-directory'; +import { AtomsInfoMap, AtomVersionsLock, CatalogLoadResult } from './types'; +import { ATOMS_MODULE_PATH, LOCK_FILE_DIR, LOCK_FILE_NAME } from './constants'; + +/** + * Get the absolute path to the lock file. + * @returns {string} Absolute path to the lock file. + * @internal + */ +export function getLockFilePath(): string { + return path.resolve(process.cwd(), LOCK_FILE_DIR, LOCK_FILE_NAME); +} + +/** + * Compute a SHA-256 hash of a value serialized as JSON. + * @param {unknown} schema - The component definition to hash. + * @internal + */ +export function hashSchema(schema: unknown): string { + const json = JSON.stringify(schema, null, 0); + return crypto.createHash('sha256').update(json).digest('hex'); +} + +/** + * Read the existing lock file. Returns null if it doesn't exist. + * @internal + */ +export function readLockFile(): AtomVersionsLock | null { + ensureSitecoreDirectory(); + const lockPath = getLockFilePath(); + if (!fs.existsSync(lockPath)) return null; + + const content = fs.readFileSync(lockPath, 'utf-8'); + return JSON.parse(content) as AtomVersionsLock; +} + +/** + * Write the lock file to disk. + * @param {AtomVersionsLock} lock - The lock data to write. Will be stringified as JSON. + * @internal + */ +export function writeLockFile(lock: AtomVersionsLock): void { + const lockPath = getLockFilePath(); + const dir = path.dirname(lockPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); +} + +/** + * Resolve the absolute path to the atoms module, trying common extensions. + * @internal + */ +export function resolveAtomsModulePath(): string | null { + const base = path.resolve(process.cwd(), ATOMS_MODULE_PATH); + const extensions = ['.ts', '.tsx']; + + for (const ext of extensions) { + if (fs.existsSync(base + ext)) return base + ext; + } + + return null; +} + +/** + * Load the raw catalog object from the project's atoms module. + * @internal + */ +export function loadCatalog(): CatalogLoadResult { + const modulePath = resolveAtomsModulePath(); + if (!modulePath) { + throw new Error(constants.ERROR_MESSAGES.MV_010(ATOMS_MODULE_PATH)); + } + + const tsx = require('tsx/cjs/api'); + + const atomsModule = tsx.require(modulePath, __filename); + const catalog = atomsModule.catalog ?? atomsModule.default?.catalog; + if (!catalog) throw new Error(constants.ERROR_MESSAGES.MV_011(modulePath)); + + return catalog; +} + +/** + * Load the current atom definitions from the project's atoms module. + * Uses tsx to import TypeScript at runtime. + * Returns a map of atom name to { version, schemaHash }. + * @param {CatalogLoadResult} [catalog] - Optional pre-loaded catalog. If omitted, the catalog is loaded from disk. + * @internal + */ +export async function loadCurrentAtoms(catalog?: CatalogLoadResult): Promise { + const modulePath = resolveAtomsModulePath(); + if (!modulePath) throw new Error(constants.ERROR_MESSAGES.MV_010(ATOMS_MODULE_PATH)); + + const resolvedCatalog = catalog ?? loadCatalog(); + const result: AtomsInfoMap = {}; + const componentNames = resolvedCatalog.componentNames ?? []; + const components = resolvedCatalog.data?.components ?? {}; + + for (const name of componentNames) { + // Use the component's full data (props schema, slots, etc.) for schema hash except the version, which is pulled out separately. + const { version: atomVersion, ...componentData } = components[name] ?? {}; + + result[name] = { + version: (atomVersion as string) ?? undefined, + schemaHash: hashSchema(componentData), + }; + } + + return result; +} diff --git a/packages/cli/src/scripts/project/atoms/validate.test.ts b/packages/cli/src/scripts/project/atoms/validate.test.ts new file mode 100644 index 0000000000..3a6111a5a8 --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/validate.test.ts @@ -0,0 +1,317 @@ +/* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { constants } from '@sitecore-content-sdk/core'; +import * as utilsModule from './utils'; +import * as loadConfigModule from '../../../utils/load-config'; +import { handler } from './validate'; + +describe('atoms/validate handler', () => { + let readLockFileStub: sinon.SinonStub; + let loadCurrentAtomsStub: sinon.SinonStub; + let loadCatalogStub: sinon.SinonStub; + let loadCliConfigStub: sinon.SinonStub; + let consoleLogStub: sinon.SinonStub; + let consoleErrorStub: sinon.SinonStub; + + // Default fixtures representing a perfectly valid state + const defaultLock = { + generated: '2024-01-01T00:00:00Z', + atoms: { + Button: { hash: 'hash-button', version: '1.0.0' }, + }, + }; + + const defaultCurrentAtoms = { + Button: { version: '1.0.0', schemaHash: 'hash-button' }, + }; + + const defaultCatalog = { data: {} }; + + const noBreakConfig = { atoms: { validation: { breakOnError: false } } }; + + beforeEach(() => { + readLockFileStub = sinon.stub(utilsModule, 'readLockFile'); + loadCurrentAtomsStub = sinon.stub(utilsModule, 'loadCurrentAtoms'); + loadCatalogStub = sinon.stub(utilsModule, 'loadCatalog'); + loadCliConfigStub = sinon.stub(loadConfigModule, 'default').returns(noBreakConfig as any); + consoleLogStub = sinon.stub(console, 'log'); + consoleErrorStub = sinon.stub(console, 'error'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should pass config arg to loadCliConfig', async () => { + readLockFileStub.returns(defaultLock); + loadCurrentAtomsStub.resolves(defaultCurrentAtoms); + loadCatalogStub.returns(defaultCatalog); + + await handler({ config: './custom-config.ts' }); + + expect(loadCliConfigStub.calledOnceWith('./custom-config.ts')).to.be.true; + }); + + it('should log success when lock is valid', async () => { + readLockFileStub.returns(defaultLock); + loadCurrentAtomsStub.resolves(defaultCurrentAtoms); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + expect(consoleLogStub.calledWith('[atoms validate] atoms.lock.json is up to date.')).to.be.true; + expect(consoleErrorStub.notCalled).to.be.true; + }); + + describe('lock file missing', () => { + it('should report issue when lock file is not found', async () => { + readLockFileStub.returns(null); + + await handler({}); + + expect(consoleErrorStub.calledWith(constants.ERROR_MESSAGES.IE_008)).to.be.true; + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('Lock file not found'))).to.be.true; + }); + }); + + describe('catalog version validation', () => { + it('should report version mismatch when lock and catalog versions differ', async () => { + readLockFileStub.returns({ ...defaultLock, version: '1.0.0', atoms: {} }); + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: { version: '2.0.0' } }); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('Catalog version mismatch'))).to.be.true; + }); + + it('should report mismatch when lock has version but catalog does not', async () => { + readLockFileStub.returns({ ...defaultLock, version: '1.0.0', atoms: {} }); + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: {} }); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('Catalog version mismatch'))).to.be.true; + }); + + it('should report mismatch when catalog has version but lock does not', async () => { + readLockFileStub.returns({ generated: '2024-01-01T00:00:00Z', atoms: {} }); + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: { version: '1.0.0' } }); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('Catalog version mismatch'))).to.be.true; + }); + + it('should not report version issue when neither lock nor catalog declare a version', async () => { + readLockFileStub.returns({ generated: '2024-01-01T00:00:00Z', atoms: {} }); + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: {} }); + + await handler({}); + + expect(consoleLogStub.calledWith('[atoms validate] atoms.lock.json is up to date.')).to.be + .true; + }); + + it('should not report version issue when catalog.data is undefined', async () => { + readLockFileStub.returns({ generated: '2024-01-01T00:00:00Z', atoms: {} }); + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({}); + + await handler({}); + + expect(consoleLogStub.calledWith('[atoms validate] atoms.lock.json is up to date.')).to.be + .true; + }); + + it('should not report issue when catalog versions match', async () => { + readLockFileStub.returns({ ...defaultLock, version: '1.0.0', atoms: {} }); + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns({ data: { version: '1.0.0' } }); + + await handler({}); + + expect(consoleLogStub.calledWith('[atoms validate] atoms.lock.json is up to date.')).to.be + .true; + }); + }); + + describe('atom-level validation', () => { + it('should report issue when atom in lock is missing from current definitions', async () => { + readLockFileStub.returns(defaultLock); + loadCurrentAtomsStub.resolves({}); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('"Button" is in the lock file but not found'))).to + .be.true; + }); + + it('should report issue when atom version in lock differs from current', async () => { + readLockFileStub.returns(defaultLock); + loadCurrentAtomsStub.resolves({ + Button: { version: '2.0.0', schemaHash: 'hash-button' }, + }); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('"Button" version mismatch'))).to.be.true; + }); + + it('should report issue when lock has atom version but current atom does not', async () => { + readLockFileStub.returns(defaultLock); + loadCurrentAtomsStub.resolves({ + Button: { version: undefined, schemaHash: 'hash-button' }, + }); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('"Button" version mismatch'))).to.be.true; + }); + + it('should report issue when current atom has version but lock does not', async () => { + readLockFileStub.returns({ + generated: '2024-01-01T00:00:00Z', + atoms: { Button: { hash: 'hash-button' } }, + }); + loadCurrentAtomsStub.resolves({ + Button: { version: '1.0.0', schemaHash: 'hash-button' }, + }); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('"Button" version mismatch'))).to.be.true; + }); + + it('should not report version issue when neither lock nor current atom declare a version', async () => { + readLockFileStub.returns({ + generated: '2024-01-01T00:00:00Z', + atoms: { Button: { hash: 'hash-button' } }, + }); + loadCurrentAtomsStub.resolves({ + Button: { version: undefined, schemaHash: 'hash-button' }, + }); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + expect(consoleLogStub.calledWith('[atoms validate] atoms.lock.json is up to date.')).to.be + .true; + }); + + it('should report issue when atom schema hash has changed', async () => { + readLockFileStub.returns(defaultLock); + loadCurrentAtomsStub.resolves({ + Button: { version: '1.0.0', schemaHash: 'new-hash-button' }, + }); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('"Button" schema has changed'))).to.be.true; + }); + + it('should report issue when current has a new atom not present in lock', async () => { + readLockFileStub.returns({ generated: '2024-01-01T00:00:00Z', atoms: {} }); + loadCurrentAtomsStub.resolves({ + NewAtom: { version: '1.0.0', schemaHash: 'hash-new' }, + }); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('"NewAtom" is new and not in the lock file'))).to + .be.true; + }); + + it('should accumulate multiple issues across atoms', async () => { + readLockFileStub.returns({ + generated: '2024-01-01T00:00:00Z', + atoms: { + Button: { hash: 'hash-button', version: '1.0.0' }, + Card: { hash: 'hash-card' }, + }, + }); + loadCurrentAtomsStub.resolves({ + Button: { version: '2.0.0', schemaHash: 'hash-button' }, + NewAtom: { version: undefined, schemaHash: 'hash-new' }, + }); + loadCatalogStub.returns(defaultCatalog); + + await handler({}); + + const errorArgs = consoleErrorStub.getCalls().map((c) => String(c.args[0])); + expect(errorArgs.some((msg) => msg.includes('"Button" version mismatch'))).to.be.true; + expect(errorArgs.some((msg) => msg.includes('"Card" is in the lock file but not found'))).to + .be.true; + expect(errorArgs.some((msg) => msg.includes('"NewAtom" is new and not in the lock file'))).to + .be.true; + }); + }); + + describe('breakOnError behavior', () => { + it('should throw when breakOnError is true and validation fails', async () => { + loadCliConfigStub.returns({ atoms: { validation: { breakOnError: true } } } as any); + readLockFileStub.returns(null); + + try { + await handler({}); + expect.fail('expected to throw'); + } catch (err: any) { + expect(err.message).to.include('Atom validation failed'); + } + }); + + it('should not throw when breakOnError is false and validation fails', async () => { + loadCliConfigStub.returns({ atoms: { validation: { breakOnError: false } } } as any); + readLockFileStub.returns(null); + + await handler({}); + expect(consoleErrorStub.called).to.be.true; + }); + + it('should default breakOnError to false when atoms config is absent', async () => { + loadCliConfigStub.returns({} as any); + readLockFileStub.returns(null); + + await handler({}); + expect(consoleErrorStub.called).to.be.true; + }); + + it('should log all individual issues before throwing', async () => { + loadCliConfigStub.returns({ atoms: { validation: { breakOnError: true } } } as any); + readLockFileStub.returns(defaultLock); + loadCurrentAtomsStub.resolves({ + Button: { version: '2.0.0', schemaHash: 'different-hash' }, + }); + loadCatalogStub.returns(defaultCatalog); + + try { + await handler({}); + } catch { + // expected + } + + // Both the header and individual issues should be logged before the throw + expect(consoleErrorStub.calledWith(constants.ERROR_MESSAGES.IE_008)).to.be.true; + }); + }); +}); diff --git a/packages/cli/src/scripts/project/atoms/validate.ts b/packages/cli/src/scripts/project/atoms/validate.ts new file mode 100644 index 0000000000..18dee218dd --- /dev/null +++ b/packages/cli/src/scripts/project/atoms/validate.ts @@ -0,0 +1,105 @@ +import { constants } from '@sitecore-content-sdk/core'; +import loadCliConfig from '../../../utils/load-config'; +import { ValidateResult } from './types'; +import { loadCatalog, loadCurrentAtoms, readLockFile } from './utils'; + +export const command = ['validate', 'v']; + +export const describe = + 'Validate that the current atom implementations match the lock file. Fails if any atom has changed without a version bump.'; + +export const builder = { + config: { + requiresArg: false, + type: 'string', + describe: 'Path to the `sitecore.cli.config` file.', + alias: 'c', + }, +}; + +export type ValidateArgs = { + config?: string; +}; + +/** + * Handler for `sitecore-tools project atoms validate`. + * Reads the lock file and compares hashes against current atom definitions. + * @param {ValidateArgs} argv - The arguments passed to the command. + */ +export async function handler(argv: ValidateArgs) { + const cliConfig = loadCliConfig(argv.config); + const breakOnError = cliConfig.atoms?.validation?.breakOnError ?? false; + + const result = await validateLockFile(); + + if (!result.valid) { + console.error(constants.ERROR_MESSAGES.IE_008); + for (const issue of result.issues) { + console.error(` - ${issue}`); + } + + if (breakOnError) throw new Error(constants.ERROR_MESSAGES.IE_009); + } else console.log('[atoms validate] atoms.lock.json is up to date.'); +} + +/** + * Validate the lock file against current atom definitions. + * @returns A result with issues if any atom hash or version does not match. + * @internal + */ +async function validateLockFile(): Promise { + const lock = readLockFile(); + if (!lock) + return { + valid: false, + issues: [constants.ERROR_MESSAGES.MV_012], + }; + + const currentAtoms = await loadCurrentAtoms(); + const catalogData = loadCatalog().data; + const catalogVersion = typeof catalogData?.version === 'string' ? catalogData.version : undefined; + const issues: string[] = []; + + // Check catalog root version drift (skip only when neither side declares a version) + if (lock.version !== undefined || catalogVersion !== undefined) { + if (catalogVersion !== lock.version) { + const lockSide = lock.version !== undefined ? `"${lock.version}"` : 'not set'; + const currentSide = catalogVersion !== undefined ? `"${catalogVersion}"` : 'not set'; + issues.push(constants.ERROR_MESSAGES.IV_008(lockSide, currentSide)); + } + } + + // Check for atoms in lock but missing from current definitions + for (const [name, entry] of Object.entries(lock.atoms)) { + if (!currentAtoms[name]) { + issues.push(constants.ERROR_MESSAGES.MV_013(name)); + continue; + } + + const current = currentAtoms[name]; + const currentVersion = current.version ?? undefined; + + // Check per-atom version drift (skip only when neither side declares a version) + if (entry.version !== undefined || currentVersion !== undefined) { + if (entry.version !== currentVersion) { + const lockSide = entry.version !== undefined ? `"${entry.version}"` : 'not set'; + const currentSide = currentVersion !== undefined ? `"${currentVersion}"` : 'not set'; + issues.push(constants.ERROR_MESSAGES.IV_009(name, lockSide, currentSide)); + } + } + + if (current.schemaHash !== entry.hash) { + issues.push(constants.ERROR_MESSAGES.IV_010(name)); + } + } + + // Check for new atoms not in lock file + for (const name of Object.keys(currentAtoms)) { + if (!lock.atoms[name]) issues.push(constants.ERROR_MESSAGES.MV_014(name)); + } + + return { + valid: issues.length === 0, + issues, + }; +} diff --git a/packages/cli/src/scripts/project/index.ts b/packages/cli/src/scripts/project/index.ts index c5a422b442..81bab48688 100644 --- a/packages/cli/src/scripts/project/index.ts +++ b/packages/cli/src/scripts/project/index.ts @@ -2,6 +2,7 @@ import { Argv } from 'yargs'; import * as build from './build'; import * as component from './component'; +import * as atoms from './atoms'; /** * @param {Argv} yargs @@ -12,7 +13,7 @@ export function builder(yargs: Argv) { describe: 'Performs project level operations', builder: (_yargs: Argv) => { _yargs = _yargs - .command([build, component] as any) + .command([build, component, atoms] as any) .strict() .demandCommand(1, 'You need to specify a command to run'); diff --git a/packages/cli/src/utils/ensure-sitecore-directory.test.ts b/packages/cli/src/utils/ensure-sitecore-directory.test.ts index 85c2977bbd..a7ddbc0af6 100644 --- a/packages/cli/src/utils/ensure-sitecore-directory.test.ts +++ b/packages/cli/src/utils/ensure-sitecore-directory.test.ts @@ -18,9 +18,11 @@ describe('ensureSitecoreDirectory', () => { const outputPath = path.resolve(process.cwd(), '.sitecore'); expect((fs.existsSync as sinon.SinonStub).calledWith(outputPath)).to.be.true; - expect((fs.mkdirSync as sinon.SinonStub).calledOnceWithExactly(outputPath, { - recursive: true, - })).to.be.true; + expect( + (fs.mkdirSync as sinon.SinonStub).calledOnceWithExactly(outputPath, { + recursive: true, + }) + ).to.be.true; }); it('should not create the output directory when it already exists', () => { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index aa47cc297a..72141fe07a 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -8,11 +8,5 @@ "typeRoots": ["node_modules/@types"], "declarationDir": "./types" }, - "exclude": [ - "node_modules", - "types", - "dist", - "src/test", - "src/**/*.test.ts", - ] + "exclude": ["node_modules", "types", "dist", "src/test", "src/**/*.test.ts"] } diff --git a/packages/content/api/api-surface.d.ts b/packages/content/api/api-surface.d.ts index 745f5479f9..b9765b9b93 100644 --- a/packages/content/api/api-surface.d.ts +++ b/packages/content/api/api-surface.d.ts @@ -18,5 +18,6 @@ export * from '../layout'; export * from '../media'; export * from '../personalize'; export * from '../site'; +export * from '../atoms'; export * from '../tools'; export * from '../node-tools'; diff --git a/packages/content/api/content-sdk-content.api.md b/packages/content/api/content-sdk-content.api.md index 3b2b0dafc6..c39a4db88c 100644 --- a/packages/content/api/content-sdk-content.api.md +++ b/packages/content/api/content-sdk-content.api.md @@ -17,6 +17,14 @@ import { GraphQLRequestClientConfig } from '@sitecore-content-sdk/core'; import { GraphQLRequestClientFactory } from '@sitecore-content-sdk/core'; import { GraphQLRequestClientFactoryConfig } from '@sitecore-content-sdk/core'; import { RetryStrategy } from '@sitecore-content-sdk/core'; +import { Spec } from '@json-render/core'; + +// @internal +export interface ActionCatalogEntry { + description: string; + name: string; + paramsSchema: object; +} // @internal export const addComponentPreviewHandler: (importMap: ImportEntry[], callback: (error: unknown | null, Component: unknown) => void) => (() => void) | undefined; @@ -24,6 +32,9 @@ export const addComponentPreviewHandler: (importMap: ImportEntry[], callback: (e // @internal export const addComponentUpdateHandler: (rootComponent: ComponentRendering, successCallback?: (updatedRootComponent: ComponentRendering) => void) => (() => void) | undefined; +// @internal +export const addDocumentUpdateHandler: (callback: (updatedRootComponent: Document_2) => void) => () => void; + // @internal export const addServerComponentPreviewHandler: (rootComponent: ComponentRendering, callback: (componentToUpdate: ComponentRendering | null, eventArgs: ServerComponentPreviewEventArgs) => void) => () => void; @@ -33,6 +44,39 @@ export function addStyleElement(stylesContent: string): void; // @internal export function applyMediaUrlRewrite(value: T, transform: (s: string) => string): T; +// @internal +export interface AtomCatalogActionEntry { + description: string | undefined; + name: string; + paramsSchema?: object; +} + +// @internal +export interface AtomCatalogComponentEntry { + allowedChildren?: string[]; + allowedParents?: string[]; + description?: string; + example?: unknown; + name: string; + propsSchema: object; + slots: string[]; + version?: string; +} + +// @internal +export interface AtomCatalogEntry { + description: string; + name: string; + propsSchema: object; + slots: string[]; +} + +// @internal +export interface AtomsCatalogPayload { + actions: ActionCatalogEntry[]; + components: AtomCatalogEntry[]; +} + // @public export class CdpHelper { static getComponentFriendlyId(pageId: string, componentId: string, language: string, scope?: string): string; @@ -223,8 +267,12 @@ export const defineCliConfig: (cliConfig: SitecoreCliConfigInput) => SitecoreCli // @public export const defineConfig: (config: SitecoreConfigInput) => SitecoreConfig; +// @internal +export type DesignLibraryAtomsError = 'render' | 'atoms-missing'; + // @public export enum DesignLibraryMode { + LowCode = "library-low-code", Metadata = "library-metadata", Normal = "library" } @@ -317,6 +365,12 @@ export interface DictionaryServiceConfig extends CacheOptions, GraphQLServiceCon pageSize?: number; } +// @internal +interface Document_2 extends Spec { + name: string; +} +export { Document_2 as Document } + // @internal export const EDITING_ALLOWED_ORIGINS: string[]; @@ -556,6 +610,11 @@ export const getContentStylesheetLink: (layoutData: LayoutServiceData, sitecoreE // @internal export function getDefaultMediaUrlTransformer(edgeUrl: string): (value: string) => string; +// Warning: (ae-forgotten-export) The symbol "DesignLibraryAtomsCatalogEvent" needs to be exported by the entry point api-surface.d.ts +// +// @internal +export function getDesignLibraryAtomsCatalogEvent(payload: SerializedCatalog): DesignLibraryAtomsCatalogEvent; + // Warning: (ae-forgotten-export) The symbol "DesignLibraryComponentPropsEvent" needs to be exported by the entry point api-surface.d.ts // // @internal @@ -842,6 +901,7 @@ export type PageMode = { name: PageModeName; designLibrary: { isVariantGeneration: boolean; + isLowCode: boolean; }; isNormal: boolean; isPreview: boolean; @@ -1085,9 +1145,19 @@ export type ScaffoldTemplate = { getNextSteps?: (componentOutputPath: string) => string[]; }; +// @internal +export const sendAtomsErrorEvent: (error: unknown, type: DesignLibraryAtomsError) => void; + // @internal export const sendErrorEvent: (uid: string, error: unknown, type: DesignLibraryPreviewError) => void; +// @internal +export interface SerializedCatalog { + actions: AtomCatalogActionEntry[]; + components: AtomCatalogComponentEntry[]; + version?: string; +} + // @internal export interface ServerComponentPreviewEventArgs extends DesignLibraryEvent { // (undocumented) @@ -1127,6 +1197,11 @@ export type SitecoreCliConfigInput = { componentMap?: GenerateMapArgs & { generator?: GenerateMapFunction; }; + atoms?: { + validation?: { + breakOnError?: boolean; + }; + }; }; // Warning: (ae-forgotten-export) The symbol "BaseSitecoreClient" needs to be exported by the entry point api-surface.d.ts @@ -1190,6 +1265,13 @@ export type SitecoreClientInit = Omit; diff --git a/packages/content/atoms.d.ts b/packages/content/atoms.d.ts new file mode 100644 index 0000000000..997de60048 --- /dev/null +++ b/packages/content/atoms.d.ts @@ -0,0 +1 @@ +export * from './types/atoms/index'; diff --git a/packages/content/package.json b/packages/content/package.json index 8e32669e5d..ad20c02c90 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-content-sdk/content", - "version": "2.1.0", + "version": "2.1.0-beta.1", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -15,7 +15,7 @@ "test": "mocha \"./src/**/*.test.ts\"", "prepublishOnly": "npm run build", "coverage": "nyc npm test", - "generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --outputFileStrategy Members --parametersFormat table --readme none --out ../../ref-docs/content --entryPoints src/index.ts --entryPoints src/config/index.ts --entryPoints src/config-cli/index.ts --entryPoints src/client/index.ts --entryPoints src/i18n/index.ts --entryPoints src/layout/index.ts --entryPoints src/media/index.ts --entryPoints src/personalize/index.ts --entryPoints src/site/index.ts --entryPoints src/editing/index.ts --entryPoints src/tools/index.ts --entryPoints src/tools/index-node.ts --entryPoints src/codegen/index.ts --githubPages false", + "generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --outputFileStrategy Members --parametersFormat table --readme none --out ../../ref-docs/content --entryPoints src/index.ts --entryPoints src/config/index.ts --entryPoints src/config-cli/index.ts --entryPoints src/client/index.ts --entryPoints src/i18n/index.ts --entryPoints src/layout/index.ts --entryPoints src/media/index.ts --entryPoints src/personalize/index.ts --entryPoints src/site/index.ts --entryPoints src/editing/index.ts --entryPoints src/atoms/index.ts --entryPoints src/tools/index.ts --entryPoints src/tools/index-node.ts --entryPoints src/codegen/index.ts --githubPages false", "api-extractor": "npm run build && api-extractor run --local --verbose", "api-extractor:verify": "api-extractor run" }, @@ -36,7 +36,7 @@ "url": "https://github.com/sitecore/content-sdk/issues" }, "devDependencies": { - "@sitecore-content-sdk/events": "^2.1.0", + "@sitecore-content-sdk/events": "2.1.0-beta.1", "@stylistic/eslint-plugin": "^5.2.2", "@types/chai": "^5.2.2", "@types/chai-spies": "^1.0.6", @@ -73,10 +73,11 @@ "typescript": "~5.8.3" }, "peerDependencies": { - "@sitecore-content-sdk/events": "^2.1.0" + "@sitecore-content-sdk/events": "2.1.0-beta.1" }, "dependencies": { - "@sitecore-content-sdk/core": "^2.1.0", + "@json-render/core": "0.19.0", + "@sitecore-content-sdk/core": "2.1.0-beta.1", "chalk": "^4.1.2", "debug": "^4.4.0", "glob": "^11.0.2", @@ -116,6 +117,11 @@ "require": "./dist/cjs/editing/codegen/index.js", "types": "./types/editing/codegen/index.d.ts" }, + "./atoms": { + "import": "./dist/esm/atoms/index.js", + "require": "./dist/cjs/atoms/index.js", + "types": "./types/atoms/index.d.ts" + }, "./editing": { "import": "./dist/esm/editing/index.js", "require": "./dist/cjs/editing/index.js", diff --git a/packages/content/src/atoms/design-library-bridge/constants.ts b/packages/content/src/atoms/design-library-bridge/constants.ts new file mode 100644 index 0000000000..fbefd48bb2 --- /dev/null +++ b/packages/content/src/atoms/design-library-bridge/constants.ts @@ -0,0 +1,18 @@ +/** + * Event name for component preview updates from design library + * @internal + */ +export const DESIGN_LIBRARY_COMPONENT_PREVIEW_EVENT_NAME = 'component:atoms:preview'; + +/** + * Event name for the atoms catalog registration (replaces legacy `atom:registry`). + * @internal + */ +export const DESIGN_LIBRARY_ATOMS_CATALOG_EVENT_NAME = 'atoms:catalog'; + +/** + * Event to send to design library when rendering atoms error occurs + * @internal + */ +export const DESIGN_LIBRARY_ATOMS_ERROR_EVENT_NAME = 'atoms:error'; + diff --git a/packages/content/src/atoms/design-library-bridge/events.test.ts b/packages/content/src/atoms/design-library-bridge/events.test.ts new file mode 100644 index 0000000000..8513bfc1b4 --- /dev/null +++ b/packages/content/src/atoms/design-library-bridge/events.test.ts @@ -0,0 +1,81 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { AtomsCatalogPayload } from './types'; +import { + addDocumentUpdateHandler, + getDesignLibraryAtomsCatalogEvent, + getDesignLibraryAtomsErrorEvent, +} from './events'; + +describe('design-library-bridge events', () => { + describe('getDesignLibraryAtomsCatalogEvent', () => { + it('returns an event with name "atoms:catalog"', () => { + const payload: AtomsCatalogPayload = { components: [], actions: [] }; + const event = getDesignLibraryAtomsCatalogEvent(payload); + + expect(event.name).to.equal('atoms:catalog'); + }); + + it('returns an event containing the provided catalog payload', () => { + const payload: AtomsCatalogPayload = { + components: [ + { + name: 'Button', + propsSchema: { type: 'object' }, + description: 'A button', + slots: ['default'], + }, + ], + actions: [{ name: 'submit', paramsSchema: { type: 'object' }, description: 'Submit form' }], + }; + + const event = getDesignLibraryAtomsCatalogEvent(payload); + + expect(event.message.components).to.have.length(1); + expect(event.message.components[0].name).to.equal('Button'); + expect(event.message.actions).to.have.length(1); + expect(event.message.actions[0].name).to.equal('submit'); + }); + }); + + describe('getDesignLibraryAtomsErrorEvent', () => { + it('returns an event with name "atoms:error"', () => { + const event = getDesignLibraryAtomsErrorEvent('some error', 'render'); + + expect(event.name).to.equal('atoms:error'); + expect(event.message.error).to.equal('some error'); + expect(event.message.type).to.equal('render'); + }); + }); + + describe('addDocumentUpdateHandler', () => { + let addEventListener: sinon.SinonStub; + let removeEventListener: sinon.SinonStub; + + beforeEach(() => { + addEventListener = sinon.stub(); + removeEventListener = sinon.stub(); + (global as any).window = { addEventListener, removeEventListener }; + }); + + afterEach(() => { + (global as any).window = undefined; + }); + + it('registers a message event listener', () => { + const callback = sinon.stub(); + addDocumentUpdateHandler(callback); + + expect(addEventListener).to.have.been.calledWith('message', sinon.match.func); + }); + + it('returns an unsubscribe function that removes the listener', () => { + const callback = sinon.stub(); + const unsubscribe = addDocumentUpdateHandler(callback); + + unsubscribe(); + + expect(removeEventListener).to.have.been.calledWith('message', sinon.match.func); + }); + }); +}); diff --git a/packages/content/src/atoms/design-library-bridge/events.ts b/packages/content/src/atoms/design-library-bridge/events.ts new file mode 100644 index 0000000000..56fcbacf57 --- /dev/null +++ b/packages/content/src/atoms/design-library-bridge/events.ts @@ -0,0 +1,92 @@ +import { constants } from '@sitecore-content-sdk/core'; +import { validateEvent } from '../../editing/design-library'; +import { Document, SerializedCatalog } from '../types'; +import { + DESIGN_LIBRARY_ATOMS_CATALOG_EVENT_NAME, + DESIGN_LIBRARY_ATOMS_ERROR_EVENT_NAME, + DESIGN_LIBRARY_COMPONENT_PREVIEW_EVENT_NAME, +} from './constants'; +import { + DesignLibraryAtomsCatalogEvent, + DesignLibraryAtomsError, + DesignLibraryAtomsErrorEvent, +} from './types'; + +const { ERROR_MESSAGES } = constants; + +/** + * Creates a DesignLibraryAtomsCatalogEvent with the given catalog payload. + * @param {AtomsCatalogPayload} payload - serialized catalog data + * @returns {DesignLibraryAtomsCatalogEvent} the event ready to be posted to Design Studio + * @internal + */ +export function getDesignLibraryAtomsCatalogEvent( + payload: SerializedCatalog +): DesignLibraryAtomsCatalogEvent { + return { + name: DESIGN_LIBRARY_ATOMS_CATALOG_EVENT_NAME, + message: payload as unknown as Record, + }; +} + +/** + * Generates a DesignLibraryAtomsErrorEvent depending on the type of error with the given error. + * @param {unknown} error - The error to be sent. + * @param {DesignLibraryAtomsError} type - The type of error. + * @returns An object representing the DesignLibraryAtomsErrorEvent. + * @internal + */ +export function getDesignLibraryAtomsErrorEvent( + error: unknown, + type: DesignLibraryAtomsError +): DesignLibraryAtomsErrorEvent { + return { + name: DESIGN_LIBRARY_ATOMS_ERROR_EVENT_NAME, + message: { error, type }, + }; +} + +/** + * Sends a design library atoms error event to the design library + * @param {unknown} error - The error object or message to be sent. + * @param {DesignLibraryAtomsError} type - The type of error, as defined in DesignLibraryAtomsError. + * @internal + */ +export const sendAtomsErrorEvent = (error: unknown, type: DesignLibraryAtomsError) => { + const errorEvent = getDesignLibraryAtomsErrorEvent(error, type); + console.error( + `Component Library: sending error event. ${ERROR_MESSAGES.CONTACT_SUPPORT}`, + errorEvent + ); + if (typeof window !== 'undefined') { + const target = window.parent && window.parent !== window ? window.parent : window; + target.postMessage(errorEvent, '*'); + } +}; + +/** + * Adds a handler for atom document update events from the design library. + * @param {(updatedRootComponent: Document) => void} callback - The callback to be invoked when a document update event is received. + * @returns A function to unsubscribe from the atom document update events. + * @internal + */ +export const addDocumentUpdateHandler = (callback: (updatedRootComponent: Document) => void) => { + const handler = (e: MessageEvent) => { + if (!validateEvent(e, DESIGN_LIBRARY_COMPONENT_PREVIEW_EVENT_NAME)) { + return; + } + + console.debug('Component Library atoms: message received', e.data); + + callback(e.data.document as Document); + }; + + window.addEventListener('message', handler); + + const unsubscribe = () => { + window.removeEventListener('message', handler); + }; + + return unsubscribe; +}; + diff --git a/packages/content/src/atoms/design-library-bridge/index.ts b/packages/content/src/atoms/design-library-bridge/index.ts new file mode 100644 index 0000000000..20dc99096a --- /dev/null +++ b/packages/content/src/atoms/design-library-bridge/index.ts @@ -0,0 +1,3 @@ +export * from './events'; +export * from './types'; + diff --git a/packages/content/src/atoms/design-library-bridge/types.ts b/packages/content/src/atoms/design-library-bridge/types.ts new file mode 100644 index 0000000000..0f1ffbf87c --- /dev/null +++ b/packages/content/src/atoms/design-library-bridge/types.ts @@ -0,0 +1,72 @@ +import { DesignLibraryEvent } from '../../editing/design-library'; +import { + DESIGN_LIBRARY_ATOMS_CATALOG_EVENT_NAME, + DESIGN_LIBRARY_ATOMS_ERROR_EVENT_NAME, +} from './constants'; + +/** + * Enumeration of error types for the design library atoms. + * @internal + */ +export type DesignLibraryAtomsError = 'render' | 'atoms-missing'; + +/** + * Represents a atom rendering error event to be sent to design library + * @internal + */ +export interface DesignLibraryAtomsErrorEvent extends DesignLibraryEvent { + name: typeof DESIGN_LIBRARY_ATOMS_ERROR_EVENT_NAME; + message: { + error: unknown; + type: DesignLibraryAtomsError; + }; +} + +/** + * Serialized component entry in the catalog payload sent to Design Studio. + * @internal + */ +export interface AtomCatalogEntry { + /** Component name (key in the catalog). */ + name: string; + /** JSON Schema representation of the component props. */ + propsSchema: object; + /** Human-readable description. */ + description: string; + /** Named slots (children). */ + slots: string[]; +} + +/** + * Serialized action entry in the catalog payload sent to Design Studio. + * @internal + */ +export interface ActionCatalogEntry { + /** Action name (key in the catalog). */ + name: string; + /** JSON Schema representation of the action params. */ + paramsSchema: object; + /** Human-readable description. */ + description: string; +} + +/** + * Payload of the atoms:catalog event sent to Design Studio. + * @internal + */ +export interface AtomsCatalogPayload { + /** Serialized component entries. */ + components: AtomCatalogEntry[]; + /** Serialized action entries. */ + actions: ActionCatalogEntry[]; +} + +/** + * Represents the atoms:catalog event sent to Design Studio. + * @internal + */ +export interface DesignLibraryAtomsCatalogEvent extends DesignLibraryEvent { + name: typeof DESIGN_LIBRARY_ATOMS_CATALOG_EVENT_NAME; + message: Record; +} + diff --git a/packages/content/src/atoms/index.ts b/packages/content/src/atoms/index.ts new file mode 100644 index 0000000000..6bce6fa4fd --- /dev/null +++ b/packages/content/src/atoms/index.ts @@ -0,0 +1,17 @@ +export type { + SerializedCatalog, + SitecoreComponentMeta, + AtomCatalogComponentEntry, + AtomCatalogActionEntry, + Document, +} from './types'; + +export { + AtomCatalogEntry, + ActionCatalogEntry, + AtomsCatalogPayload, + getDesignLibraryAtomsCatalogEvent, + sendAtomsErrorEvent, + DesignLibraryAtomsError, + addDocumentUpdateHandler, +} from './design-library-bridge'; diff --git a/packages/content/src/atoms/types.ts b/packages/content/src/atoms/types.ts new file mode 100644 index 0000000000..7195c9aa7b --- /dev/null +++ b/packages/content/src/atoms/types.ts @@ -0,0 +1,72 @@ +import { Spec } from '@json-render/core'; + +/** + * Serialized atom info for a single component, sent to Design Studio. + * @internal + */ +export interface AtomCatalogComponentEntry { + /** Component name (key in the catalog). */ + name: string; + /** JSON Schema representation of the component props. */ + propsSchema: object; + /** Human-readable description. */ + description?: string; + /** Named slots (children). */ + slots: string[]; + /** Semver version of this component definition. */ + version?: string; + /** Component names that are allowed as children in this component's slots. */ + allowedChildren?: string[]; + /** Component names that this component is allowed to be placed inside. */ + allowedParents?: string[]; + /** Example prop values for AI prompt generation. Auto-generated from Zod schema if omitted. */ + example?: unknown; +} + +/** + * Serialized action info, sent to Design Studio. + * @internal + */ +export interface AtomCatalogActionEntry { + /** Action name (key in the catalog). */ + name: string; + /** JSON Schema representation of the action params. */ + paramsSchema?: object; + /** Human-readable description. */ + description: string | undefined; +} + +/** + * Full catalog payload sent to Design Studio. + * @internal + */ +export interface SerializedCatalog { + /** Catalog root version from `defineAtomsCatalog`. Absent when not declared. */ + version?: string; + /** Serialized component entries. */ + components: AtomCatalogComponentEntry[]; + /** Serialized action entries. */ + actions: AtomCatalogActionEntry[]; +} + +/** + * Sitecore-specific placement metadata added to a component definition. + * @public + */ +export interface SitecoreComponentMeta { + /** Semver version of this component definition. */ + version?: string; + /** Component names that are allowed as children in this component's slots. */ + allowedChildren?: string[]; + /** Component names that this component is allowed to be placed inside. */ + allowedParents?: string[]; +} + +/** + * A document is a JSON object that conforms to the JSON Schema specification. + * @internal + */ +export interface Document extends Spec { + /** Human-readable identifier of the document. */ + name: string; +} diff --git a/packages/content/src/client/sitecore-client.test.ts b/packages/content/src/client/sitecore-client.test.ts index 0e878c7fa6..7bbe6e5f19 100644 --- a/packages/content/src/client/sitecore-client.test.ts +++ b/packages/content/src/client/sitecore-client.test.ts @@ -366,6 +366,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -410,6 +411,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -608,6 +610,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -653,6 +656,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -700,6 +704,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -887,6 +892,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -940,6 +946,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -1147,6 +1154,7 @@ describe('SitecoreClient', () => { isEditing: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }, }); @@ -1208,6 +1216,7 @@ describe('SitecoreClient', () => { isEditing: true, designLibrary: { isVariantGeneration: true, + isLowCode: false, }, }, }); @@ -1297,6 +1306,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }); @@ -1308,6 +1318,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }); @@ -1319,6 +1330,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }); @@ -1330,6 +1342,7 @@ describe('SitecoreClient', () => { isDesignLibrary: true, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }); @@ -1337,6 +1350,7 @@ describe('SitecoreClient', () => { name: DesignLibraryMode.Metadata, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, isNormal: false, isPreview: false, @@ -1348,6 +1362,7 @@ describe('SitecoreClient', () => { name: DesignLibraryMode.Normal, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, isNormal: false, isPreview: false, @@ -1355,6 +1370,18 @@ describe('SitecoreClient', () => { isDesignLibrary: true, }); + expect(sitecoreClient['getPageMode'](DesignLibraryMode.LowCode)).to.deep.equal({ + name: DesignLibraryMode.LowCode, + isNormal: false, + isPreview: false, + isEditing: true, + isDesignLibrary: true, + designLibrary: { + isVariantGeneration: false, + isLowCode: true, + }, + }); + expect(sitecoreClient['getPageMode']('invalid-mode' as any)).to.deep.equal({ name: 'invalid-mode', isNormal: false, @@ -1363,6 +1390,7 @@ describe('SitecoreClient', () => { isDesignLibrary: false, designLibrary: { isVariantGeneration: false, + isLowCode: false, }, }); }); diff --git a/packages/content/src/client/sitecore-client.ts b/packages/content/src/client/sitecore-client.ts index b0038ff3c2..340ea0f2ad 100644 --- a/packages/content/src/client/sitecore-client.ts +++ b/packages/content/src/client/sitecore-client.ts @@ -74,6 +74,10 @@ export type PageMode = { * Whether the page is in variant generation mode */ isVariantGeneration: boolean; + /** + * Whether the page is in low code component editing mode + */ + isLowCode: boolean; }; /** * Whether the page is in normal mode @@ -750,7 +754,7 @@ export class SitecoreClient implements BaseSitecoreClient { isPreview: false, isEditing: false, isDesignLibrary: false, - designLibrary: { isVariantGeneration: false }, + designLibrary: { isVariantGeneration: false, isLowCode: false }, }; switch (mode) { @@ -774,6 +778,11 @@ export class SitecoreClient implements BaseSitecoreClient { pageMode.isDesignLibrary = true; pageMode.isEditing = true; break; + case DesignLibraryMode.LowCode: + pageMode.isDesignLibrary = true; + pageMode.designLibrary.isLowCode = true; + pageMode.isEditing = true; + break; default: break; diff --git a/packages/content/src/config/models.ts b/packages/content/src/config/models.ts index 17acda07c3..a9c5a431be 100644 --- a/packages/content/src/config/models.ts +++ b/packages/content/src/config/models.ts @@ -260,6 +260,22 @@ export type SitecoreCliConfigInput = { */ generator?: GenerateMapFunction; }; + /** + * Configuration for the `sitecore-tools project atoms` CLI commands. + */ + atoms?: { + /** + * Validation configuration for atoms. + */ + validation?: { + /** + * When true, the CLI will exit with a non-zero code on validation errors. + * Useful for CI pipelines that should fail on broken atom contracts. + * @default false + */ + breakOnError?: boolean; + }; + }; }; /** diff --git a/packages/content/src/editing/models.ts b/packages/content/src/editing/models.ts index 198806f6ec..4017bf0de2 100644 --- a/packages/content/src/editing/models.ts +++ b/packages/content/src/editing/models.ts @@ -82,6 +82,8 @@ export enum DesignLibraryMode { Normal = 'library', /** Metadata mode */ Metadata = 'library-metadata', + /** Low code mode */ + LowCode = 'library-low-code', } /** diff --git a/packages/content/tsconfig.json b/packages/content/tsconfig.json index 89997e6ec7..7c8692c69d 100644 --- a/packages/content/tsconfig.json +++ b/packages/content/tsconfig.json @@ -25,6 +25,7 @@ "personalize.d.ts", "config-cli.d.ts", "codegen.d.ts", + "atoms.d.ts", "src/**/*.test.ts", "src/test-data/**/*", "src/tools/codegen/test-data/**/*", diff --git a/packages/core/api/content-sdk-core.api.md b/packages/core/api/content-sdk-core.api.md index d7c0a877dc..17fa5fa248 100644 --- a/packages/core/api/content-sdk-core.api.md +++ b/packages/core/api/content-sdk-core.api.md @@ -122,6 +122,9 @@ const ERROR_MESSAGES: { readonly IV_005: "[IV-005] Incorrect value for \"expiryDate\". Format the value according to ISO 8601."; readonly IV_006: (maxAttributes: number) => string; readonly IV_007: (siteName: string) => string; + readonly IV_008: (lockSide: string, currentSide: string) => string; + readonly IV_009: (name: string, lockSide: string, currentSide: string) => string; + readonly IV_010: (name: string) => string; readonly IE_001: (pluginName: string, dependency: string) => string; readonly IE_002: "[IE-002] SDK not initialized. You must first initialize the SDK using \"initContentSdk()\"."; readonly IE_003: "[IE-003] Timeout exceeded. The server did not respond within the allotted time."; @@ -129,6 +132,8 @@ const ERROR_MESSAGES: { readonly IE_005: "[IE-005] Unable to set the \"sc_cid\" cookie because the client ID could not be retrieved from the server. Make sure to set the correct values for \"contextId\" and \"siteName\". If the issue persists, try again later or use try-catch blocks to handle this error."; readonly IE_006: "[IE-006] Unable to set the \"sc_cid_personalize\" cookie because the visitor ID could not be retrieved from the server. Make sure to set the correct values for \"contextId\" and \"siteName\". If the issue persists, try again later or use try-catch blocks to handle this error."; readonly IE_007: (hostName: string) => string; + readonly IE_008: "[IE-008] Lock file validation failed:"; + readonly IE_009: "[IE-009] Atom validation failed. See issues above. You see this error because `breakOnError` is enabled in your CLI config."; readonly MV_001: "[MV-001] \"contextId\" is required."; readonly MV_002: "[MV-002] \"siteName\" is required."; readonly MV_003: "[MV-003] \"identifiers\" is required."; @@ -138,6 +143,11 @@ const ERROR_MESSAGES: { readonly MV_007: "[MV-007] Provide either \"contextId\" or both \"apiHost\" and \"apiKey\"."; readonly MV_008: "[MV-008] Verify that sitecore.config is properly imported and correctly referenced."; readonly MV_009: "[MV-009] \"language\" is required."; + readonly MV_010: (modulePath: string) => string; + readonly MV_011: (modulePath: string) => string; + readonly MV_012: "[MV-012] Lock file not found. Run `sitecore-tools project atoms update` to generate it."; + readonly MV_013: (name: string) => string; + readonly MV_014: (name: string) => string; readonly CONTACT_SUPPORT: "If the issue persists, please contact Sitecore Support."; }; diff --git a/packages/core/package.json b/packages/core/package.json index 6bb3c7c784..63d940a9b4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-content-sdk/core", - "version": "2.1.0", + "version": "2.1.0-beta.1", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index fab9569dc0..d72b455d7a 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -51,6 +51,11 @@ export const ERROR_MESSAGES = { `[IV-006] "extensionData" supports maximum ${maxAttributes} attributes. Reduce the number of attributes.`, IV_007: (siteName: string) => `[IV-007] Site "${siteName}" does not exist or site item tree is missing.`, + IV_008: (lockSide: string, currentSide: string) => + `[IV-008] Catalog version mismatch: lock file has ${lockSide}, current is ${currentSide}.`, + IV_009: (name: string, lockSide: string, currentSide: string) => + `[IV-009] Atom "${name}" version mismatch: lock file has ${lockSide}, current is ${currentSide}.`, + IV_010: (name: string) => `[IV-010] Atom "${name}" schema has changed.`, /** IE errors are related to incorrect execution */ IE_001: (pluginName: string, dependency: string) => @@ -65,6 +70,9 @@ export const ERROR_MESSAGES = { IE_006: '[IE-006] Unable to set the "sc_cid_personalize" cookie because the visitor ID could not be retrieved from the server. Make sure to set the correct values for "contextId" and "siteName". If the issue persists, try again later or use try-catch blocks to handle this error.', IE_007: (hostName: string) => `[IE-007] Could not resolve site for host "${hostName}".`, + IE_008: '[IE-008] Lock file validation failed:', + IE_009: + '[IE-009] Atom validation failed. See issues above. You see this error because `breakOnError` is enabled in your CLI config.', /** MV errors are related to missing values */ MV_001: '[MV-001] "contextId" is required.', @@ -77,6 +85,15 @@ export const ERROR_MESSAGES = { MV_007: '[MV-007] Provide either "contextId" or both "apiHost" and "apiKey".', MV_008: '[MV-008] Verify that sitecore.config is properly imported and correctly referenced.', MV_009: '[MV-009] "language" is required.', + MV_010: (modulePath: string) => + `[MV-010] Atoms module not found at ${modulePath}.{ts,tsx}. Ensure your atoms are defined in src/atoms/index.{ts,tsx} and export a catalog.`, + MV_011: (modulePath: string) => + `[MV-011] Atoms module at ${modulePath} does not export "catalog". Export the result of defineAtomsCatalog as "catalog".`, + MV_012: '[MV-012] Lock file not found. Run `sitecore-tools project atoms update` to generate it.', + MV_013: (name: string) => + `[MV-013] Atom "${name}" is in the lock file but not found in current definitions.`, + MV_014: (name: string) => + `[MV-014] Atom "${name}" is new and not in the lock file. Run \`sitecore-tools project atoms update\` to add it.`, /** Generic follow-up when the user should contact support */ CONTACT_SUPPORT: 'If the issue persists, please contact Sitecore Support.', diff --git a/packages/create-content-sdk-app/package.json b/packages/create-content-sdk-app/package.json index 6d89a083ab..fafe0d3d5c 100644 --- a/packages/create-content-sdk-app/package.json +++ b/packages/create-content-sdk-app/package.json @@ -1,6 +1,6 @@ { "name": "create-content-sdk-app", - "version": "2.1.0", + "version": "2.1.0-beta.1", "description": "Sitecore Content SDK initializer", "bin": "./dist/index.js", "scripts": { diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/gitignore b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/gitignore index 162d77c44a..0a3a3a3838 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/gitignore +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/gitignore @@ -22,4 +22,5 @@ .vercel # sitecore temp files (regenerated by `sitecore-tools project component generate-map`) -.sitecore/ +.sitecore/* +!.sitecore/atoms.lock.json \ No newline at end of file diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/package.json b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/package.json index c699a9cdf2..191b623928 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/package.json +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/package.json @@ -16,7 +16,7 @@ }, "license": "Apache-2.0", "scripts": { - "build": "cross-env NODE_ENV=production npm-run-all --serial sitecore-tools:generate-map sitecore-tools:build next:build", + "build": "cross-env NODE_ENV=production npm-run-all --serial sitecore-tools:atoms:validate sitecore-tools:generate-map sitecore-tools:build next:build", "lint": "eslint ./src/**/*.tsx ./src/**/*.ts", "next:build": "next build", "next:dev": "cross-env NODE_OPTIONS='--inspect' next dev", @@ -24,8 +24,10 @@ "sitecore-tools:generate-map": "sitecore-tools project component generate-map", "sitecore-tools:generate-map:watch": "sitecore-tools project component generate-map --watch", "sitecore-tools:build": "sitecore-tools project build", - "dev": "cross-env NODE_ENV=development npm-run-all --serial sitecore-tools:generate-map sitecore-tools:build --parallel next:dev sitecore-tools:generate-map:watch", - "start": "cross-env-shell NODE_ENV=production npm-run-all --serial build next:start" + "dev": "cross-env NODE_ENV=development npm-run-all --serial sitecore-tools:atoms:validate sitecore-tools:generate-map sitecore-tools:build --parallel next:dev sitecore-tools:generate-map:watch", + "start": "cross-env-shell NODE_ENV=production npm-run-all --serial build next:start", + "sitecore-tools:atoms:validate": "sitecore-tools project atoms validate", + "sitecore-tools:atoms:update": "sitecore-tools project atoms update" }, "dependencies": { "@sitecore-content-sdk/analytics-core": "<%- versions['@sitecore-content-sdk/analytics-core'] %>", diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/sitecore.cli.config.ts b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/sitecore.cli.config.ts index 4f756541f4..c558cc9b57 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/sitecore.cli.config.ts +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/sitecore.cli.config.ts @@ -19,6 +19,11 @@ export default defineCliConfig({ }), ], }, + atoms: { + validation: { + breakOnError: false, + }, + }, componentMap: { paths: ['src/components'], exclude: ['src/components/content-sdk/*'], diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/src/Providers.tsx b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/src/Providers.tsx index 2bc2eed055..bee8fe4610 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/src/Providers.tsx +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/src/Providers.tsx @@ -3,14 +3,19 @@ import React from 'react'; import { Page, SitecoreProvider } from '@sitecore-content-sdk/nextjs'; import scConfig from 'sitecore.config'; import components from '.sitecore/component-map.client'; +import { catalog, registry } from 'src/atoms'; +import { useRouter } from 'next/navigation'; export default function Providers({ children, page }: { children: React.ReactNode; page: Page }) { + const router = useRouter(); + return ( import('.sitecore/import-map.client')} + atomsConfig={{ catalog, registry, navigate: router.push }} > {children} diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/src/atoms/index.tsx b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/src/atoms/index.tsx new file mode 100644 index 0000000000..94edbdb118 --- /dev/null +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router-cache-components/src/atoms/index.tsx @@ -0,0 +1,10 @@ +import { defineAtomsCatalog, defineAtomsRegistry } from '@sitecore-content-sdk/nextjs'; + +export const catalog = defineAtomsCatalog({ + components: {}, + actions: {}, +}); + +export const registry = defineAtomsRegistry(catalog, { + components: {}, +}); diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/gitignore b/packages/create-content-sdk-app/src/templates/nextjs-app-router/gitignore index 1bab76c255..c2553a1bca 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/gitignore +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/gitignore @@ -21,5 +21,6 @@ # vercel .vercel -# sitecore temp files -.sitecore/ +# sitecore files +.sitecore/* +!.sitecore/atoms.lock.json \ No newline at end of file diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/package.json b/packages/create-content-sdk-app/src/templates/nextjs-app-router/package.json index f35727af14..37ab0a17ac 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/package.json +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/package.json @@ -16,7 +16,7 @@ }, "license": "Apache-2.0", "scripts": { - "build": "cross-env NODE_ENV=production npm-run-all --serial sitecore-tools:generate-map sitecore-tools:build next:build", + "build": "cross-env NODE_ENV=production npm-run-all --serial sitecore-tools:atoms:validate sitecore-tools:generate-map sitecore-tools:build next:build", "lint": "eslint ./src/**/*.tsx ./src/**/*.ts", "next:build": "next build", "next:dev": "cross-env NODE_OPTIONS='--inspect' next dev", @@ -24,8 +24,10 @@ "sitecore-tools:generate-map": "sitecore-tools project component generate-map", "sitecore-tools:generate-map:watch": "sitecore-tools project component generate-map --watch", "sitecore-tools:build": "sitecore-tools project build", - "dev": "cross-env NODE_ENV=development npm-run-all --serial sitecore-tools:generate-map sitecore-tools:build --parallel next:dev sitecore-tools:generate-map:watch", - "start": "cross-env-shell NODE_ENV=production npm-run-all --serial build next:start" + "dev": "cross-env NODE_ENV=development npm-run-all --serial sitecore-tools:atoms:validate sitecore-tools:generate-map sitecore-tools:build --parallel next:dev sitecore-tools:generate-map:watch", + "start": "cross-env-shell NODE_ENV=production npm-run-all --serial build next:start", + "sitecore-tools:atoms:validate": "sitecore-tools project atoms validate", + "sitecore-tools:atoms:update": "sitecore-tools project atoms update" }, "dependencies": { "@sitecore-content-sdk/analytics-core": "<%- versions['@sitecore-content-sdk/analytics-core'] %>", @@ -37,7 +39,8 @@ "next": "^16.2.0", "react": "^19.2.1", "react-dom": "^19.2.1", - "next-intl": "^4.3.5" + "next-intl": "^4.3.5", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.cli.config.ts b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.cli.config.ts index 4f756541f4..c558cc9b57 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.cli.config.ts +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/sitecore.cli.config.ts @@ -19,6 +19,11 @@ export default defineCliConfig({ }), ], }, + atoms: { + validation: { + breakOnError: false, + }, + }, componentMap: { paths: ['src/components'], exclude: ['src/components/content-sdk/*'], diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/src/Providers.tsx b/packages/create-content-sdk-app/src/templates/nextjs-app-router/src/Providers.tsx index 2bc2eed055..bee8fe4610 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs-app-router/src/Providers.tsx +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/src/Providers.tsx @@ -3,14 +3,19 @@ import React from 'react'; import { Page, SitecoreProvider } from '@sitecore-content-sdk/nextjs'; import scConfig from 'sitecore.config'; import components from '.sitecore/component-map.client'; +import { catalog, registry } from 'src/atoms'; +import { useRouter } from 'next/navigation'; export default function Providers({ children, page }: { children: React.ReactNode; page: Page }) { + const router = useRouter(); + return ( import('.sitecore/import-map.client')} + atomsConfig={{ catalog, registry, navigate: router.push }} > {children} diff --git a/packages/create-content-sdk-app/src/templates/nextjs-app-router/src/atoms/index.tsx b/packages/create-content-sdk-app/src/templates/nextjs-app-router/src/atoms/index.tsx new file mode 100644 index 0000000000..94edbdb118 --- /dev/null +++ b/packages/create-content-sdk-app/src/templates/nextjs-app-router/src/atoms/index.tsx @@ -0,0 +1,10 @@ +import { defineAtomsCatalog, defineAtomsRegistry } from '@sitecore-content-sdk/nextjs'; + +export const catalog = defineAtomsCatalog({ + components: {}, + actions: {}, +}); + +export const registry = defineAtomsRegistry(catalog, { + components: {}, +}); diff --git a/packages/create-content-sdk-app/src/templates/nextjs/gitignore b/packages/create-content-sdk-app/src/templates/nextjs/gitignore index 1bab76c255..9aa0b92590 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/gitignore +++ b/packages/create-content-sdk-app/src/templates/nextjs/gitignore @@ -21,5 +21,6 @@ # vercel .vercel -# sitecore temp files -.sitecore/ +# sitecore files +.sitecore/* +!.sitecore/atoms.lock.json diff --git a/packages/create-content-sdk-app/src/templates/nextjs/package.json b/packages/create-content-sdk-app/src/templates/nextjs/package.json index 4bb81df427..e317e6998c 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/package.json +++ b/packages/create-content-sdk-app/src/templates/nextjs/package.json @@ -48,7 +48,7 @@ "typescript": "~5.8.3" }, "scripts": { - "build": "cross-env NODE_ENV=production npm-run-all --serial sitecore-tools:generate-map sitecore-tools:build next:build", + "build": "cross-env NODE_ENV=production npm-run-all --serial sitecore-tools:atoms:validate sitecore-tools:generate-map sitecore-tools:build next:build", "lint": "eslint ./src/**/*.tsx ./src/**/*.ts", "next:build": "next build", "next:dev": "cross-env NODE_OPTIONS='--inspect' next dev", @@ -56,7 +56,9 @@ "sitecore-tools:generate-map": "sitecore-tools project component generate-map", "sitecore-tools:generate-map:watch": "sitecore-tools project component generate-map --watch", "sitecore-tools:build": "sitecore-tools project build", - "dev": "cross-env NODE_ENV=development npm-run-all --serial sitecore-tools:generate-map sitecore-tools:build --parallel next:dev sitecore-tools:generate-map:watch", - "start": "cross-env-shell NODE_ENV=production npm-run-all --serial build next:start" + "dev": "cross-env NODE_ENV=development npm-run-all --serial sitecore-tools:atoms:validate sitecore-tools:generate-map sitecore-tools:build --parallel next:dev sitecore-tools:generate-map:watch", + "start": "cross-env-shell NODE_ENV=production npm-run-all --serial build next:start", + "sitecore-tools:atoms:validate": "sitecore-tools project atoms validate", + "sitecore-tools:atoms:update": "sitecore-tools project atoms update" } } diff --git a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.cli.config.ts b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.cli.config.ts index 3a0056c3b9..ba5f6692c5 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/sitecore.cli.config.ts +++ b/packages/create-content-sdk-app/src/templates/nextjs/sitecore.cli.config.ts @@ -19,6 +19,11 @@ export default defineCliConfig({ }), ], }, + atoms: { + validation: { + breakOnError: false, + }, + }, componentMap: { paths: ['src/components'], // Exclude content-sdk auxillary components diff --git a/packages/create-content-sdk-app/src/templates/nextjs/src/Providers.tsx b/packages/create-content-sdk-app/src/templates/nextjs/src/Providers.tsx index 1a0136e949..e879fa0ee2 100644 --- a/packages/create-content-sdk-app/src/templates/nextjs/src/Providers.tsx +++ b/packages/create-content-sdk-app/src/templates/nextjs/src/Providers.tsx @@ -6,6 +6,8 @@ import { } from '@sitecore-content-sdk/nextjs'; import components from '.sitecore/component-map'; import scConfig from 'sitecore.config'; +import { catalog, registry } from 'src/atoms'; +import { useRouter } from 'next/navigation'; const Providers = ({ children, @@ -16,6 +18,8 @@ const Providers = ({ componentProps?: ComponentPropsCollection; page: Page; }) => { + const router = useRouter(); + return ( import('.sitecore/import-map')} + atomsConfig={{ catalog, registry, navigate: router.push }} > {children} diff --git a/packages/create-content-sdk-app/src/templates/nextjs/src/atoms/index.tsx b/packages/create-content-sdk-app/src/templates/nextjs/src/atoms/index.tsx new file mode 100644 index 0000000000..94edbdb118 --- /dev/null +++ b/packages/create-content-sdk-app/src/templates/nextjs/src/atoms/index.tsx @@ -0,0 +1,10 @@ +import { defineAtomsCatalog, defineAtomsRegistry } from '@sitecore-content-sdk/nextjs'; + +export const catalog = defineAtomsCatalog({ + components: {}, + actions: {}, +}); + +export const registry = defineAtomsRegistry(catalog, { + components: {}, +}); diff --git a/packages/events/package.json b/packages/events/package.json index 10c00c94d9..ff8d2de223 100644 --- a/packages/events/package.json +++ b/packages/events/package.json @@ -7,8 +7,8 @@ "url": "https://github.com/Sitecore/content-sdk/issues" }, "dependencies": { - "@sitecore-content-sdk/analytics-core": "^2.1.0", - "@sitecore-content-sdk/core": "^2.1.0", + "@sitecore-content-sdk/analytics-core": "2.1.0-beta.1", + "@sitecore-content-sdk/core": "2.1.0-beta.1", "debug": "^4.4.3" }, "description": "Enables real-time, unified tracking to send events to Sitecore.", @@ -76,5 +76,5 @@ "api-extractor": "npm run build && api-extractor run --local --verbose", "api-extractor:verify": "api-extractor run" }, - "version": "2.1.0" + "version": "2.1.0-beta.1" } diff --git a/packages/nextjs/api/content-sdk-nextjs.api.md b/packages/nextjs/api/content-sdk-nextjs.api.md index 79ff65d1e6..9ac3535330 100644 --- a/packages/nextjs/api/content-sdk-nextjs.api.md +++ b/packages/nextjs/api/content-sdk-nextjs.api.md @@ -7,6 +7,13 @@ import { AnalyticsAdapter } from '@sitecore-content-sdk/analytics-core/internal'; import { AppPlaceholder } from '@sitecore-content-sdk/react'; import { AppPlaceholderProps } from '@sitecore-content-sdk/react'; +import { AtomActionDefinition } from '@sitecore-content-sdk/react'; +import { AtomActionHandler } from '@sitecore-content-sdk/react'; +import { AtomComponentDefinition } from '@sitecore-content-sdk/react'; +import { AtomsActionsMap } from '@sitecore-content-sdk/react'; +import { AtomsCatalogInput } from '@sitecore-content-sdk/react'; +import { AtomsComponentsMap } from '@sitecore-content-sdk/react'; +import { AtomsConfig } from '@sitecore-content-sdk/react'; import { BYOCClientWrapper } from '@sitecore-content-sdk/react'; import { BYOCComponent } from '@sitecore-content-sdk/react'; import { BYOCComponentParams } from '@sitecore-content-sdk/react'; @@ -27,10 +34,14 @@ import { ComponentRendering } from '@sitecore-content-sdk/content/layout'; import { constants } from '@sitecore-content-sdk/core'; import { createGraphQLClientFactory } from '@sitecore-content-sdk/content/client'; import { DateField } from '@sitecore-content-sdk/react'; +import { DateFieldSchema } from '@sitecore-content-sdk/react'; +import { dateFieldSchema } from '@sitecore-content-sdk/react'; import { DeepRequired } from '@sitecore-content-sdk/content/config'; import { DefaultEmptyFieldEditingComponentImage } from '@sitecore-content-sdk/react'; import { DefaultEmptyFieldEditingComponentText } from '@sitecore-content-sdk/react'; import { DefaultRetryStrategy } from '@sitecore-content-sdk/content/client'; +import { defineAtomsCatalog as defineAtomsCatalog_2 } from '@sitecore-content-sdk/react'; +import { defineAtomsRegistry as defineAtomsRegistry_2 } from '@sitecore-content-sdk/react'; import { DesignLibrary } from '@sitecore-content-sdk/react'; import { DesignLibraryRenderPreviewData } from '@sitecore-content-sdk/content/editing'; import { DictionaryPhrases } from '@sitecore-content-sdk/content/i18n'; @@ -60,6 +71,8 @@ import { FetchOptions } from '@sitecore-content-sdk/content/client'; import { Field } from '@sitecore-content-sdk/content/layout'; import { File as File_2 } from '@sitecore-content-sdk/react'; import { FileField } from '@sitecore-content-sdk/react'; +import { FileFieldSchema } from '@sitecore-content-sdk/react'; +import { fileFieldSchema } from '@sitecore-content-sdk/react'; import { Form } from '@sitecore-content-sdk/react'; import { GenerateMapArgs } from '@sitecore-content-sdk/content/tools'; import { GenerateMapFunction } from '@sitecore-content-sdk/content/tools'; @@ -88,6 +101,8 @@ import { GraphQLRequestClientFactoryConfig } from '@sitecore-content-sdk/content import { HTMLLink } from '@sitecore-content-sdk/content'; import { Image as Image_2 } from '@sitecore-content-sdk/react'; import { ImageField } from '@sitecore-content-sdk/react'; +import { ImageFieldSchema } from '@sitecore-content-sdk/react'; +import { imageFieldSchema } from '@sitecore-content-sdk/react'; import { ImageFieldValue } from '@sitecore-content-sdk/react'; import { ImageProps } from '@sitecore-content-sdk/react'; import { ImageProps as ImageProps_2 } from 'next/image'; @@ -106,6 +121,8 @@ import { LayoutServiceContextData } from '@sitecore-content-sdk/content/layout'; import { LayoutServiceData } from '@sitecore-content-sdk/content/layout'; import { LayoutServicePageState } from '@sitecore-content-sdk/content/layout'; import { LinkField } from '@sitecore-content-sdk/react'; +import { LinkFieldSchema } from '@sitecore-content-sdk/react'; +import { linkFieldSchema } from '@sitecore-content-sdk/react'; import { LinkFieldValue } from '@sitecore-content-sdk/react'; import { LinkProps as LinkProps_2 } from '@sitecore-content-sdk/react'; import { LinkProps as LinkProps_3 } from 'next/link'; @@ -136,6 +153,7 @@ import { PlaceholderComponentProps } from '@sitecore-content-sdk/react'; import { PlaceholderData } from '@sitecore-content-sdk/content/layout'; import { PlaceholdersData } from '@sitecore-content-sdk/content/layout'; import { PreviewData } from 'next'; +import { PropMeta } from '@sitecore-content-sdk/react'; import { default as React_2 } from 'react'; import { ReactContentSdkComponent } from '@sitecore-content-sdk/react'; import { ReactNode } from 'react'; @@ -152,6 +170,8 @@ import { resolveUrl } from '@sitecore-content-sdk/core/tools'; import { RetryStrategy } from '@sitecore-content-sdk/content/client'; import { revalidateTag } from 'next/cache'; import { RichTextField } from '@sitecore-content-sdk/react'; +import { RichTextFieldSchema } from '@sitecore-content-sdk/react'; +import { richTextFieldSchema } from '@sitecore-content-sdk/react'; import { RichTextProps as RichTextProps_2 } from '@sitecore-content-sdk/react'; import { RobotsQueryResult } from '@sitecore-content-sdk/content/site'; import { RobotsService } from '@sitecore-content-sdk/content/site'; @@ -177,6 +197,9 @@ import { SiteResolver } from '@sitecore-content-sdk/content/site'; import { StaticPath } from '@sitecore-content-sdk/content'; import { Text as Text_2 } from '@sitecore-content-sdk/react'; import { TextField } from '@sitecore-content-sdk/react'; +import { TextFieldSchema } from '@sitecore-content-sdk/react'; +import { textFieldSchema } from '@sitecore-content-sdk/react'; +import { useBoundProp } from '@sitecore-content-sdk/react'; import { useSitecore } from '@sitecore-content-sdk/react'; import { withAppPlaceholder } from '@sitecore-content-sdk/react'; import { withDatasourceCheck } from '@sitecore-content-sdk/react'; @@ -184,6 +207,7 @@ import { withEditorChromes } from '@sitecore-content-sdk/react'; import { withEmptyFieldEditingComponent } from '@sitecore-content-sdk/react'; import { withFieldMetadata } from '@sitecore-content-sdk/react'; import { withPlaceholder } from '@sitecore-content-sdk/react'; +import { withPropMeta } from '@sitecore-content-sdk/react'; import { withSitecore } from '@sitecore-content-sdk/react'; import { WriteImportMapArgs } from '@sitecore-content-sdk/content/node-tools'; @@ -218,6 +242,20 @@ export class AppRouterMultisiteProxy extends MultisiteProxy { protected shouldWarnWhenDisabled(_res: NextResponse): void; } +export { AtomActionDefinition } + +export { AtomActionHandler } + +export { AtomComponentDefinition } + +export { AtomsActionsMap } + +export { AtomsCatalogInput } + +export { AtomsComponentsMap } + +export { AtomsConfig } + // @public export class BotTrackingProxy extends ProxyBase { constructor(config: BotTrackingProxyConfig); @@ -394,6 +432,10 @@ export function createSitemapRouteHandler(options: RouteHandlerOptions): { export { DateField } +export { DateFieldSchema } + +export { dateFieldSchema } + // @public const debug_2: Record; export { debug_2 as debug } @@ -410,6 +452,12 @@ export { DefaultRetryStrategy } // @public export const defaultServerImportEntries: ImportEntry[]; +// @public +export const defineAtomsCatalog: typeof defineAtomsCatalog_2; + +// @public +export const defineAtomsRegistry: typeof defineAtomsRegistry_2; + // @public export const defineCliConfig: (cliConfig: SitecoreCliConfigInput) => SitecoreCliConfig; @@ -538,6 +586,10 @@ export { File_2 as File } export { FileField } +export { FileFieldSchema } + +export { fileFieldSchema } + export { Form } // @public @@ -624,6 +676,10 @@ export { Image_2 as Image } export { ImageField } +export { ImageFieldSchema } + +export { imageFieldSchema } + export { ImageFieldValue } export { ImageProps } @@ -664,6 +720,10 @@ export const Link: React_2.ForwardRefExoticComponent & Re export { LinkField } +export { LinkFieldSchema } + +export { linkFieldSchema } + export { LinkFieldValue } // Warning: (ae-forgotten-export) The symbol "supportedNextLinkProps" needs to be exported by the entry point api-surface.d.ts @@ -847,6 +907,8 @@ export type PreviewProxyConfig = { client: SitecoreClient; }; +export { PropMeta } + // @public export type ProxiesContext = Map; @@ -954,6 +1016,10 @@ export const RichText: { export { RichTextField } +export { RichTextFieldSchema } + +export { richTextFieldSchema } + // @public export type RichTextProps = RichTextProps_2 & { internalLinksSelector?: string; @@ -1122,6 +1188,12 @@ export { Text_2 as Text } export { TextField } +export { TextFieldSchema } + +export { textFieldSchema } + +export { useBoundProp } + // @public export function useComponentProps(componentUid: string | undefined): ComponentData | undefined; @@ -1139,6 +1211,8 @@ export { withFieldMetadata } export { withPlaceholder } +export { withPropMeta } + export { withSitecore } // @public diff --git a/packages/nextjs/atoms.d.ts b/packages/nextjs/atoms.d.ts new file mode 100644 index 0000000000..08ea70d18b --- /dev/null +++ b/packages/nextjs/atoms.d.ts @@ -0,0 +1 @@ +export * from './types/atoms/index'; diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 2e3818e389..531f078f86 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,187 +1,192 @@ -{ - "name": "@sitecore-content-sdk/nextjs", - "version": "2.1.0", - "main": "dist/cjs/index.js", - "module": "dist/esm/index.js", - "sideEffects": false, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "scripts": { - "build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig-esm.json", - "clean": "del-cli dist types", - "lint": "eslint \"./src/**/*.tsx\" \"./src/**/*.ts\"", - "test": "mocha --require ./test/setup.js \"./src/**/*.test.ts\" \"./src/**/*.test.tsx\" --exit", - "prepublishOnly": "npm run build", - "coverage": "nyc npm test", - "generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --outputFileStrategy Members --parametersFormat table --readme none --out ../../ref-docs/nextjs --entryPoints src/index.ts --entryPoints src/monitoring/index.ts --entryPoints src/editing/index.ts --entryPoints src/proxy/index.ts --entryPoints src/middleware/index.ts --entryPoints src/context/index.ts --entryPoints src/utils/index.ts --entryPoints src/site/index.ts --entryPoints src/client/index.ts --entryPoints src/tools/index.ts --entryPoints src/editing/codegen/index.ts --entryPoints src/route-handler/index.ts --githubPages false", - "api-extractor": "npm run build && api-extractor run --local --verbose", - "api-extractor:verify": "api-extractor run" - }, - "engines": { - "node": ">=24" - }, - "author": { - "name": "Sitecore Corporation", - "url": "https://doc.sitecore.com/xmc/en/developers/content-sdk/index.html" - }, - "license": "Apache-2.0", - "homepage": "https://doc.sitecore.com/xmc/en/developers/content-sdk/index.html", - "bugs": { - "url": "https://github.com/sitecore/content-sdk/issues" - }, - "devDependencies": { - "@sitecore-content-sdk/analytics-core": "^2.1.0", - "@sitecore-content-sdk/personalize": "^2.1.0", - "@stylistic/eslint-plugin": "^5.2.2", - "@testing-library/dom": "^10.4.0", - "@testing-library/react": "^16.3.0", - "@types/chai": "^5.2.2", - "@types/chai-string": "^1.4.5", - "@types/mocha": "^10.0.10", - "@types/node": "^24.10.4", - "@types/proxyquire": "^1.3.31", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@types/sinon": "^17.0.4", - "@types/sinon-chai": "^3.2.9", - "@typescript-eslint/eslint-plugin": "8.39.0", - "@typescript-eslint/parser": "8.39.0", - "chai": "^4.4.1", - "chai-string": "^1.6.0", - "chalk": "^4.1.2", - "cross-fetch": "^4.1.0", - "del-cli": "^6.0.0", - "eslint": "^9.32.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-import": "2.32.0", - "eslint-plugin-jsdoc": "52.0.3", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "7.37.5", - "eslint-plugin-react-hooks": "5.2.0", - "glob": "^11.0.2", - "jsdom": "^26.1.0", - "mocha": "^11.2.2", - "next": "^16.2.0", - "nock": "14.0.0-beta.7", - "nyc": "^17.1.0", - "prettier": "^2.8.0", - "proxyquire": "^2.1.3", - "react": "^19.2.1", - "react-dom": "^19.2.1", - "sinon": "^20.0.0", - "sinon-chai": "^3.7.0", - "ts-node": "^10.9.2", - "typescript": "~5.8.3" - }, - "peerDependencies": { - "@sitecore-content-sdk/analytics-core": "^2.1.0", - "@sitecore-content-sdk/events": "^2.1.0", - "@sitecore-content-sdk/personalize": "^2.1.0", - "next": "^16.2.0", - "react": "^19.2.1", - "react-dom": "^19.2.1", - "typescript": "^5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - }, - "dependencies": { - "@babel/parser": "^7.27.2", - "@sitecore-content-sdk/content": "^2.1.0", - "@sitecore-content-sdk/core": "^2.1.0", - "@sitecore-content-sdk/events": "^2.1.0", - "@sitecore-content-sdk/react": "^2.1.0", - "recast": "^0.23.11", - "regex-parser": "^2.3.1" - }, - "exports": { - ".": { - "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.js", - "types": "./types/index.d.ts" - }, - "./client": { - "import": "./dist/esm/client/index.js", - "require": "./dist/cjs/client/index.js", - "types": "./types/client/index.d.ts" - }, - "./codegen": { - "import": "./dist/esm/editing/codegen/index.js", - "require": "./dist/cjs/editing/codegen/index.js", - "types": "./types/editing/codegen/index.d.ts" - }, - "./component-props-loader": { - "import": "./dist/esm/component-props-loader/index.js", - "require": "./dist/cjs/component-props-loader/index.js", - "types": "./types/component-props-loader/index.d.ts" - }, - "./config": { - "import": "./dist/esm/config/index.js", - "require": "./dist/cjs/config/index.js", - "types": "./types/config/index.d.ts" - }, - "./config-cli": { - "import": "./dist/esm/config-cli/index.js", - "require": "./dist/cjs/config-cli/index.js", - "types": "./types/config-cli/index.d.ts" - }, - "./editing": { - "import": "./dist/esm/editing/index.js", - "require": "./dist/cjs/editing/index.js", - "types": "./types/editing/index.d.ts" - }, - "./proxy": { - "import": "./dist/esm/proxy/index.js", - "require": "./dist/cjs/proxy/index.js", - "types": "./types/proxy/index.d.ts" - }, - "./middleware": { - "import": "./dist/esm/middleware/index.js", - "require": "./dist/cjs/middleware/index.js", - "types": "./types/middleware/index.d.ts" - }, - "./monitoring": { - "import": "./dist/esm/monitoring/index.js", - "require": "./dist/cjs/monitoring/index.js", - "types": "./types/monitoring/index.d.ts" - }, - "./route-handler": { - "import": "./dist/esm/route-handler/index.js", - "require": "./dist/cjs/route-handler/index.js", - "types": "./types/route-handler/index.d.ts" - }, - "./search": { - "import": "./dist/esm/search/index.js", - "require": "./dist/cjs/search/index.js", - "types": "./types/search/index.d.ts" - }, - "./site": { - "import": "./dist/esm/site/index.js", - "require": "./dist/cjs/site/index.js", - "types": "./types/site/index.d.ts" - }, - "./tools": { - "import": "./dist/esm/tools/index.js", - "require": "./dist/cjs/tools/index.js", - "types": "./types/tools/index.d.ts" - }, - "./utils": { - "import": "./dist/esm/utils/index.js", - "require": "./dist/cjs/utils/index.js", - "types": "./types/utils/index.d.ts" - } - }, - "description": "", - "types": "types/index.d.ts", - "gitHead": "2f4820efddf4454eeee58ed1b2cc251969efdf5b", - "files": [ - "dist", - "types", - "/*.js", - "/*.d.ts" - ] -} +{ + "name": "@sitecore-content-sdk/nextjs", + "version": "2.1.0-beta.1", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "sideEffects": false, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig-esm.json", + "clean": "del-cli dist types", + "lint": "eslint \"./src/**/*.tsx\" \"./src/**/*.ts\"", + "test": "mocha --require ./test/setup.js \"./src/**/*.test.ts\" \"./src/**/*.test.tsx\" --exit", + "prepublishOnly": "npm run build", + "coverage": "nyc npm test", + "generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --outputFileStrategy Members --parametersFormat table --readme none --out ../../ref-docs/nextjs --entryPoints src/index.ts --entryPoints src/monitoring/index.ts --entryPoints src/editing/index.ts --entryPoints src/proxy/index.ts --entryPoints src/middleware/index.ts --entryPoints src/context/index.ts --entryPoints src/utils/index.ts --entryPoints src/site/index.ts --entryPoints src/client/index.ts --entryPoints src/tools/index.ts --entryPoints src/editing/codegen/index.ts --entryPoints src/route-handler/index.ts --githubPages false", + "api-extractor": "npm run build && api-extractor run --local --verbose", + "api-extractor:verify": "api-extractor run" + }, + "engines": { + "node": ">=24" + }, + "author": { + "name": "Sitecore Corporation", + "url": "https://doc.sitecore.com/xmc/en/developers/content-sdk/index.html" + }, + "license": "Apache-2.0", + "homepage": "https://doc.sitecore.com/xmc/en/developers/content-sdk/index.html", + "bugs": { + "url": "https://github.com/sitecore/content-sdk/issues" + }, + "devDependencies": { + "@sitecore-content-sdk/analytics-core": "2.1.0-beta.1", + "@sitecore-content-sdk/personalize": "2.1.0-beta.1", + "@stylistic/eslint-plugin": "^5.2.2", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.3.0", + "@types/chai": "^5.2.2", + "@types/chai-string": "^1.4.5", + "@types/mocha": "^10.0.10", + "@types/node": "^24.10.4", + "@types/proxyquire": "^1.3.31", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@types/sinon": "^17.0.4", + "@types/sinon-chai": "^3.2.9", + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "chai": "^4.4.1", + "chai-string": "^1.6.0", + "chalk": "^4.1.2", + "cross-fetch": "^4.1.0", + "del-cli": "^6.0.0", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "2.32.0", + "eslint-plugin-jsdoc": "52.0.3", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "5.2.0", + "glob": "^11.0.2", + "jsdom": "^26.1.0", + "mocha": "^11.2.2", + "next": "^16.2.0", + "nock": "14.0.0-beta.7", + "nyc": "^17.1.0", + "prettier": "^2.8.0", + "proxyquire": "^2.1.3", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "sinon": "^20.0.0", + "sinon-chai": "^3.7.0", + "ts-node": "^10.9.2", + "typescript": "~5.8.3" + }, + "peerDependencies": { + "@sitecore-content-sdk/analytics-core": "2.1.0-beta.1", + "@sitecore-content-sdk/events": "2.1.0-beta.1", + "@sitecore-content-sdk/personalize": "2.1.0-beta.1", + "next": "^16.2.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "typescript": "^5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "dependencies": { + "@babel/parser": "^7.27.2", + "@sitecore-content-sdk/content": "2.1.0-beta.1", + "@sitecore-content-sdk/core": "2.1.0-beta.1", + "@sitecore-content-sdk/events": "2.1.0-beta.1", + "@sitecore-content-sdk/react": "2.1.0-beta.1", + "recast": "^0.23.11", + "regex-parser": "^2.3.1" + }, + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "types": "./types/index.d.ts" + }, + "./atoms": { + "import": "./dist/esm/atoms/index.js", + "require": "./dist/cjs/atoms/index.js", + "types": "./types/atoms/index.d.ts" + }, + "./client": { + "import": "./dist/esm/client/index.js", + "require": "./dist/cjs/client/index.js", + "types": "./types/client/index.d.ts" + }, + "./codegen": { + "import": "./dist/esm/editing/codegen/index.js", + "require": "./dist/cjs/editing/codegen/index.js", + "types": "./types/editing/codegen/index.d.ts" + }, + "./component-props-loader": { + "import": "./dist/esm/component-props-loader/index.js", + "require": "./dist/cjs/component-props-loader/index.js", + "types": "./types/component-props-loader/index.d.ts" + }, + "./config": { + "import": "./dist/esm/config/index.js", + "require": "./dist/cjs/config/index.js", + "types": "./types/config/index.d.ts" + }, + "./config-cli": { + "import": "./dist/esm/config-cli/index.js", + "require": "./dist/cjs/config-cli/index.js", + "types": "./types/config-cli/index.d.ts" + }, + "./editing": { + "import": "./dist/esm/editing/index.js", + "require": "./dist/cjs/editing/index.js", + "types": "./types/editing/index.d.ts" + }, + "./proxy": { + "import": "./dist/esm/proxy/index.js", + "require": "./dist/cjs/proxy/index.js", + "types": "./types/proxy/index.d.ts" + }, + "./middleware": { + "import": "./dist/esm/middleware/index.js", + "require": "./dist/cjs/middleware/index.js", + "types": "./types/middleware/index.d.ts" + }, + "./monitoring": { + "import": "./dist/esm/monitoring/index.js", + "require": "./dist/cjs/monitoring/index.js", + "types": "./types/monitoring/index.d.ts" + }, + "./route-handler": { + "import": "./dist/esm/route-handler/index.js", + "require": "./dist/cjs/route-handler/index.js", + "types": "./types/route-handler/index.d.ts" + }, + "./search": { + "import": "./dist/esm/search/index.js", + "require": "./dist/cjs/search/index.js", + "types": "./types/search/index.d.ts" + }, + "./site": { + "import": "./dist/esm/site/index.js", + "require": "./dist/cjs/site/index.js", + "types": "./types/site/index.d.ts" + }, + "./tools": { + "import": "./dist/esm/tools/index.js", + "require": "./dist/cjs/tools/index.js", + "types": "./types/tools/index.d.ts" + }, + "./utils": { + "import": "./dist/esm/utils/index.js", + "require": "./dist/cjs/utils/index.js", + "types": "./types/utils/index.d.ts" + } + }, + "description": "", + "types": "types/index.d.ts", + "gitHead": "2f4820efddf4454eeee58ed1b2cc251969efdf5b", + "files": [ + "dist", + "types", + "/*.js", + "/*.d.ts" + ] +} diff --git a/packages/nextjs/src/atoms/index.ts b/packages/nextjs/src/atoms/index.ts new file mode 100644 index 0000000000..dff78c75e5 --- /dev/null +++ b/packages/nextjs/src/atoms/index.ts @@ -0,0 +1,27 @@ +export { + useBoundProp, + withPropMeta, + type AtomComponentDefinition, + type AtomActionDefinition, + type AtomsCatalogInput, + type AtomsComponentsMap, + type AtomActionHandler, + type AtomsActionsMap, + type AtomsConfig, + type PropMeta, + textFieldSchema, + richTextFieldSchema, + dateFieldSchema, + linkFieldSchema, + imageFieldSchema, + fileFieldSchema, + type TextFieldSchema, + type RichTextFieldSchema, + type DateFieldSchema, + type LinkFieldSchema, + type ImageFieldSchema, + type FileFieldSchema, +} from '@sitecore-content-sdk/react'; + +export { defineAtomsCatalog, defineAtomsRegistry } from './re-exports'; + diff --git a/packages/nextjs/src/atoms/re-exports.ts b/packages/nextjs/src/atoms/re-exports.ts new file mode 100644 index 0000000000..fb0dfc1057 --- /dev/null +++ b/packages/nextjs/src/atoms/re-exports.ts @@ -0,0 +1,75 @@ +import { + defineAtomsCatalog as defineAtomsCatalogReact, + defineAtomsRegistry as defineAtomsRegistryReact, +} from '@sitecore-content-sdk/react'; + +/** + * Define an atoms catalog from component and action definitions. + * + * Pass component/action definitions exactly as json-render expects them. + * The returned catalog carries full type information so `defineAtomsRegistry` + * can infer props per component. + * @param {T} input - Catalog input with `components` and optionally `actions` + * @returns A typed json-render Catalog + * @example + * ```ts + * import { z } from 'zod'; + * import { defineAtomsCatalog } from '@sitecore-content-sdk/nextjs'; + * + * const catalog = defineAtomsCatalog({ + * components: { + * Button: { + * props: z.object({ label: z.string(), variant: z.enum(['primary', 'secondary']) }), + * description: 'A clickable button', + * slots: ['default'], + * }, + * Card: { + * props: z.object({ title: z.string() }), + * description: 'A content card', + * slots: ['default'], + * }, + * }, + * actions: { + * submit: { + * params: z.object({ formId: z.string() }), + * description: 'Submit a form', + * }, + * }, + * }); + * ``` + * @public + */ +export const defineAtomsCatalog = defineAtomsCatalogReact; + +/** + * Define an atoms registry that maps catalog definitions to Nextjs implementations. + * + * Each component receives `{ props, children, emit, on, bindings, loading }` + * @param catalog - The catalog created by defineAtomsCatalog + * @param options - Component and action implementations + * @returns Registry result with component registry and action handlers + * @example + * + * ```tsx + * import { defineAtomsRegistry } from '@sitecore-content-sdk/nextjs'; + * + * const { registry, handlers, executeAction } = defineAtomsRegistry(catalog, { + * components: { + * Button: ({ props, children, emit }) => ( + * + * ), + * Card: ({ props, children }) => ( + *

{props.title}

{children}
+ * ), + * }, + * actions: { + * submit: async (params) => { + * await fetch('/api/submit', { method: 'POST', body: JSON.stringify(params) }); + * }, + * }, + * }); + * ``` + * @public + */ +export const defineAtomsRegistry: typeof defineAtomsRegistryReact = defineAtomsRegistryReact; + diff --git a/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.test.tsx b/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.test.tsx index 3ef371b322..55f514b86b 100644 --- a/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.test.tsx +++ b/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.test.tsx @@ -18,6 +18,7 @@ use(sinonChai); describe('', () => { let DesignLibraryStub: sinon.SinonStub; + let DesignLibraryLowCodeComponentStub: sinon.SinonStub; let DesignLibraryServerStub: sinon.SinonStub; let DesignLibraryApp: any; const sandbox = sinon.createSandbox(); @@ -27,6 +28,10 @@ describe('', () => { .stub() .returns(
Client Component Rendered
); + DesignLibraryLowCodeComponentStub = sandbox + .stub() + .returns(
Low Code Component Rendered
); + DesignLibraryServerStub = sandbox .stub() .resolves(
Server Component Rendered
); @@ -35,6 +40,7 @@ describe('', () => { const module = proxyquire('./DesignLibraryApp', { '@sitecore-content-sdk/react': { DesignLibrary: DesignLibraryStub, + DesignLibraryLowCodeComponent: DesignLibraryLowCodeComponentStub, }, './DesignLibraryServer': { DesignLibraryServer: DesignLibraryServerStub, @@ -51,12 +57,17 @@ describe('', () => { const modeLibrary: PageMode = { name: DesignLibraryMode.Normal, isDesignLibrary: true, - designLibrary: { isVariantGeneration: false }, + designLibrary: { isVariantGeneration: false, isLowCode: false }, isNormal: false, isPreview: false, isEditing: true, }; + const modeLibraryLowCode: PageMode = { + ...modeLibrary, + designLibrary: { ...modeLibrary.designLibrary, isLowCode: true }, + }; + const ClientComponent: React.FC<{ [prop: string]: unknown }> = () =>
Client Component
; const ServerComponent: React.FC<{ [prop: string]: unknown }> = () =>
Server Component
; @@ -115,6 +126,26 @@ describe('', () => { render(awaitedDesignLibraryServer); expect(DesignLibraryStub).to.have.been.calledOnce; + expect(DesignLibraryLowCodeComponentStub).to.not.have.been.called; + expect(DesignLibraryServerStub).to.not.have.been.called; + }); + + it('should render DesignLibraryLowCodeComponent when isLowCode is true', () => { + const layoutData: LayoutServiceData = getTestLayoutData().layoutData; + const page = getPage(layoutData, modeLibraryLowCode); + const componentMap = createComponentMap('ContentBlock', 'client'); + + const awaitedDesignLibraryServer = DesignLibraryApp({ + page, + rendering: layoutData.sitecore.route as any, + componentMap, + loadServerImportMap: sinon.stub(), + }); + + render(awaitedDesignLibraryServer); + + expect(DesignLibraryLowCodeComponentStub).to.have.been.calledOnce; + expect(DesignLibraryStub).to.not.have.been.called; expect(DesignLibraryServerStub).to.not.have.been.called; }); diff --git a/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.tsx b/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.tsx index 445befc264..91d3b0c2fb 100644 --- a/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.tsx +++ b/packages/nextjs/src/components/DesignLibrary/DesignLibraryApp.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { EDITING_COMPONENT_PLACEHOLDER } from '@sitecore-content-sdk/content/layout'; import { DesingLibraryAppProps } from './models'; import { DesignLibraryServer } from './DesignLibraryServer'; -import { DesignLibrary } from '@sitecore-content-sdk/react'; +import { DesignLibrary, DesignLibraryLowCodeComponent } from '@sitecore-content-sdk/react'; /** * Design Library component intended to be used by the NextJs app router application @@ -11,6 +11,7 @@ import { DesignLibrary } from '@sitecore-content-sdk/react'; * delegates to the appropriate rendering implementation: * - Client components are rendered using the `DesignLibrary` component * - Server components are rendered using the `DesignLibraryServer` component + * - Low code components are rendered using the `DesignLibraryLowCodeComponent` component * @param {DesingLibraryAppProps} props - The properties for the Design Library App. * @public */ @@ -22,13 +23,16 @@ export const DesignLibraryApp = ({ const { route } = page.layout.sitecore; if (!route) return null; + const isLowCode = page.mode.designLibrary.isLowCode; const rendering = route?.placeholders[EDITING_COMPONENT_PLACEHOLDER]?.[0]; const component = componentMap.get(rendering?.componentName || ''); const isClient = component && component.componentType === 'client'; return ( <> - {isClient ? ( + {isLowCode ? ( + + ) : isClient ? ( ) : ( React_2.JSX.Element; @@ -65,6 +75,43 @@ export const AppPlaceholder: (props: AppPlaceholderProps) => React_2.JSX.Element // @public export type AppPlaceholderProps = Omit & Required>; +// Warning: (ae-forgotten-export) The symbol "BaseAction" needs to be exported by the entry point api-surface.d.ts +// +// @public +export type AtomActionDefinition = BaseAction; + +// @public +export type AtomActionHandler = (params: Record) => Promise | void; + +// Warning: (ae-forgotten-export) The symbol "BaseComponent" needs to be exported by the entry point api-surface.d.ts +// +// @public +export type AtomComponentDefinition = BaseComponent & SitecoreComponentMeta; + +// @public +export type AtomsActionsMap = Record; + +// Warning: (ae-forgotten-export) The symbol "BaseCatalog" needs to be exported by the entry point api-surface.d.ts +// +// @public +export type AtomsCatalogInput = BaseCatalog & { + version?: string; + components: Record; + actions: Record; +}; + +// Warning: (ae-forgotten-export) The symbol "AtomsComponentRenderer" needs to be exported by the entry point api-surface.d.ts +// +// @public +export type AtomsComponentsMap = Record; + +// @public +export interface AtomsConfig { + catalog: Catalog; + navigate?: (path: string) => void; + registry: DefineRegistryResult; +} + // @public export class BYOCComponent extends React_2.Component { constructor(props: BYOCComponentProps); @@ -144,6 +191,14 @@ export interface DateFieldProps extends EditableFieldProps { tag?: string; } +// @public +export type DateFieldSchema = z.infer>; + +// @public +export const dateFieldSchema: (extra?: z.ZodRawShape) => z.ZodObject<{ + value: z.ZodOptional; +}, z.core.$strip>; + export { debug_2 as debug } // @public @@ -160,6 +215,36 @@ export const DefaultEmptyFieldEditingComponentText: React_2.FC<{ export { DefaultRetryStrategy } +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point api-surface.d.ts +// +// @public +export function defineAtomsCatalog(input: Exact): Catalog< { +spec: SchemaType<"object", { +root: SchemaType<"string", unknown>; +elements: SchemaType<"record", SchemaType<"object", { +type: SchemaType<"ref", string>; +props: SchemaType<"propsOf", string>; +children: SchemaType<"array", SchemaType<"string", unknown>>; +visible: SchemaType<"any", unknown>; +}>>; +}>; +catalog: SchemaType<"object", { +components: SchemaType<"map", { +props: SchemaType<"zod", unknown>; +slots: SchemaType<"array", SchemaType<"string", unknown>>; +description: SchemaType<"string", unknown>; +example: SchemaType<"any", unknown>; +}>; +actions: SchemaType<"map", { +params: SchemaType<"zod", unknown>; +description: SchemaType<"string", unknown>; +}>; +}>; +}, Exact>; + +// @public +export const defineAtomsRegistry: typeof defineRegistry; + // @public export const DesignLibrary: () => React_2.JSX.Element | null; @@ -183,6 +268,9 @@ export class DesignLibraryErrorBoundary extends React_2.Component React_2.JSX.Element; + export { DictionaryPhrases } export { DictionaryService } @@ -269,6 +357,18 @@ export interface FileField { value: FileFieldValue; } +// @public +export type FileFieldSchema = z.infer>; + +// @public +export const fileFieldSchema: (extra?: z.ZodRawShape) => z.ZodObject<{ + value: z.ZodObject<{ + src: z.ZodOptional; + title: z.ZodOptional; + displayName: z.ZodOptional; + }, z.core.$loose>; +}, z.core.$strip>; + // Warning: (ae-forgotten-export) The symbol "FormProps" needs to be exported by the entry point api-surface.d.ts // // @public @@ -298,6 +398,20 @@ export interface ImageField { value?: ImageFieldValue; } +// @public +export type ImageFieldSchema = z.infer>; + +// @public +export const imageFieldSchema: (extra?: z.ZodRawShape) => z.ZodObject<{ + value: z.ZodOptional; + alt: z.ZodOptional; + width: z.ZodOptional>; + height: z.ZodOptional>; + class: z.ZodOptional; + }, z.core.$loose>>; +}, z.core.$strip>; + // @public export interface ImageFieldValue { // (undocumented) @@ -360,6 +474,24 @@ export interface LinkField { value: LinkFieldValue; } +// @public +export type LinkFieldSchema = z.infer>; + +// @public +export const linkFieldSchema: (extra?: z.ZodRawShape) => z.ZodObject<{ + value: z.ZodObject<{ + href: z.ZodOptional; + className: z.ZodOptional; + class: z.ZodOptional; + title: z.ZodOptional; + target: z.ZodOptional; + text: z.ZodOptional; + anchor: z.ZodOptional; + querystring: z.ZodOptional; + linktype: z.ZodOptional; + }, z.core.$loose>; +}, z.core.$strip>; + // @public export interface LinkFieldValue { // (undocumented) @@ -444,6 +576,11 @@ interface PlaceholderProps { export { PlaceholderProps as PlaceholderComponentProps } export { PlaceholderProps } +// @public +export type PropMeta = { + control?: string; +}; + // @public export type ReactContentSdkComponent = (ComponentType | ReactModule) & { componentType?: 'server' | 'client' | 'universal'; @@ -472,6 +609,14 @@ export interface RichTextField extends FieldMetadata { value?: string; } +// @public +export type RichTextFieldSchema = z.infer>; + +// @public +export const richTextFieldSchema: (extra?: z.ZodRawShape) => z.ZodObject<{ + value: z.ZodOptional; +}, z.core.$strip>; + // @public export interface RichTextProps extends EditableFieldProps { // (undocumented) @@ -498,6 +643,7 @@ export const SitecoreProviderReactContext: React_2.Context Promise; @@ -521,6 +667,17 @@ export interface TextField extends FieldMetadata { value?: string | number; } +// @public +export type TextFieldSchema = z.infer>; + +// @public +export const textFieldSchema: (extra?: z.ZodRawShape) => z.ZodObject<{ + value: z.ZodOptional>; +}, z.core.$strip>; + +// @public +export const useBoundProp: typeof useBoundProp_2; + // @public export const useInfiniteSearch: (options: UseInfiniteSearchOptions) => UseInfiniteSearchState; @@ -613,6 +770,9 @@ export function withFieldMetadata(Component: ComponentType) => (props: W) => React_2.JSX.Element; +// @public +export function withPropMeta(schema: T, meta: PropMeta): T; + // Warning: (ae-forgotten-export) The symbol "WithSitecoreHocProps" needs to be exported by the entry point api-surface.d.ts // // @public @deprecated (undocumented) @@ -621,7 +781,7 @@ export function withSitecore(options?: UseSitecoreOptions): { + it('returns components and actions arrays', () => { + const catalog = defineAtomsCatalog({ + components: { + Text: { props: z.object({ content: z.string() }), description: 'A text node' }, + }, + actions: {}, + }); + + const result = serializeCatalog(catalog); + + expect(result).to.have.property('components').that.is.an('array'); + expect(result).to.have.property('actions').that.is.an('array'); + }); + + it('omits version when not set on the catalog', () => { + const catalog = defineAtomsCatalog({ + components: { + Text: { props: z.object({ content: z.string() }), description: 'A text node' }, + }, + actions: {}, + }); + + const result = serializeCatalog(catalog); + + expect(result).to.not.have.property('version'); + }); + + it('includes catalog-level version when set', () => { + const catalog = defineAtomsCatalog({ + version: '2.0.0', + components: { + Text: { props: z.object({ content: z.string() }), description: 'A text node' }, + }, + actions: {}, + }); + const result = serializeCatalog(catalog); + expect(result).to.have.property('version', '2.0.0'); + }); + + it('serializes component with full schema', () => { + const catalog = defineAtomsCatalog({ + components: { + Button: { + version: '0.3.0', + props: z.object({ label: z.string() }), + description: 'A button', + example: { label: 'Hello world' }, + allowedChildren: ['Button', 'Text'], + allowedParents: ['Column', 'Row'], + slots: ['header', 'body', 'footer'], + }, + }, + actions: {}, + }); + + const [comp] = serializeCatalog(catalog).components; + + expect(comp.name).to.equal('Button'); + expect(comp.description).to.equal('A button'); + expect(comp.propsSchema).to.deep.equal({ + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { label: { type: 'string' } }, + required: ['label'], + additionalProperties: false, + }); + expect(comp.slots).to.deep.equal(['header', 'body', 'footer']); + expect(comp.allowedChildren).to.deep.equal(['Button', 'Text']); + expect(comp.allowedParents).to.deep.equal(['Column', 'Row']); + expect(comp.example).to.deep.equal({ label: 'Hello world' }); + expect(comp.version).to.equal('0.3.0'); + }); + + it('serializes multiple components in catalog key order', () => { + const catalog = defineAtomsCatalog({ + components: { + Alpha: { props: z.object({}), description: 'Alpha' }, + Beta: { props: z.object({}), description: 'Beta' }, + Gamma: { props: z.object({}), description: 'Gamma' }, + }, + actions: {}, + }); + + const names = serializeCatalog(catalog).components.map((c) => c.name); + + expect(names).to.deep.equal(['Alpha', 'Beta', 'Gamma']); + }); + + it('returns an empty actions array when catalog has no actions', () => { + const catalog = defineAtomsCatalog({ + components: { + Text: { props: z.object({ content: z.string() }), description: 'Text' }, + }, + actions: {}, + }); + expect(serializeCatalog(catalog).actions).to.deep.equal([]); + }); + + it('serializes action name and description', () => { + const catalog = defineAtomsCatalog({ + components: { + Button: { props: z.object({ label: z.string() }), description: 'A button' }, + }, + actions: { + submit: { params: z.object({ formId: z.string() }), description: 'Submit the form' }, + }, + }); + const [action] = serializeCatalog(catalog).actions; + expect(action.name).to.equal('submit'); + expect(action.description).to.equal('Submit the form'); + }); + + it('converts action params to JSON Schema', () => { + const catalog = defineAtomsCatalog({ + components: { + Button: { props: z.object({ label: z.string() }), description: 'A button' }, + }, + actions: { + navigate: { params: z.object({ path: z.string() }), description: 'Navigate' }, + }, + }); + const [action] = serializeCatalog(catalog).actions; + expect(action.paramsSchema).to.deep.equal({ + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { path: { type: 'string' } }, + required: ['path'], + additionalProperties: false, + }); + }); + + it('converts action without params to JSON Schema', () => { + const catalog = defineAtomsCatalog({ + components: {}, + actions: { + navigate: { description: 'Navigate' }, + }, + }); + + const [action] = serializeCatalog(catalog).actions; + + expect(action).to.not.have.property('paramsSchema'); + }); + + it('serializes multiple actions in catalog key order', () => { + const catalog = defineAtomsCatalog({ + components: { + Button: { props: z.object({ label: z.string() }), description: 'Button' }, + }, + actions: { + open: { params: z.object({ id: z.string() }), description: 'Open' }, + close: { params: z.object({ id: z.string() }), description: 'Close' }, + }, + }); + const names = serializeCatalog(catalog).actions.map((a) => a.name); + expect(names).to.deep.equal(['open', 'close']); + }); +}); + diff --git a/packages/react/src/atoms/catalog-serializer.ts b/packages/react/src/atoms/catalog-serializer.ts new file mode 100644 index 0000000000..2eb6f77252 --- /dev/null +++ b/packages/react/src/atoms/catalog-serializer.ts @@ -0,0 +1,57 @@ +import type { Catalog } from '@json-render/core'; +import { AtomsCatalogInput } from './types'; +import { + AtomCatalogActionEntry, + AtomCatalogComponentEntry, + SerializedCatalog, +} from '@sitecore-content-sdk/content/atoms'; + +/** + * Serialize a json-render Catalog into the payload shape expected by Design Studio. + * @param { Catalog } catalog - The json-render Catalog to serialize + * @returns Serialized catalog for the Design Library event + * @internal + */ +export function serializeCatalog(catalog: Catalog): SerializedCatalog { + const { version, components, actions } = catalog.data; + + const serializedComponents: AtomCatalogComponentEntry[] = Object.entries(components).map( + ([name, component]) => { + const serializedComponent: AtomCatalogComponentEntry = { + name, + propsSchema: component.props.toJSONSchema(), + description: component.description, + slots: component.slots ?? ['default'], + allowedChildren: component.allowedChildren, + allowedParents: component.allowedParents, + example: component.example, + }; + + if (component.version) serializedComponent.version = component.version; + + return serializedComponent; + } + ); + + const serializedActions: AtomCatalogActionEntry[] = Object.entries(actions).map( + ([name, action]) => { + const serializedAction: AtomCatalogActionEntry = { + name, + description: action.description, + }; + + if (action.params) serializedAction.paramsSchema = action.params.toJSONSchema(); + + return serializedAction; + } + ); + + const serializedCatalog: SerializedCatalog = { + components: serializedComponents, + actions: serializedActions, + }; + + if (version) serializedCatalog.version = version; + + return serializedCatalog; +} diff --git a/packages/react/src/atoms/create-ncc.test.tsx b/packages/react/src/atoms/create-ncc.test.tsx new file mode 100644 index 0000000000..6b4d3283aa --- /dev/null +++ b/packages/react/src/atoms/create-ncc.test.tsx @@ -0,0 +1,36 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import { expect } from 'chai'; +import type { Document } from '@sitecore-content-sdk/content/atoms'; +import type { DefineRegistryResult } from '@json-render/react'; +import { createNCC } from '.'; + +describe('create-ncc', () => { + const mockDoc: Document = { + name: 'TestComponent', + root: 'root-el', + elements: { + 'root-el': { + type: 'Card', + props: { title: 'Hello' }, + children: [], + }, + }, + state: { count: 0 }, + }; + + const mockRegistry: DefineRegistryResult = { + registry: {} as any, + handlers: () => ({}), + executeAction: async () => {}, + }; + + it('returns an FC with the document name as displayName', () => { + const View = createNCC(mockDoc, mockRegistry); + expect(View.displayName).to.equal('TestComponent'); + }); + + it('returns a functional component', () => { + const View = createNCC(mockDoc, mockRegistry); + expect(typeof View).to.equal('function'); + }); +}); diff --git a/packages/react/src/atoms/create-ncc.tsx b/packages/react/src/atoms/create-ncc.tsx new file mode 100644 index 0000000000..b2c93abb23 --- /dev/null +++ b/packages/react/src/atoms/create-ncc.tsx @@ -0,0 +1,57 @@ +'use client'; +import React, { type FC, useState } from 'react'; +import { Renderer, StateProvider, ActionProvider, VisibilityProvider } from '@json-render/react'; +import type { DefineRegistryResult } from '@json-render/react'; +import { createStateStore } from '@json-render/react'; +import type { StateModel } from '@json-render/core'; +import { Document } from '@sitecore-content-sdk/content/atoms'; +import { useSitecore } from '../components/SitecoreProvider'; + +/** + * Creates a React functional component that renders the given Component Layout document + * using json-render's Renderer. The document's flat element map is passed as a spec. + * @param {Document} doc - Component Layout document (flat spec format) + * @param {DefineRegistryResult} registryResult - The registry from defineAtomsRegistry + * @returns {FC>} FC that accepts runtime props merged into spec state + * @internal + */ +export function createNCC( + doc: Document, + registryResult: DefineRegistryResult +): FC> { + const { registry, handlers } = registryResult; + + const initialState: StateModel = { + ...(doc.state ?? {}), + }; + + const Generated: FC> = (runtimeProps) => { + const { atomsConfig } = useSitecore(); + const [store] = useState(() => + createStateStore({ + ...initialState, + ...runtimeProps, + }) + ); + const [resolvedHandlers] = useState(() => + handlers( + () => (updater) => store.update(updater(store.getSnapshot())), + () => store.getSnapshot() + ) + ); + + return ( + + + + + + + + ); + }; + + Generated.displayName = doc.name; + + return Generated; +} diff --git a/packages/react/src/atoms/define-atoms-catalog.ts b/packages/react/src/atoms/define-atoms-catalog.ts new file mode 100644 index 0000000000..92d7e71695 --- /dev/null +++ b/packages/react/src/atoms/define-atoms-catalog.ts @@ -0,0 +1,45 @@ +import { defineCatalog } from '@json-render/core'; +import { schema } from '@json-render/react'; +import type { AtomsCatalogInput, Exact } from './types'; + +/** + * Define an atoms catalog from component and action definitions. + * + * Pass component/action definitions exactly as json-render expects them. + * The returned catalog carries full type information so `defineAtomsRegistry` + * can infer props per component. + * @param {T} input - Catalog input with `components` and optionally `actions` + * @returns A typed json-render Catalog + * @example + * ```ts + * import { z } from 'zod'; + * import { defineAtomsCatalog } from '@sitecore-content-sdk/react'; + * + * const catalog = defineAtomsCatalog({ + * components: { + * Button: { + * props: z.object({ label: z.string(), variant: z.enum(['primary', 'secondary']) }), + * description: 'A clickable button', + * slots: ['default'], + * }, + * Card: { + * props: z.object({ title: z.string() }), + * description: 'A content card', + * slots: ['default'], + * }, + * }, + * actions: { + * submit: { + * params: z.object({ formId: z.string() }), + * description: 'Submit a form', + * }, + * }, + * }); + * ``` + * @public + */ +export function defineAtomsCatalog( + input: Exact +) { + return defineCatalog(schema, input); +} diff --git a/packages/react/src/atoms/define-atoms-registry.ts b/packages/react/src/atoms/define-atoms-registry.ts new file mode 100644 index 0000000000..317a38631a --- /dev/null +++ b/packages/react/src/atoms/define-atoms-registry.ts @@ -0,0 +1,34 @@ +'use client'; +import { defineRegistry } from '@json-render/react'; + +/** + * Define an atoms registry that maps catalog definitions to React implementations. + * + * Each component receives `{ props, children, emit, on, bindings, loading }` + * @param catalog - The catalog created by defineAtomsCatalog + * @param options - Component and action implementations + * @returns Registry result with component registry and action handlers + * @example + * + * ```tsx + * import { defineAtomsRegistry } from '@sitecore-content-sdk/react'; + * + * const { registry, handlers, executeAction } = defineAtomsRegistry(catalog, { + * components: { + * Button: ({ props, children, emit }) => ( + * + * ), + * Card: ({ props, children }) => ( + *

{props.title}

{children}
+ * ), + * }, + * actions: { + * submit: async (params) => { + * await fetch('/api/submit', { method: 'POST', body: JSON.stringify(params) }); + * }, + * }, + * }); + * ``` + * @public + */ +export const defineAtomsRegistry = defineRegistry; diff --git a/packages/react/src/atoms/field-schemas.test.ts b/packages/react/src/atoms/field-schemas.test.ts new file mode 100644 index 0000000000..2609e4fe94 --- /dev/null +++ b/packages/react/src/atoms/field-schemas.test.ts @@ -0,0 +1,202 @@ +import { expect } from 'chai'; +import { z } from 'zod'; +import { + textFieldSchema, + richTextFieldSchema, + dateFieldSchema, + linkFieldSchema, + imageFieldSchema, + fileFieldSchema, + type LinkFieldSchema, + type ImageFieldSchema, + type FileFieldSchema, +} from './field-schemas'; + +describe('field-schemas', () => { + const getFieldMeta = ( + schemaOrJsonSchema: z.ZodType | Record + ): Record | undefined => { + if (typeof schemaOrJsonSchema !== 'object' || schemaOrJsonSchema === null) { + return undefined; + } + if ('_zod' in schemaOrJsonSchema) { + const obj = schemaOrJsonSchema as Record & { + meta?: () => Record; + }; + const m = typeof obj.meta === 'function' ? obj.meta() : undefined; + return m?.meta as Record | undefined; + } + return (schemaOrJsonSchema as Record).meta as + | Record + | undefined; + }; + + const factories = [ + { name: 'textFieldSchema', factory: textFieldSchema, control: 'Single-Line Text' }, + { name: 'richTextFieldSchema', factory: richTextFieldSchema, control: 'Rich Text' }, + { name: 'dateFieldSchema', factory: dateFieldSchema, control: 'Date' }, + { name: 'linkFieldSchema', factory: linkFieldSchema, control: 'Link' }, + { name: 'imageFieldSchema', factory: imageFieldSchema, control: 'Image' }, + { name: 'fileFieldSchema', factory: fileFieldSchema, control: 'File' }, + ]; + + factories.forEach(({ name, factory, control }) => { + describe(name, () => { + it('returns a ZodObject', () => { + expect(factory()).to.be.instanceOf(z.ZodObject); + }); + + it(`has control hint "${control}"`, () => { + const meta = getFieldMeta(factory()); + expect(meta).to.deep.equal({ control }); + }); + + it('preserves control hint when extra shape is provided', () => { + const meta = getFieldMeta(factory({ extra: z.string() })); + expect(meta).to.deep.equal({ control }); + }); + + it('includes extra shape in the schema', () => { + const schema = factory({ extra: z.string() }); + expect(schema.shape).to.have.property('extra'); + }); + }); + }); + + describe('textFieldSchema', () => { + it('parses a valid text field', () => { + expect(() => textFieldSchema().parse({ value: 'hello' })).not.to.throw(); + }); + + it('parses a numeric value', () => { + expect(() => textFieldSchema().parse({ value: 42 })).not.to.throw(); + }); + + it('parses with no value', () => { + expect(() => textFieldSchema().parse({})).not.to.throw(); + }); + }); + + describe('richTextFieldSchema', () => { + it('parses a valid rich text field', () => { + expect(() => richTextFieldSchema().parse({ value: '

hello

' })).not.to.throw(); + }); + + it('parses with no value', () => { + expect(() => richTextFieldSchema().parse({})).not.to.throw(); + }); + }); + + describe('dateFieldSchema', () => { + it('parses a valid date field', () => { + expect(() => dateFieldSchema().parse({ value: '20231025T120000Z' })).not.to.throw(); + }); + + it('parses with no value', () => { + expect(() => dateFieldSchema().parse({})).not.to.throw(); + }); + }); + + describe('linkFieldSchema', () => { + it('parses a valid link field', () => { + expect(() => + linkFieldSchema().parse({ value: { href: '/about', text: 'About', target: '_blank' } }) + ).not.to.throw(); + }); + + it('parses an empty value object', () => { + expect(() => linkFieldSchema().parse({ value: {} })).not.to.throw(); + }); + + it('passes through unknown attributes on the value', () => { + const result = linkFieldSchema().parse({ + value: { href: '/', 'data-custom': 'foo' }, + }) as LinkFieldSchema & { value: Record }; + expect(result.value['data-custom']).to.equal('foo'); + }); + }); + + describe('imageFieldSchema', () => { + it('parses a valid image field', () => { + expect(() => + imageFieldSchema().parse({ + value: { src: '/img.png', alt: 'An image', width: 100, height: 200 }, + }) + ).not.to.throw(); + }); + + it('parses with no value', () => { + expect(() => imageFieldSchema().parse({})).not.to.throw(); + }); + + it('passes through unknown attributes on the value', () => { + const result = imageFieldSchema().parse({ + value: { src: '/img.png', 'data-id': '1' }, + }) as ImageFieldSchema & { value: Record }; + expect(result.value['data-id']).to.equal('1'); + }); + }); + + describe('fileFieldSchema', () => { + it('parses a valid file field', () => { + expect(() => + fileFieldSchema().parse({ value: { src: '/doc.pdf', title: 'My Doc', displayName: 'doc' } }) + ).not.to.throw(); + }); + + it('parses an empty value object', () => { + expect(() => fileFieldSchema().parse({ value: {} })).not.to.throw(); + }); + + it('passes through unknown properties on the value', () => { + const result = fileFieldSchema().parse({ + value: { src: '/doc.pdf', customProp: 'bar' }, + }) as FileFieldSchema & { value: Record }; + expect(result.value.customProp).to.equal('bar'); + }); + }); + + describe('defineAtomsCatalog integration', () => { + it('accepts textFieldSchema as a prop in a component definition', () => { + const props = z.object({ title: textFieldSchema() }); + const parsed = props.safeParse({ title: { value: 'Hello' } }); + expect(parsed.success).to.equal(true); + }); + + it('accepts richTextFieldSchema as a prop', () => { + const props = z.object({ body: richTextFieldSchema() }); + const parsed = props.safeParse({ body: { value: '

content

' } }); + expect(parsed.success).to.equal(true); + }); + + it('accepts linkFieldSchema as a prop', () => { + const props = z.object({ cta: linkFieldSchema() }); + const parsed = props.safeParse({ cta: { value: { href: '/about', text: 'About' } } }); + expect(parsed.success).to.equal(true); + }); + + it('accepts imageFieldSchema as a prop', () => { + const props = z.object({ image: imageFieldSchema() }); + const parsed = props.safeParse({ image: { value: { src: '/img.png', alt: 'Alt' } } }); + expect(parsed.success).to.equal(true); + }); + + it('accepts fileFieldSchema as a prop', () => { + const props = z.object({ doc: fileFieldSchema() }); + const parsed = props.safeParse({ doc: { value: { src: '/file.pdf', title: 'Doc' } } }); + expect(parsed.success).to.equal(true); + }); + + it('accepts dateFieldSchema as a prop', () => { + const props = z.object({ publishedAt: dateFieldSchema() }); + const parsed = props.safeParse({ publishedAt: { value: '20240101T000000Z' } }); + expect(parsed.success).to.equal(true); + }); + + it('preserves control hint on field schemas', () => { + const props = z.object({ cta: linkFieldSchema() }); + const ctaShape = props.shape.cta as z.ZodType; + expect(getFieldMeta(ctaShape)).to.deep.equal({ control: 'Link' }); + }); + }); +}); diff --git a/packages/react/src/atoms/field-schemas.ts b/packages/react/src/atoms/field-schemas.ts new file mode 100644 index 0000000000..d0e824d8a8 --- /dev/null +++ b/packages/react/src/atoms/field-schemas.ts @@ -0,0 +1,164 @@ +/** Zod schemas for Sitecore field types, for use in defineAtomsCatalog prop definitions. */ +import { z } from 'zod'; +import { withPropMeta } from './schema-utils'; + +/** + * Zod schema for a Sitecore Single-Line Text. + * Mirrors the Sitecore Text component (`Text.tsx` in `@sitecore-content-sdk/react`). + * @param {z.ZodRawShape} [extra] - Optional additional shape to merge into the schema. + * @returns A ZodObject with `value?: string | number` and the DS control hint attached. + * @public + */ +export const textFieldSchema = (extra?: z.ZodRawShape) => + withPropMeta( + z.object({ + value: z.union([z.string(), z.number()]).optional(), + ...extra, + }), + { control: 'Single-Line Text' } + ); + +/** + * Zod schema for a Sitecore Rich Text field. + * Mirrors the Sitecore Rich Text component (`RichText.tsx` in `@sitecore-content-sdk/react`). + * @param {z.ZodRawShape} [extra] - Optional additional shape to merge into the schema. + * @returns A ZodObject with `value?: string` and the DS control hint attached. + * @public + */ +export const richTextFieldSchema = (extra?: z.ZodRawShape) => + withPropMeta( + z.object({ + value: z.string().optional(), + ...extra, + }), + { control: 'Rich Text' } + ); + +/** + * Zod schema for a Sitecore Date field. + * Mirrors the field shape used in the Date component (`Date.tsx` in `@sitecore-content-sdk/react`). + * @param {z.ZodRawShape} [extra] - Optional additional shape to merge into the schema. + * @returns A ZodObject with `value?: string` and the DS control hint attached. + * @public + */ +export const dateFieldSchema = (extra?: z.ZodRawShape) => + withPropMeta( + z.object({ + value: z.string().optional(), + ...extra, + }), + { control: 'Date' } + ); + +/** + * Zod schema for a Sitecore Link field. + * Mirrors the Sitecore Link component (`Link.tsx` in `@sitecore-content-sdk/react`). + * The inner value object uses `z.looseObject` to allow arbitrary Sitecore-added attributes, + * matching the `[attributeName: string]: unknown` index signature on `LinkFieldValue`. + * @param {z.ZodRawShape} [extra] - Optional additional shape to merge into the outer schema. + * @returns A ZodObject with `value: LinkFieldValue` and the DS control hint attached. + * @public + */ +export const linkFieldSchema = (extra?: z.ZodRawShape) => + withPropMeta( + z.object({ + value: z.looseObject({ + href: z.string().optional(), + className: z.string().optional(), + class: z.string().optional(), + title: z.string().optional(), + target: z.string().optional(), + text: z.string().optional(), + anchor: z.string().optional(), + querystring: z.string().optional(), + linktype: z.string().optional(), + }), + ...extra, + }), + { control: 'Link' } + ); + +/** + * Zod schema for a Sitecore Image field. + * Mirrors the Sitecore Image component (`Image.tsx` in `@sitecore-content-sdk/react`). + * The inner value object uses `z.looseObject` to allow arbitrary HTML attributes, + * matching the `[attributeName: string]: unknown` index signature on `ImageFieldValue`. + * @param {z.ZodRawShape} [extra] - Optional additional shape to merge into the outer schema. + * @returns A ZodObject with `value?: ImageFieldValue` and the DS control hint attached. + * @public + */ +export const imageFieldSchema = (extra?: z.ZodRawShape) => + withPropMeta( + z.object({ + value: z + .looseObject({ + src: z.string().optional(), + alt: z.string().optional(), + width: z.union([z.string(), z.number()]).optional(), + height: z.union([z.string(), z.number()]).optional(), + class: z.string().optional(), + }) + .optional(), + ...extra, + }), + { control: 'Image' } + ); + +/** + * Zod schema for a Sitecore File field. + * Mirrors the Sitecore File component (`File.tsx` in `@sitecore-content-sdk/react`). + * The inner value object uses `z.looseObject` to allow arbitrary extra properties, + * matching the `[propName: string]: unknown` index signature on `FileFieldValue`. + * @param {z.ZodRawShape} [extra] - Optional additional shape to merge into the outer schema. + * @returns A ZodObject with `value: FileFieldValue` and the DS control hint attached. + * @public + */ +export const fileFieldSchema = (extra?: z.ZodRawShape) => + withPropMeta( + z.object({ + value: z.looseObject({ + src: z.string().optional(), + title: z.string().optional(), + displayName: z.string().optional(), + }), + ...extra, + }), + { control: 'File' } + ); + +/** + * Inferred type for a Sitecore Single-Line Text / Multi-Line Text field prop. + * Use this to type component props that accept a text field. + * @public + */ +export type TextFieldSchema = z.infer>; + +/** + * Inferred type for a Sitecore Rich Text field prop. + * @public + */ +export type RichTextFieldSchema = z.infer>; + +/** + * Inferred type for a Sitecore Date field prop. + * @public + */ +export type DateFieldSchema = z.infer>; + +/** + * Inferred type for a Sitecore General Link field prop. + * @public + */ +export type LinkFieldSchema = z.infer>; + +/** + * Inferred type for a Sitecore Image field prop. + * @public + */ +export type ImageFieldSchema = z.infer>; + +/** + * Inferred type for a Sitecore File field prop. + * @public + */ +export type FileFieldSchema = z.infer>; diff --git a/packages/react/src/atoms/index.ts b/packages/react/src/atoms/index.ts new file mode 100644 index 0000000000..ea7c94e69a --- /dev/null +++ b/packages/react/src/atoms/index.ts @@ -0,0 +1,30 @@ +export type { + AtomComponentDefinition, + AtomActionDefinition, + AtomsCatalogInput, + AtomsComponentsMap, + AtomActionHandler, + AtomsActionsMap, + AtomsConfig, + Exact, +} from './types'; +export { defineAtomsCatalog } from './define-atoms-catalog'; +export { defineAtomsRegistry } from './define-atoms-registry'; +export { serializeCatalog } from './catalog-serializer'; +export { withPropMeta, type PropMeta } from './schema-utils'; +export { + textFieldSchema, + richTextFieldSchema, + dateFieldSchema, + linkFieldSchema, + imageFieldSchema, + fileFieldSchema, + type TextFieldSchema, + type RichTextFieldSchema, + type DateFieldSchema, + type LinkFieldSchema, + type ImageFieldSchema, + type FileFieldSchema, +} from './field-schemas'; +export { createNCC } from './create-ncc'; +export * from './re-exports'; diff --git a/packages/react/src/atoms/re-exports.ts b/packages/react/src/atoms/re-exports.ts new file mode 100644 index 0000000000..245f63ee90 --- /dev/null +++ b/packages/react/src/atoms/re-exports.ts @@ -0,0 +1,23 @@ +import { useBoundProp as useBoundPropInternal } from '@json-render/react'; + +/** + * Hook for two-way bound props. Returns `[value, setValue]` where: + * + * - `value` is the already-resolved prop value (passed through from render props) + * - `setValue` writes back to the bound state path (no-op if not bound) + * + * Designed to work with the `bindings` map that the renderer provides when + * a prop uses `{ $bindState: "/path" }` or `{ $bindItem: "field" }`. + * @example + * ```tsx + * import { useBoundProp } from '@sitecore-content-sdk/react'; + * + * const Input: ComponentRenderer = ({ props, bindings }) => { + * const [value, setValue] = useBoundProp(props.value, bindings?.value); + * return setValue(e.target.value)} />; + * }; + * ``` + * @public + */ +export const useBoundProp = useBoundPropInternal; + diff --git a/packages/react/src/atoms/schema-utils.ts b/packages/react/src/atoms/schema-utils.ts new file mode 100644 index 0000000000..aab73dc4c2 --- /dev/null +++ b/packages/react/src/atoms/schema-utils.ts @@ -0,0 +1,26 @@ +/** Schema metadata for atom props/events. */ +import { z } from 'zod'; + +/** + * Prop metadata (e.g. control hint for Design Studio). + * @public + */ +export type PropMeta = { control?: string }; + +const META_KEY = 'meta'; + +/** + * Attach editor hint (e.g. control type) to a prop schema. Metadata is stored under a key that + * survives JSON Schema conversion for Design Studio. + * @param {import('zod').ZodType} schema - Zod type for the prop + * @param {PropMeta} meta - Editor metadata (e.g. control) + * @returns The same Zod type with meta attached (or schema unchanged if .meta is not callable) + * @public + */ +export function withPropMeta(schema: T, meta: PropMeta): T { + const s = schema as unknown as { meta?: (m: Record) => T }; + if (typeof s.meta === 'function') { + return s.meta({ [META_KEY]: meta }); + } + return schema; +} diff --git a/packages/react/src/atoms/types.ts b/packages/react/src/atoms/types.ts new file mode 100644 index 0000000000..c8e4dfa46c --- /dev/null +++ b/packages/react/src/atoms/types.ts @@ -0,0 +1,76 @@ +import type { Catalog, InferCatalogInput } from '@json-render/core'; +import type { ComponentRenderer, DefineRegistryResult, ReactSchema } from '@json-render/react'; +import { SitecoreComponentMeta } from '@sitecore-content-sdk/content/atoms'; + +type BaseCatalog = InferCatalogInput; +type BaseComponent = BaseCatalog['components'][string]; +type BaseAction = BaseCatalog['actions'][string]; + +/** + * Utility type that prevents extra keys beyond those defined in `Base`. + * @internal + */ +export type Exact = T & Record, never>; + +/** + * Component definition in the atoms catalog input. + * @public + */ +export type AtomComponentDefinition = BaseComponent & SitecoreComponentMeta; + +/** + * Action definition in the atoms catalog input. + * @public + */ +export type AtomActionDefinition = BaseAction; + +/** + * Input shape for defineAtomsCatalog. + * Extends json-render's base catalog input with Sitecore-specific fields. + * @public + */ +export type AtomsCatalogInput = BaseCatalog & { + /** Semver version of the catalog as a whole. Used by the lock file and Design Studio. */ + version?: string; + /** Component definitions keyed by name. */ + components: Record; + /** Action definitions keyed by name (required). */ + actions: Record; +}; + +/** + * Type alias for the component renderer. + * @public + */ +export type AtomsComponentRenderer = ComponentRenderer; + +/** + * Component implementations map for defineAtomsRegistry. + * @public + */ +export type AtomsComponentsMap = Record; + +/** + * Action handler function. + * @public + */ +export type AtomActionHandler = (params: Record) => Promise | void; + +/** + * Action implementations map for defineAtomsRegistry. + * @public + */ +export type AtomsActionsMap = Record; + +/** + * Props the developer passes to the provider for atoms support. + * @public + */ +export interface AtomsConfig { + /** The json-render catalog (schema + component/action definitions). */ + catalog: Catalog; + /** The registry result returned by defineAtomsRegistry. */ + registry: DefineRegistryResult; + /** Optional navigate function to be passed to action handlers for navigation purposes. */ + navigate?: (path: string) => void; +} diff --git a/packages/react/src/components/DesignLibrary/DesignLibrary.test.tsx b/packages/react/src/components/DesignLibrary/DesignLibrary.test.tsx index e5261aa983..df8ecc990c 100644 --- a/packages/react/src/components/DesignLibrary/DesignLibrary.test.tsx +++ b/packages/react/src/components/DesignLibrary/DesignLibrary.test.tsx @@ -1,733 +1,129 @@ /* eslint-disable jsdoc/require-jsdoc */ /* eslint-disable no-unused-expressions */ -/* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */ import React from 'react'; import sinon from 'sinon'; -import { expect } from 'chai'; -import { Page, PageMode } from '@sitecore-content-sdk/content/client'; -import { - LayoutServiceData, - EDITING_COMPONENT_PLACEHOLDER, -} from '@sitecore-content-sdk/content/layout'; -import { act, fireEvent, render, waitFor } from '@testing-library/react'; -import { DesignLibrary } from './DesignLibrary'; -import { getTestLayoutData } from '../../test-data/component-editing-data'; -import { SitecoreProvider } from '../SitecoreProvider'; -import { RichText } from '../RichText'; -import { Text } from '../Text'; -import { Placeholder } from '../Placeholder'; +import { expect, use as chaiUse } from 'chai'; +import sinonChai from 'sinon-chai'; +chaiUse(sinonChai); +import { render, waitFor } from '@testing-library/react'; +import { DesignLibrary, __mockDependencies } from './DesignLibrary'; +import { SitecoreProvider } from '../SitecoreProvider'; import { DesignLibraryStatus, getDesignLibraryStatusEvent, - DesignLibraryMode, } from '@sitecore-content-sdk/content/editing'; -import { __mockDependencies } from './DesignLibrary'; -import { - DesignLibraryPreviewError, - getDesignLibraryErrorEvent, -} from '@sitecore-content-sdk/content/codegen'; -import { after } from 'node:test'; -import * as rscUtils from '#rsc-env'; - -before(() => { - if (typeof window !== 'undefined' && !window.requestAnimationFrame) { - (window as any).requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(cb, 0); - } -}); +import type { AtomsConfig } from '../../atoms/types'; +import type { ImportMapImport } from './models'; +import type { DefineRegistryResult } from '@json-render/react'; describe('', () => { - after(() => { - sandbox.restore(); - }); const sandbox = sinon.createSandbox(); - before(() => { - sandbox.replace(rscUtils, 'rsc', false as any); - }); - const postMessageSpy = sandbox.spy(window, 'postMessage'); - const components = new Map(); - - const api = { - edge: { - contextId: 'test-context-id', - clientContextId: 'test-client-context-id', - edgeUrl: 'https://test-edge-url.com', - }, - local: { apiKey: 'test-api-key', apiHost: 'https://test-api-host.com', path: '/test-path' }, - }; - - // Modes - const modeLibraryMetadata: PageMode = { - name: DesignLibraryMode.Metadata, - isDesignLibrary: true, - designLibrary: { isVariantGeneration: false }, - isNormal: false, - isPreview: false, - isEditing: true, - }; - - const modeLibrary_Gen: PageMode = { - name: DesignLibraryMode.Normal, - isDesignLibrary: true, - designLibrary: { isVariantGeneration: true }, - isNormal: false, - isPreview: false, - isEditing: false, - }; - - const modeLibraryMetadata_Gen: PageMode = { - name: DesignLibraryMode.Metadata, - isDesignLibrary: true, - designLibrary: { isVariantGeneration: true }, - isNormal: false, - isPreview: false, - isEditing: true, - }; - - const getPage = (layout?: LayoutServiceData, pageMode: PageMode = modeLibrary): Page => ({ - locale: 'en', - layout: layout || { sitecore: { context: {}, route: null } }, - mode: pageMode, - }); - const ContentBlock: React.FC<{ - [prop: string]: unknown; - fields?: { content: { value: string }; heading: { value: string } }; - }> = (props) => ( -
- - -
- ); + const apiStub = {} as any; + const emptyComponentMap = new Map(); + const loadImportMapStub = async (): Promise => + ({ + default: [], + } as unknown as ImportMapImport); - const InnerBlock: React.FC<{ [prop: string]: unknown; fields?: { text: { value: string } } }> = ( - props - ) => ( -
- -
- ); - - components.set('ContentBlock', ContentBlock); - components.set('InnerBlock', InnerBlock); - - async function sendUpdate(details: { uid: string; fields?: any; params?: any }) { - const ev = document.createEvent('Event'); - ev.initEvent('message', false, true); - (ev as any).origin = window.location.origin; - (ev as any).data = { name: 'component:update', details }; - await fireEvent(window, ev); - } - - const defaultImportMap = () => - Promise.resolve({ - default: [{ module: 'react', exports: [{ name: 'default', value: React }] }], - }); - - const unsubscribeSpy = sandbox.spy(); - let addComponentPreviewHandlerSpy: sinon.SinonStub; let postToDesignLibrarySpy: sinon.SinonStub; - let sendErrorEventSpy: sinon.SinonStub; - let callbackEvent: any = null; - - const RENDER_ID = 'test-content'; - const PLACEHOLDER_GUID = '00000000-0000-0000-0000-000000000000'; - const joinHtml = (parts: string[]) => parts.join(''); - const expectContains = (html: string, parts: string[]) => - expect(html).to.contain(joinHtml(parts)); - - const expectedInitialMarkup = (guid = PLACEHOLDER_GUID, id = RENDER_ID) => - joinHtml([ - '
', - ``, - ``, - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - '', - '', - '
', - '', - '', - '
', - ]); - - const postedEventsJson = (spy: sinon.SinonSpy) => - spy.getCalls().map((c) => JSON.stringify(c.args[0])); - - const expectStatus = ( - spy: sinon.SinonSpy, - status: DesignLibraryStatus, - id: string, - opts: { strict?: boolean } = {} - ) => { - const target = JSON.stringify(getDesignLibraryStatusEvent(status, id)); - const events = postedEventsJson(spy); - if (opts.strict) { - expect(events).to.include(target); - } else { - expect(events.some((e) => e.includes(target))).to.be.true; - } + const mockRegistry: DefineRegistryResult = { + registry: {} as any, + handlers: () => ({}), + executeAction: async () => {}, }; - beforeEach(() => { - postMessageSpy.resetHistory(); - unsubscribeSpy.resetHistory(); - postToDesignLibrarySpy = sandbox.stub().callsFake((evt) => { - // postToDesignLibrary calls window.postMessage internally - window.postMessage(evt, '*'); - }); - sendErrorEventSpy = sandbox.stub().callsFake((uid, error, type) => { - // sendErrorEvent calls window.postMessage internally - const errorEvent = getDesignLibraryErrorEvent(uid, error, type); - window.postMessage(errorEvent, '*'); - }); - __mockDependencies({ - postToDesignLibrary: postToDesignLibrarySpy, - sendErrorEvent: sendErrorEventSpy, - }); + const mockCatalog = { + componentNames: ['Button'], + actionNames: [], + data: { components: { Button: { description: 'A button', slots: ['default'] } } }, + jsonSchema: () => ({}), + } as any; - if (typeof (globalThis as any).requestAnimationFrame === 'undefined') { - (globalThis as any).requestAnimationFrame = (cb: Function) => setTimeout(cb, 0); - (globalThis as any).cancelAnimationFrame = (id: any) => clearTimeout(id); - } - if (typeof window !== 'undefined') { - (window as any).requestAnimationFrame = (globalThis as any).requestAnimationFrame; - (window as any).cancelAnimationFrame = (globalThis as any).cancelAnimationFrame; - } - }); + const atomsConfig: AtomsConfig = { + catalog: mockCatalog, + registry: mockRegistry, + }; - it('should render null if not in design library mode', () => { - const page = getPage(getTestLayoutData().layoutData, { - name: DesignLibraryMode.Normal, - isDesignLibrary: false, - designLibrary: { - isVariantGeneration: false, + const getPage = (overrides: Record = {}) => ({ + locale: 'en', + layout: { + sitecore: { + context: {}, + route: { + uid: 'test-uid', + placeholders: { + 'editing-componentmode-placeholder': [ + { + uid: 'component-1', + componentName: 'TestComponent', + fields: {}, + params: {}, + }, + ], + }, + }, }, - isNormal: false, - isPreview: false, - isEditing: false, - }); - - const rendered = render( - - - , - { container: document.body } - ); - expect(rendered.baseElement.innerHTML).to.equal(''); - }); - - describe('mode=library and isVariantGeneration=false', () => { - let page: Page; - - const modeLibrary: PageMode = { - name: DesignLibraryMode.Normal, + }, + mode: { + name: 'normal', isDesignLibrary: true, designLibrary: { isVariantGeneration: false }, isNormal: false, isPreview: false, isEditing: false, - }; - - async function sendUpdateEvent(details: { uid: string; fields?: any; params?: any }) { - const ev = document.createEvent('Event'); - ev.initEvent('message', false, true); - (ev as any).origin = window.location.origin; - (ev as any).data = { name: 'component:update', details }; - await fireEvent(window, ev); - } - - beforeEach(() => { - const basic = getTestLayoutData(); - page = { - locale: 'en', - layout: basic.layoutData, - mode: modeLibrary, - }; - postMessageSpy.resetHistory(); - }); - - it('renders real component and sends READY + initial RENDERED', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibrary); - - const rendered = render( - - - - ); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - ].join('') - ); - - expect( - postMessageSpy - .getCalls() - .some( - (c) => - JSON.stringify(c.args[0]) === - JSON.stringify(getDesignLibraryStatusEvent(DesignLibraryStatus.READY, 'test-content')) - ) - ).to.be.true; - - await waitFor(() => { - expect( - postMessageSpy - .getCalls() - .some((c) => - JSON.stringify(c.args[0]).includes( - JSON.stringify( - getDesignLibraryStatusEvent(DesignLibraryStatus.RENDERED, 'test-content') - ) - ) - ) - ).to.be.true; - }); - }); - - it('should render component with placeholders', () => { - page.layout = getTestLayoutData(true).layoutData; - - const rendered = render( - - - , - { container: document.body } - ); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - '
', - 'Its an inner component', - '
', - '
', - ].join('') - ); - }); - - it('should update root component', async () => { - const rendered = render( - - - , - { container: document.body } - ); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - ].join('') - ); - - await sendUpdateEvent({ - uid: 'test-content', - fields: { content: { value: 'new content!' } }, - }); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - 'new content!', - '
', - ].join('') - ); - }); - - it('should update nested component', async () => { - const withPlaceholder = getTestLayoutData(true); - page.layout = withPlaceholder.layoutData; - - const rendered = render( - - - , - { container: document.body } - ); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - '
', - 'Its an inner component', - '
', - '
', - ].join('') - ); - - await sendUpdateEvent({ - uid: 'test-inner', - fields: { text: { value: 'new inner content!' } }, - }); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - '
', - 'new inner content!', - '
', - '
', - ].join('') - ); - }); + ...overrides, + }, }); - describe('mode=library-metadata and isVariantGeneration=false', () => { - it('renders real component and sends READY + initial RENDERED (Pages Router)', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibraryMetadata); - - const rendered = render( - - - - ); - - expect(rendered.baseElement.innerHTML).to.contain(expectedInitialMarkup()); - - expectStatus(postMessageSpy, DesignLibraryStatus.READY, RENDER_ID, { strict: true }); - - await waitFor(() => expectStatus(postMessageSpy, DesignLibraryStatus.RENDERED, RENDER_ID)); + beforeEach(() => { + postToDesignLibrarySpy = sandbox.stub(); + __mockDependencies({ + postToDesignLibrary: postToDesignLibrarySpy, + addComponentPreviewHandler: sandbox.stub(), + sendErrorEvent: sandbox.stub(), }); }); - describe('mode=library&generation=variant and isVariantGeneration=true', () => { - beforeEach(() => { - addComponentPreviewHandlerSpy = sandbox.stub().callsFake((_importMap, cb) => { - callbackEvent = cb; - return unsubscribeSpy; - }); - __mockDependencies({ - addComponentPreviewHandler: addComponentPreviewHandlerSpy, - postToDesignLibrary: postToDesignLibrarySpy, - sendErrorEvent: sendErrorEventSpy, - }); - - postMessageSpy.resetHistory(); - }); - - it('fires component:ready on mount', () => { - const page = getPage(getTestLayoutData().layoutData, modeLibrary_Gen); - - render( - - - - ); - - const expectedReady = getDesignLibraryStatusEvent(DesignLibraryStatus.READY, 'test-content'); - expect( - postMessageSpy - .getCalls() - .some((c) => JSON.stringify(c.args[0]) === JSON.stringify(expectedReady)) - ).to.be.true; - - const readyCount = postMessageSpy - .getCalls() - .filter( - (c) => - c.args[0]?.name === expectedReady.name && - c.args[0]?.message?.uid === expectedReady.message.uid - ).length; - expect(readyCount).to.equal(1); - }); - - it('fires component:rendered only after generated component is received', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibrary_Gen); - - render( - - - - ); - - const expectedRendered = getDesignLibraryStatusEvent( - DesignLibraryStatus.RENDERED, - 'test-content' - ); - expect( - postMessageSpy - .getCalls() - .some((c) => JSON.stringify(c.args[0]) === JSON.stringify(expectedRendered)) - ).to.be.false; - - await waitFor(() => { - expect(addComponentPreviewHandlerSpy).to.have.been.called; - }); - - const TestComponent = () =>
Generated!
; - callbackEvent(null, TestComponent); - - await waitFor(() => { - expect( - postMessageSpy - .getCalls() - .some((c) => JSON.stringify(c.args[0]) === JSON.stringify(expectedRendered)) - ).to.be.true; - }); - }); - - it('renders real component first, wires generation, then switches to generated component', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibrary_Gen); - - const rendered = render( - - - - ); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - ].join('') - ); - - await waitFor(() => { - expect(addComponentPreviewHandlerSpy).to.have.been.called; - }); - - const TestComponent = () =>
Generated!
; - callbackEvent(null, TestComponent); - - await waitFor(() => { - expect(rendered.baseElement.innerHTML).to.contain('
Generated!
'); - }); - }); - - it('renders real component first, wires generation, then switches to generated component when loadImportMap provided via SitecoreProvider', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibrary_Gen); - - const rendered = render( - - - - ); - - expect(rendered.baseElement.innerHTML).to.contain( - [ - '
', - '
', - '

This is a live set of examples of how to use Content SDK

\n', - '
', - ].join('') - ); - - await waitFor(() => { - expect(addComponentPreviewHandlerSpy).to.have.been.called; - }); - - const TestComponent = () =>
Generated!
; - callbackEvent(null, TestComponent); - - await waitFor(() => { - expect(rendered.baseElement.innerHTML).to.contain('
Generated!
'); - }); - }); - - it('updates via component:update after switch', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibrary_Gen); - - const Gen = (props: any) =>
{props.fields?.content?.value}
; - - render( - - - - ); - - await waitFor(() => { - expect(addComponentPreviewHandlerSpy).to.have.been.called; - callbackEvent(null, Gen); - }); - - await sendUpdate({ - uid: 'test-content', - fields: { content: { value: 'updated!' } }, - }); - - await waitFor(() => { - expect( - postMessageSpy - .getCalls() - .some((c) => - JSON.stringify(c.args[0]).includes( - JSON.stringify( - getDesignLibraryStatusEvent(DesignLibraryStatus.RENDERED, 'test-content') - ) - ) - ) - ).to.be.true; - }); - }); - - it('sends error event when no import map is provided', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibrary_Gen); - - render( - - - - ); - - await waitFor(() => { - expect( - postMessageSpy - .getCalls() - .some((c) => - JSON.stringify(c.args[0]).includes( - JSON.stringify( - getDesignLibraryErrorEvent( - 'test-content', - 'No loadImportMap provided', - DesignLibraryPreviewError.ImportMapMissing - ) - ) - ) - ) - ).to.be.true; - }); - }); + afterEach(() => { + sandbox.restore(); }); - describe('?mode=library-metadata&generation=variant and isVariantGeneration=true', () => { - beforeEach(() => { - addComponentPreviewHandlerSpy = sandbox.stub().callsFake((_importMap, cb) => { - callbackEvent = cb; - return unsubscribeSpy; - }); - __mockDependencies({ - addComponentPreviewHandler: addComponentPreviewHandlerSpy, - postToDesignLibrary: postToDesignLibrarySpy, - sendErrorEvent: sendErrorEventSpy, - }); - }); - - const expectedGeneratedParts = [ - '', - '
Gen-Metadata
', - '
', - ]; - - const triggerGeneration = async () => { - await act(async () => { - callbackEvent(null, () => ( - -
Gen-Metadata
-
- )); - }); - }; - - it('renders real component first, wires generation, then switches to generated component (Pages Router)', async () => { - const page = getPage(getTestLayoutData().layoutData, modeLibraryMetadata_Gen); - - const rendered = render( - - - - ); - - expect(rendered.baseElement.innerHTML).to.contain(expectedInitialMarkup()); - - await waitFor(() => expect(addComponentPreviewHandlerSpy).to.have.been.called); - - expectStatus(postMessageSpy, DesignLibraryStatus.READY, RENDER_ID, { strict: true }); - - await triggerGeneration(); - - await waitFor(() => expectContains(rendered.baseElement.innerHTML, expectedGeneratedParts)); - - await waitFor(() => expectStatus(postMessageSpy, DesignLibraryStatus.RENDERED, RENDER_ID)); - }); + it('renders null when not in design library mode', () => { + const page = getPage({ isDesignLibrary: false }); + const { container } = render( + + + + ); + expect(container.innerHTML).to.equal(''); }); - describe('error handling', () => { - it('should render ErrorComponent when rendering UID is missing', () => { - const layoutData = getTestLayoutData().layoutData; - - // Remove UID from the component - const renderingWithoutUid = { - ...layoutData.sitecore.route, - placeholders: { - [EDITING_COMPONENT_PLACEHOLDER]: [ - { - ...layoutData.sitecore.route.placeholders?.[EDITING_COMPONENT_PLACEHOLDER]?.[0], - uid: undefined, - }, - ], - }, - }; - - const page = { - locale: 'en', - layout: { ...layoutData, sitecore: { ...layoutData.sitecore, route: renderingWithoutUid } }, - mode: modeLibraryMetadata, - }; - - const rendered = render( - - - - ); - - expect(rendered.baseElement.innerHTML).to.contain( - 'Rendering UID is missing in the rendering data' + it('posts READY status on mount when in design library mode', async () => { + const page = getPage(); + render( + + + + ); + await waitFor(() => { + expect(postToDesignLibrarySpy).to.have.been.calledWith( + getDesignLibraryStatusEvent(DesignLibraryStatus.READY, 'component-1') ); }); }); - after(() => { - sandbox.restore(); - }); }); diff --git a/packages/react/src/components/DesignLibrary/DesignLibrary.tsx b/packages/react/src/components/DesignLibrary/DesignLibrary.tsx index d496f927e2..122c48ae78 100644 --- a/packages/react/src/components/DesignLibrary/DesignLibrary.tsx +++ b/packages/react/src/components/DesignLibrary/DesignLibrary.tsx @@ -18,6 +18,8 @@ import { Placeholder, PlaceholderMetadata } from '../Placeholder'; import { DesignLibraryErrorBoundary } from './DesignLibraryErrorBoundary'; import { DynamicComponent } from './models'; import { ErrorComponent } from '../ErrorBoundary'; +import { serializeCatalog } from '../../atoms'; +import { getDesignLibraryAtomsCatalogEvent } from '@sitecore-content-sdk/content/atoms'; let { getDesignLibraryImportMapEvent, @@ -48,7 +50,7 @@ export const __mockDependencies = (mocks: any) => { * @public */ export const DesignLibrary = () => { - const { page, loadImportMap } = useSitecore(); + const { page, loadImportMap, atomsConfig } = useSitecore(); const route = page.layout.sitecore.route; const rendering = route?.placeholders[EDITING_COMPONENT_PLACEHOLDER]?.[0]; const uid = rendering?.uid; @@ -139,6 +141,11 @@ export const DesignLibrary = () => { const importMapEvent = getDesignLibraryImportMapEvent(uid, importMap); postToDesignLibrary(importMapEvent); + if (atomsConfig?.catalog) { + const catalogPayload = serializeCatalog(atomsConfig.catalog); + postToDesignLibrary(getDesignLibraryAtomsCatalogEvent(catalogPayload)); + } + const propsEvent = getDesignLibraryComponentPropsEvent( uid, propsState.fields, @@ -152,7 +159,7 @@ export const DesignLibrary = () => { cancelled = true; unsubscribe && unsubscribe(); }; - }, [isDesignLibrary, isVariantGeneration, uid, loadImportMap, propsState]); + }, [isDesignLibrary, isVariantGeneration, uid, loadImportMap, propsState, atomsConfig]); return (
diff --git a/packages/react/src/components/DesignLibrary/DesignLibraryLowCodeComponent.test.tsx b/packages/react/src/components/DesignLibrary/DesignLibraryLowCodeComponent.test.tsx new file mode 100644 index 0000000000..22a472673c --- /dev/null +++ b/packages/react/src/components/DesignLibrary/DesignLibraryLowCodeComponent.test.tsx @@ -0,0 +1,134 @@ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable no-unused-expressions */ +import React from 'react'; +import sinon from 'sinon'; +import { expect, use as chaiUse } from 'chai'; +import sinonChai from 'sinon-chai'; + +chaiUse(sinonChai); +import { render, waitFor } from '@testing-library/react'; +import { DesignLibraryLowCodeComponent, __mockDependencies } from './DesignLibraryLowCodeComponent'; +import { SitecoreProvider } from '../SitecoreProvider'; +import { + DesignLibraryStatus, + getDesignLibraryStatusEvent, +} from '@sitecore-content-sdk/content/editing'; +import type { AtomsConfig } from '../../atoms/types'; +import type { ImportMapImport } from './models'; +import type { DefineRegistryResult } from '@json-render/react'; + +describe('', () => { + const sandbox = sinon.createSandbox(); + + const apiStub = {} as any; + const emptyComponentMap = new Map(); + const loadImportMapStub = async (): Promise => ({} as ImportMapImport); + + let postToDesignLibrarySpy: sinon.SinonStub; + let sendAtomsErrorEventSpy: sinon.SinonStub; + let addDocumentUpdateHandlerStub: sinon.SinonStub; + + const mockRegistry: DefineRegistryResult = { + registry: {} as any, + handlers: () => ({}), + executeAction: async () => {}, + }; + + const mockCatalog = { + componentNames: ['Button'], + actionNames: [], + data: { + components: { + Button: { + props: { toJSONSchema: () => ({}) }, + description: 'A button', + slots: ['default'], + }, + }, + actions: {}, + }, + jsonSchema: () => ({}), + } as any; + + const atomsConfig: AtomsConfig = { + catalog: mockCatalog, + registry: mockRegistry, + }; + + const getPage = () => ({ + locale: 'en', + layout: { sitecore: { context: {}, route: null } }, + mode: { + name: 'normal', + isDesignLibrary: false, + designLibrary: { isVariantGeneration: false, isLowCode: false }, + isNormal: true, + isPreview: false, + isEditing: false, + }, + }); + + beforeEach(() => { + postToDesignLibrarySpy = sandbox.stub(); + sendAtomsErrorEventSpy = sandbox.stub(); + addDocumentUpdateHandlerStub = sandbox.stub().returns(() => {}); + __mockDependencies({ + postToDesignLibrary: postToDesignLibrarySpy, + sendAtomsErrorEvent: sendAtomsErrorEventSpy, + addDocumentUpdateHandler: addDocumentUpdateHandlerStub, + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + const renderComponent = (runtime?: AtomsConfig) => + render( + + + + ); + + it('posts READY status on mount', async () => { + renderComponent(atomsConfig); + await waitFor(() => { + expect(postToDesignLibrarySpy).to.have.been.calledWith( + getDesignLibraryStatusEvent(DesignLibraryStatus.READY, 'low-code-component') + ); + }); + }); + + it('sends error when no catalog is provided', async () => { + renderComponent(undefined); + await waitFor(() => { + expect(sendAtomsErrorEventSpy).to.have.been.calledWith( + 'No atoms catalog provided', + 'atoms-missing' + ); + }); + }); + + it('posts atoms:catalog event when catalog is available', async () => { + renderComponent(atomsConfig); + await waitFor(() => { + const catalogCall = postToDesignLibrarySpy + .getCalls() + .find((c: sinon.SinonSpyCall) => c.args[0]?.name === 'atoms:catalog'); + expect(catalogCall).to.not.be.undefined; + }); + }); + + it('subscribes to document update handler', async () => { + renderComponent(atomsConfig); + await waitFor(() => { + expect(addDocumentUpdateHandlerStub).to.have.been.called; + }); + }); +}); diff --git a/packages/react/src/components/DesignLibrary/DesignLibraryLowCodeComponent.tsx b/packages/react/src/components/DesignLibrary/DesignLibraryLowCodeComponent.tsx new file mode 100644 index 0000000000..32d05be7de --- /dev/null +++ b/packages/react/src/components/DesignLibrary/DesignLibraryLowCodeComponent.tsx @@ -0,0 +1,79 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import { useSitecore } from '../SitecoreProvider'; +import { serializeCatalog } from '../../atoms'; +import { StudioComponentWrapper } from './StudioComponentWrapper'; +import type { Document } from '@sitecore-content-sdk/content/atoms'; +import * as editing from '@sitecore-content-sdk/content/editing'; +import * as atoms from '@sitecore-content-sdk/content/atoms'; +import { DesignLibraryErrorBoundary } from '../..'; + +let { postToDesignLibrary, getDesignLibraryStatusEvent, DesignLibraryStatus } = editing; +let { addDocumentUpdateHandler, getDesignLibraryAtomsCatalogEvent, sendAtomsErrorEvent } = atoms; + +export const __mockDependencies = (mocks: any) => { + if (mocks.postToDesignLibrary) { + postToDesignLibrary = mocks.postToDesignLibrary; + } + if (mocks.sendAtomsErrorEvent) { + sendAtomsErrorEvent = mocks.sendAtomsErrorEvent; + } + if (mocks.addDocumentUpdateHandler) { + addDocumentUpdateHandler = mocks.addDocumentUpdateHandler; + } +}; + +/** + * Design Library Low Code component. + * + * Facilitates the communication between the Design Studio and the Rendering Host when previewing a low code component built with the Atoms. + * - On mount, it serializes the atoms catalog and sends it to the Design Studio via the `atoms:catalog` event. + * - Receives Component model data updates via document update handler and renders the low code component + * via `StudioComponentWrapper` (same client path as Studio / NCC preview elsewhere). + * @internal + */ +export const DesignLibraryLowCodeComponent = () => { + const { atomsConfig } = useSitecore(); + const [currentDocument, setCurrentDocument] = useState(null); + const [renderKey, setRenderKey] = useState(0); + + useEffect(() => { + postToDesignLibrary( + getDesignLibraryStatusEvent(DesignLibraryStatus.READY, 'low-code-component') + ); + }, []); + + useEffect(() => { + if (!atomsConfig?.catalog) { + sendAtomsErrorEvent('No atoms catalog provided', 'atoms-missing'); + return; + } + + const payload = serializeCatalog(atomsConfig.catalog); + postToDesignLibrary(getDesignLibraryAtomsCatalogEvent(payload)); + + const unsubDocumentUpdate = addDocumentUpdateHandler((updatedDocument) => { + setCurrentDocument(updatedDocument); + setRenderKey((k) => k + 1); + }); + + return () => unsubDocumentUpdate(); + }, [atomsConfig]); + + useEffect(() => { + if (renderKey === 0) return; + + postToDesignLibrary( + getDesignLibraryStatusEvent(DesignLibraryStatus.RENDERED, 'low-code-component') + ); + }, [renderKey]); + + return ( + + + + ); +}; diff --git a/packages/react/src/components/DesignLibrary/StudioComponentServerWrapper.test.tsx b/packages/react/src/components/DesignLibrary/StudioComponentServerWrapper.test.tsx new file mode 100644 index 0000000000..42dc8ecb60 --- /dev/null +++ b/packages/react/src/components/DesignLibrary/StudioComponentServerWrapper.test.tsx @@ -0,0 +1,225 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from 'react'; +import { expect } from 'chai'; +import { createSandbox, SinonSandbox, SinonStub } from 'sinon'; +import proxyquire from 'proxyquire'; +import type { Document } from '@sitecore-content-sdk/content/atoms'; + +describe('StudioComponentServerWrapper', () => { + let sandbox: SinonSandbox; + + let module: any; + let StudioComponentServerWrapper: any; + + let fetcherGetStub: SinonStub; + let StudioComponentWrapperStub: SinonStub; + let consoleWarnStub: SinonStub; + let consoleErrorStub: SinonStub; + let resolveEdgeUrlStub: SinonStub; + + const sampleDocument: Document = { + name: 'hero', + root: 'test', + elements: { + test: { type: 'Text', props: { content: 'Hello' }, children: [] }, + }, + }; + + beforeEach(() => { + sandbox = createSandbox(); + + fetcherGetStub = sandbox.stub().resolves({ data: JSON.stringify(sampleDocument) }); + StudioComponentWrapperStub = sandbox + .stub() + .returns(React.createElement('div', { 'data-test': 'wrapper' })); + consoleWarnStub = sandbox.stub(console, 'warn'); + consoleErrorStub = sandbox.stub(console, 'error'); + resolveEdgeUrlStub = sandbox.stub().returns('https://edge.example.com'); + + module = proxyquire('./StudioComponentServerWrapper', { + '@sitecore-content-sdk/core': { + NativeDataFetcher: class { + get: SinonStub; + constructor() { + this.get = fetcherGetStub; + } + }, + }, + '@sitecore-content-sdk/core/tools': { + resolveEdgeUrl: resolveEdgeUrlStub, + }, + '@sitecore-content-sdk/content': { + debug: { layout: undefined }, + }, + './StudioComponentWrapper': { + StudioComponentWrapper: StudioComponentWrapperStub, + }, + }); + + StudioComponentServerWrapper = module.StudioComponentServerWrapper; + + process.env.SITECORE_EDGE_PLATFORM_HOSTNAME = 'https://edge.example.com'; + }); + + afterEach(() => { + sandbox.restore(); + delete process.env.SITECORE_EDGE_PLATFORM_HOSTNAME; + }); + + describe('early returns', () => { + it('returns null when componentRef is empty string', async () => { + const result = await StudioComponentServerWrapper({ componentRef: '' }); + expect(result).to.be.null; + }); + + it('returns null when componentRef is undefined', async () => { + const result = await StudioComponentServerWrapper({ componentRef: undefined as any }); + expect(result).to.be.null; + }); + + it('returns null when no path can be extracted from componentRef', async () => { + const result = await StudioComponentServerWrapper({ + componentRef: 'some/path/variant1', + fieldNames: 'nonexistent', + }); + // 'nonexistent' does not match 'variant1' and there is no 'default' segment + expect(result).to.be.null; + expect(consoleWarnStub).to.have.been.calledWithMatch( + 'StudioComponentServerWrapper: failed to extract path' + ); + }); + }); + + describe('path extraction (extractVariantPathFromComponentRef)', () => { + it('uses the path whose last segment matches fieldNames', async () => { + const result = await StudioComponentServerWrapper({ + componentRef: 'org/components/hero/mobile | org/components/hero/desktop', + fieldNames: 'desktop', + }); + + expect(result).to.not.be.null; + // Ensure fetcher was called with the desktop path + const calledUrl: string = fetcherGetStub.firstCall.args[0]; + expect(calledUrl).to.include('desktop'); + }); + + it('falls back to the "default" variant when fieldNames does not match', async () => { + const result = await StudioComponentServerWrapper({ + componentRef: 'org/components/hero/default | org/components/hero/mobile', + fieldNames: 'desktop', + }); + + expect(result).to.not.be.null; + const calledUrl: string = fetcherGetStub.firstCall.args[0]; + expect(calledUrl).to.include('default'); + }); + + it('uses "default" fieldNames when fieldNames prop is omitted', async () => { + const result = await StudioComponentServerWrapper({ + componentRef: 'org/components/hero/default', + }); + + expect(result).to.not.be.null; + const calledUrl: string = fetcherGetStub.firstCall.args[0]; + expect(calledUrl).to.include('default'); + }); + + it('returns null and warns when no path matches and no default exists', async () => { + const result = await StudioComponentServerWrapper({ + componentRef: 'org/components/hero/mobile', + fieldNames: 'desktop', + }); + + expect(result).to.be.null; + expect(consoleWarnStub).to.have.been.calledWithMatch( + 'StudioComponentServerWrapper: failed to extract path' + ); + }); + }); + + describe('fetchDocument — URL construction', () => { + it('prepends /mms/ to a relative path that starts with /', async () => { + await StudioComponentServerWrapper({ componentRef: '/components/hero/default' }); + + const calledUrl: string = fetcherGetStub.firstCall.args[0]; + expect(calledUrl).to.include('/mms/components/hero/default'); + }); + + it('prepends /mms/ to a relative path without a leading /', async () => { + await StudioComponentServerWrapper({ componentRef: 'components/hero/default' }); + + const calledUrl: string = fetcherGetStub.firstCall.args[0]; + expect(calledUrl).to.include('/mms/components/hero/default'); + }); + }); + + describe('fetchDocument — fetch errors', () => { + it('returns null and errors when fetcher.get throws', async () => { + fetcherGetStub.rejects(new Error('network error')); + + const result = await StudioComponentServerWrapper({ + componentRef: 'components/hero/default', + }); + + expect(result).to.be.null; + expect(consoleErrorStub).to.have.been.calledWithMatch( + 'StudioComponentServerWrapper: failed to fetch component layout' + ); + }); + + it('returns null and errors when response body is not valid JSON', async () => { + fetcherGetStub.resolves({ data: 'not-json{{' }); + + const result = await StudioComponentServerWrapper({ + componentRef: 'components/hero/default', + }); + + expect(result).to.be.null; + expect(consoleErrorStub).to.have.been.calledWithMatch( + 'StudioComponentServerWrapper: failed to parse component layout response' + ); + }); + }); + + describe('fetchDocument — path validation', () => { + it('renders when fieldNames matches the variant segment in componentRef', async () => { + const result = await StudioComponentServerWrapper({ + componentRef: 'org/components/hero/nonexistent', + fieldNames: 'nonexistent', + }); + + expect(result).to.not.be.null; + expect(fetcherGetStub).to.have.been.calledOnce; + expect(result.type).to.equal(StudioComponentWrapperStub); + expect(result.props.document).to.deep.equal(sampleDocument); + }); + + it('returns null and errors when URL resolution fails', async () => { + resolveEdgeUrlStub.throws(new Error('invalid hostname')); + + const result = await StudioComponentServerWrapper({ + componentRef: 'components/hero/default', + }); + + expect(result).to.be.null; + expect(consoleErrorStub).to.have.been.calledWithMatch( + 'StudioComponentServerWrapper: failed to resolve component from' + ); + }); + }); + + describe('successful render', () => { + it('renders StudioComponentWrapper with the fetched document', async () => { + const result = await StudioComponentServerWrapper({ + componentRef: 'components/hero/default', + }); + + // The server wrapper returns a React element (JSX), not a rendered output. + // Assert the element type and props directly. + expect(result).to.not.be.null; + expect(result.type).to.equal(StudioComponentWrapperStub); + expect(result.props.document).to.deep.equal(sampleDocument); + }); + }); +}); diff --git a/packages/react/src/components/DesignLibrary/StudioComponentServerWrapper.tsx b/packages/react/src/components/DesignLibrary/StudioComponentServerWrapper.tsx new file mode 100644 index 0000000000..cfd4d76e3a --- /dev/null +++ b/packages/react/src/components/DesignLibrary/StudioComponentServerWrapper.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { StudioComponentWrapper } from './StudioComponentWrapper'; +import { NativeDataFetcher, NativeDataFetcherResponse } from '@sitecore-content-sdk/core'; +import { debug } from '@sitecore-content-sdk/content'; +import { Document } from '@sitecore-content-sdk/content/atoms'; +import { resolveEdgeUrl } from '@sitecore-content-sdk/core/tools'; + +/** + * Props accepted by the RSC `StudioComponentServerWrapper`. + * @internal + */ +export type StudioComponentServerWrapperProps = { + /** + * Pipe separated relative paths to the Studio component layout JSON in MMS with the last segment as the variant name. The path matching `FieldNames` will be used, or `default` if no match. + */ + componentRef: string; + /** + * Field name to match against the last segment of the `componentRef` paths. If no match is found, the `default` path will be used. + */ + fieldNames?: string; +}; + +/** + * Server component for Studio (NCC) components. Fetches the component layout + * `Document` from MMS server-side and renders the client `StudioComponentWrapper`. + * @param {StudioComponentServerWrapperProps} props incoming props + * @returns rendered `StudioComponentWrapper` + * @internal + */ +export const StudioComponentServerWrapper = async (props: StudioComponentServerWrapperProps) => { + const componentRef = props.componentRef || ''; + if (!componentRef) return null; + + const path = extractVariantPathFromComponentRef(componentRef, props.fieldNames); + if (!path) return null; + + const document = await fetchDocument(path); + if (!document) return null; + + return ; +}; + +/** + * Extracts the variant path from a component reference. + * @param {string} componentRef The component reference string. + * @param {string} fieldNames The field names to extract the variant path for. + * @returns {string} The variant path. + * @internal + */ +function extractVariantPathFromComponentRef( + componentRef: string, + fieldNames: string = 'default' +): string | null { + const paths = componentRef.split('|').reduce((acc, part) => { + const path = part.trim(); + const segments = path.split('/'); + const variant = segments[segments.length - 1]; + + acc.set(variant, path); + + return acc; + }, new Map()); + + const path = paths.get(fieldNames) || paths.get('default') || null; + + if (!path) { + console.warn( + `StudioComponentServerWrapper: failed to extract path from ComponentRef "${componentRef}" with fieldNames "${fieldNames}". ` + + 'Ensure the ComponentRef is in the expected format and that the correct fieldNames are provided.' + ); + } + + return path; +} + +/** + * Prefix for MMS component paths in componentRef. The final URL will be resolved as `${host}/${MMS_COMPONENT_PATH_PREFIX}/${path}`. + */ +const MMS_COMPONENT_PATH_PREFIX = 'mms'; + +/** + * Fetch a Studio component layout `Document` by reference. + * @param {string} path extracted component reference (path) from `params.ComponentRef` + * @returns {Promise} the resolved component layout, or `null` on missing path, fetch failure, or un-parseable body. + */ +async function fetchDocument(path: string): Promise { + let url: string; + try { + const pathWithMmsPrefix = path.startsWith('/') + ? `/${MMS_COMPONENT_PATH_PREFIX}${path}` + : `/${MMS_COMPONENT_PATH_PREFIX}/${path}`; + + const hostURL = resolveEdgeUrl(); + + url = new URL(pathWithMmsPrefix, hostURL).toString(); + } catch (err) { + console.error(`StudioComponentServerWrapper: failed to resolve component from "${path}"`, err); + return null; + } + + let response: NativeDataFetcherResponse; + try { + const fetcher = new NativeDataFetcher({ debugger: debug.layout }); + response = await fetcher.get(url); + } catch (error) { + console.error( + `StudioComponentServerWrapper: failed to fetch component layout from ${url}`, + error + ); + return null; + } + + try { + const document: Document = JSON.parse(response.data); + + return document; + } catch (err) { + console.error( + `StudioComponentServerWrapper: failed to parse component layout response from ${url}`, + err + ); + return null; + } +} diff --git a/packages/react/src/components/DesignLibrary/StudioComponentWrapper.test.tsx b/packages/react/src/components/DesignLibrary/StudioComponentWrapper.test.tsx new file mode 100644 index 0000000000..f7a9e79071 --- /dev/null +++ b/packages/react/src/components/DesignLibrary/StudioComponentWrapper.test.tsx @@ -0,0 +1,899 @@ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable no-unused-expressions */ +import React from 'react'; +import { expect } from 'chai'; +import { render, fireEvent, act } from '@testing-library/react'; +import sinon from 'sinon'; +import { z } from 'zod'; +import { defineAtomsCatalog } from '../../atoms/define-atoms-catalog'; +import { defineAtomsRegistry } from '../../atoms/define-atoms-registry'; +import { useBoundProp } from '../../atoms'; +import { StudioComponentWrapper } from './StudioComponentWrapper'; +import { SitecoreProvider } from '../SitecoreProvider'; +import type { Document } from '@sitecore-content-sdk/content/atoms'; +import type { AtomsConfig } from '../../atoms/types'; +import type { ImportMapImport } from './models'; + +// ============================================================================= +// Shared provider helpers +// ============================================================================= + +const apiStub = {} as any; +const emptyComponentMap = new Map(); +const loadImportMapStub = async (): Promise => ({} as ImportMapImport); + +const getPage = () => ({ + locale: 'en', + layout: { sitecore: { context: {}, route: null } }, + mode: { + name: 'normal', + isDesignLibrary: false, + designLibrary: { isVariantGeneration: false }, + isNormal: true, + isPreview: false, + isEditing: false, + }, +}); + +function wrapInProvider(ui: React.ReactNode, atoms?: AtomsConfig) { + return ( + + {ui} + + ); +} + +// ============================================================================= +// Shared component stubs +// ============================================================================= + +const TextStub = ({ props }: any) => {(props as any)?.content}; +const BoxStub = ({ children }: any) =>
{children}
; +const ButtonStub = ({ props, emit }: any) => ( + +); +const CheckboxStub = ({ props, bindings }: any) => { + const [checked, setChecked] = useBoundProp((props as any)?.checked, bindings?.checked); + return ( + setChecked(e.target.checked)} + /> + ); +}; + +// ============================================================================= +// Shared catalogs + registries +// ============================================================================= + +/** Minimal catalog for prop-resolution tests (no Button, no actions). */ +const textBoxCatalog = defineAtomsCatalog({ + components: { + Text: { props: z.object({ content: z.string().optional() }), description: 'Text' }, + Box: { props: z.object({}), description: 'Box', slots: ['default'] }, + }, + actions: {}, +}); + +const textBoxRegistry = defineAtomsRegistry(textBoxCatalog, { + components: { + Text: ({ props, children }) => ( + + {props?.content ?? children} + + ), + Box: ({ children }) =>
{children}
, + }, + actions: {}, +}); + +const textBoxConfig: AtomsConfig = { catalog: textBoxCatalog, registry: textBoxRegistry }; + +/** Catalog with Text + Box + Button for interactive action tests. */ +const interactiveCatalog = defineAtomsCatalog({ + components: { + Text: { props: z.object({ content: z.string().optional() }), description: 'Text' }, + Box: { props: z.object({}), description: 'Box', slots: ['default'] }, + Button: { props: z.object({ label: z.string().optional() }), description: 'Button' }, + }, + actions: {}, +}); + +const interactiveRegistry = defineAtomsRegistry(interactiveCatalog, { + components: { Text: TextStub, Box: BoxStub, Button: ButtonStub }, +}); + +const interactiveConfig: AtomsConfig = { + catalog: interactiveCatalog, + registry: interactiveRegistry, +}; + +/** Catalog with Box + Text + Checkbox for two-way binding tests. */ +const bindCatalog = defineAtomsCatalog({ + components: { + Box: { props: z.object({}), description: 'Box', slots: ['default'] }, + Text: { props: z.object({ content: z.string().optional() }), description: 'Text' }, + Checkbox: { props: z.object({ checked: z.boolean().optional() }), description: 'Checkbox' }, + }, + actions: {}, +}); + +const bindRegistry = defineAtomsRegistry(bindCatalog, { + components: { Box: BoxStub, Text: TextStub, Checkbox: CheckboxStub }, +}); + +const bindConfig: AtomsConfig = { catalog: bindCatalog, registry: bindRegistry }; + +/** Catalog that adds Button to the bind set (for the external-setState test). */ +const bindWithButtonCatalog = defineAtomsCatalog({ + components: { + Box: { props: z.object({}), description: 'Box', slots: ['default'] }, + Text: { props: z.object({ content: z.string().optional() }), description: 'Text' }, + Checkbox: { props: z.object({ checked: z.boolean().optional() }), description: 'Checkbox' }, + Button: { props: z.object({ label: z.string().optional() }), description: 'Button' }, + }, + actions: {}, +}); + +const bindWithButtonRegistry = defineAtomsRegistry(bindWithButtonCatalog, { + components: { Box: BoxStub, Text: TextStub, Checkbox: CheckboxStub, Button: ButtonStub }, +}); + +const bindWithButtonConfig: AtomsConfig = { + catalog: bindWithButtonCatalog, + registry: bindWithButtonRegistry, +}; + +// ============================================================================= +// Sample documents +// ============================================================================= + +const sampleDoc: Document = { + name: 'hero', + root: 'root-el', + elements: { 'root-el': { type: 'Box', props: {}, children: [] } }, +}; + +// ============================================================================= +// Tests +// ============================================================================= + +describe('', () => { + const renderInProvider = (ui: React.ReactNode, atoms = textBoxConfig) => + render(wrapInProvider(ui, atoms)); + + // --------------------------------------------------------------------------- + // Guard conditions + // --------------------------------------------------------------------------- + + describe('guard conditions', () => { + it('renders null when document is null', () => { + const { container } = renderInProvider(); + expect(container.innerHTML).to.equal(''); + }); + + it('renders null when document is undefined', () => { + const { container } = renderInProvider(); + expect(container.innerHTML).to.equal(''); + }); + + it('renders null when atomsConfig is not provided', () => { + const { container } = render( + wrapInProvider(, undefined) + ); + expect(container.innerHTML).to.equal(''); + }); + + it('renders a view when document and atomsConfig are both provided', () => { + const { container } = renderInProvider(); + expect(container.innerHTML).to.not.equal(''); + }); + }); + + // --------------------------------------------------------------------------- + // Prop resolution + // --------------------------------------------------------------------------- + + describe('prop resolution', () => { + describe('$state', () => { + it('resolves a top-level path from doc.state', () => { + const doc: Document = { + name: 'state-test', + root: 'r', + elements: { + r: { type: 'Text', props: { content: { $state: '/message' } }, children: [] }, + }, + state: { message: 'Hello from state' }, + }; + const { getByTestId } = renderInProvider(); + expect(getByTestId('text-el').textContent).to.equal('Hello from state'); + }); + + it('resolves a nested JSON Pointer path', () => { + const doc: Document = { + name: 'nested-state', + root: 'r', + elements: { + r: { type: 'Text', props: { content: { $state: '/user/name' } }, children: [] }, + }, + state: { user: { name: 'Alice' } }, + }; + const { getByTestId } = renderInProvider(); + expect(getByTestId('text-el').textContent).to.equal('Alice'); + }); + }); + + describe('$template', () => { + it('interpolates state values into the template string', () => { + const doc: Document = { + name: 'template-test', + root: 'r', + elements: { + r: { + type: 'Text', + // Single-quoted — ${/name} is a literal template token, not a JS template literal + props: { content: { $template: 'Hello, ${/name}!' } }, + children: [], + }, + }, + state: { name: 'Alice' }, + }; + const { getByTestId } = renderInProvider(); + expect(getByTestId('text-el').textContent).to.equal('Hello, Alice!'); + }); + }); + + describe('$cond / $then / $else', () => { + it('renders the $then branch when the condition is truthy', () => { + const doc: Document = { + name: 'cond-truthy', + root: 'r', + elements: { + r: { + type: 'Text', + props: { + content: { $cond: { $state: '/isAdmin' }, $then: 'Admin', $else: 'Member' }, + }, + children: [], + }, + }, + state: { isAdmin: true }, + }; + const { getByTestId } = renderInProvider(); + expect(getByTestId('text-el').textContent).to.equal('Admin'); + }); + + it('renders the $else branch when the condition is falsy', () => { + const doc: Document = { + name: 'cond-falsy', + root: 'r', + elements: { + r: { + type: 'Text', + props: { + content: { $cond: { $state: '/isAdmin' }, $then: 'Admin', $else: 'Member' }, + }, + children: [], + }, + }, + state: { isAdmin: false }, + }; + const { getByTestId } = renderInProvider(); + expect(getByTestId('text-el').textContent).to.equal('Member'); + }); + }); + }); + + // --------------------------------------------------------------------------- + // Actions + // --------------------------------------------------------------------------- + + describe('actions', () => { + const renderInteractive = (ui: React.ReactNode) => renderInProvider(ui, interactiveConfig); + + describe('built-in setState', () => { + it('updates a $state-bound prop and re-renders', async () => { + const doc: Document = { + name: 'setstate-test', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['btn', 'display'] }, + btn: { + type: 'Button', + props: { label: 'Update' }, + on: { + press: { action: 'setState', params: { statePath: '/message', value: 'updated' } }, + }, + children: [], + }, + display: { type: 'Text', props: { content: { $state: '/message' } }, children: [] }, + }, + state: { message: 'initial' }, + }; + + const { getByTestId } = renderInteractive(); + expect(getByTestId('text-el').textContent).to.equal('initial'); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(getByTestId('text-el').textContent).to.equal('updated'); + }); + + it('updates a nested JSON Pointer path', async () => { + const doc: Document = { + name: 'nested-setstate', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['btn', 'display'] }, + btn: { + type: 'Button', + props: { label: 'Go' }, + on: { + press: { action: 'setState', params: { statePath: '/user/name', value: 'Bob' } }, + }, + children: [], + }, + display: { type: 'Text', props: { content: { $state: '/user/name' } }, children: [] }, + }, + state: { user: { name: 'Alice' } }, + }; + + const { getByTestId } = renderInteractive(); + expect(getByTestId('text-el').textContent).to.equal('Alice'); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(getByTestId('text-el').textContent).to.equal('Bob'); + }); + + it('a chained setState reads the value mutated by the preceding action', async () => { + const doc: Document = { + name: 'chained-setstate', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['btn', 'display'] }, + btn: { + type: 'Button', + props: { label: 'Go' }, + on: { + press: [ + { action: 'setState', params: { statePath: '/counter', value: 42 } }, + { + action: 'setState', + params: { statePath: '/copy', value: { $state: '/counter' } }, + }, + ], + }, + children: [], + }, + display: { type: 'Text', props: { content: { $state: '/copy' } }, children: [] }, + }, + state: { counter: 0, copy: 0 }, + }; + + const { getByTestId } = renderInteractive(); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(getByTestId('text-el').textContent).to.equal('42'); + }); + }); + + describe('built-in pushState', () => { + it('appends an item to a state array and re-renders the list', async () => { + const doc: Document = { + name: 'pushstate-test', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['btn', 'list'] }, + btn: { + type: 'Button', + props: { label: 'Add' }, + on: { + press: { + action: 'pushState', + params: { statePath: '/items', value: { id: '3', name: 'Charlie' } }, + }, + }, + children: [], + }, + list: { + type: 'Box', + props: {}, + repeat: { statePath: '/items', key: 'id' }, + children: ['item'], + }, + item: { type: 'Text', props: { content: { $item: 'name' } }, children: [] }, + }, + state: { + items: [ + { id: '1', name: 'Alice' }, + { id: '2', name: 'Bob' }, + ], + }, + }; + + const { getAllByTestId, getByTestId } = renderInteractive( + + ); + expect(getAllByTestId('text-el')).to.have.length(2); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(getAllByTestId('text-el')).to.have.length(3); + expect(getAllByTestId('text-el')[2].textContent).to.equal('Charlie'); + }); + }); + + describe('built-in removeState', () => { + it('removes the item at the given index from a state array', async () => { + const doc: Document = { + name: 'removestate-test', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['btn', 'list'] }, + btn: { + type: 'Button', + props: { label: 'Remove Middle' }, + on: { + press: { action: 'removeState', params: { statePath: '/items', index: 1 } }, + }, + children: [], + }, + list: { + type: 'Box', + props: {}, + repeat: { statePath: '/items', key: 'id' }, + children: ['item'], + }, + item: { type: 'Text', props: { content: { $item: 'name' } }, children: [] }, + }, + state: { + items: [ + { id: '1', name: 'Alice' }, + { id: '2', name: 'Bob' }, + { id: '3', name: 'Charlie' }, + ], + }, + }; + + const { getAllByTestId, getByTestId } = renderInteractive( + + ); + expect(getAllByTestId('text-el')).to.have.length(3); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + const remaining = getAllByTestId('text-el').map((n) => n.textContent); + expect(remaining).to.deep.equal(['Alice', 'Charlie']); + }); + + it('removes the first item when index is 0', async () => { + const doc: Document = { + name: 'removestate-first', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['btn', 'list'] }, + btn: { + type: 'Button', + props: { label: 'Remove First' }, + on: { + press: { action: 'removeState', params: { statePath: '/items', index: 0 } }, + }, + children: [], + }, + list: { + type: 'Box', + props: {}, + repeat: { statePath: '/items', key: 'id' }, + children: ['item'], + }, + item: { type: 'Text', props: { content: { $item: 'name' } }, children: [] }, + }, + state: { + items: [ + { id: '1', name: 'Alice' }, + { id: '2', name: 'Bob' }, + ], + }, + }; + + const { getAllByTestId, getByTestId } = renderInteractive( + + ); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + const remaining = getAllByTestId('text-el').map((n) => n.textContent); + expect(remaining).to.deep.equal(['Bob']); + }); + }); + + describe('built-in navigate', () => { + const navigateSpy = sinon.stub(); + + const navigateCatalog = defineAtomsCatalog({ + components: { + Button: { props: z.object({ label: z.string().optional() }), description: 'Button' }, + }, + actions: { + save: { params: z.object({}), description: 'Save and navigate' }, + }, + }); + + const navigateRegistry = defineAtomsRegistry(navigateCatalog, { + components: { Button: ButtonStub }, + actions: { save: async () => {} }, + }); + + const navigateConfig: AtomsConfig = { + catalog: navigateCatalog, + registry: navigateRegistry, + navigate: navigateSpy, + }; + + beforeEach(() => navigateSpy.resetHistory()); + + it('calls the navigate callback with the path from onSuccess after the action succeeds', async () => { + const doc: Document = { + name: 'navigate-test', + root: 'r', + elements: { + r: { + type: 'Button', + props: { label: 'Save' }, + on: { press: { action: 'save', onSuccess: { navigate: '/dashboard' } } }, + children: [], + }, + }, + }; + + const { getByTestId } = renderInProvider( + , + navigateConfig + ); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(navigateSpy.calledOnce).to.be.true; + expect(navigateSpy.firstCall.args[0]).to.equal('/dashboard'); + }); + + it('does not call navigate when onSuccess is not set', async () => { + const doc: Document = { + name: 'navigate-no-success', + root: 'r', + elements: { + r: { + type: 'Button', + props: { label: 'Save' }, + on: { press: { action: 'save' } }, + children: [], + }, + }, + }; + + const { getByTestId } = renderInProvider( + , + navigateConfig + ); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(navigateSpy.called).to.be.false; + }); + }); + + describe('custom action handlers', () => { + const greetSpy = sinon.stub(); + + const greetCatalog = defineAtomsCatalog({ + components: { + Text: { props: z.object({ content: z.string().optional() }), description: 'Text' }, + Box: { props: z.object({}), description: 'Box', slots: ['default'] }, + Button: { props: z.object({ label: z.string().optional() }), description: 'Button' }, + }, + actions: { + greet: { params: z.object({ name: z.string() }), description: 'Greet someone' }, + }, + }); + + const greetRegistry = defineAtomsRegistry(greetCatalog, { + components: { Text: TextStub, Box: BoxStub, Button: ButtonStub }, + actions: { greet: async (params) => greetSpy(params) }, + }); + + const greetConfig: AtomsConfig = { catalog: greetCatalog, registry: greetRegistry }; + + beforeEach(() => greetSpy.resetHistory()); + + it('calls the registered handler with the correct params', async () => { + const doc: Document = { + name: 'custom-action', + root: 'r', + elements: { + r: { + type: 'Button', + props: { label: 'Say Hello' }, + on: { press: { action: 'greet', params: { name: 'World' } } }, + children: [], + }, + }, + }; + + const { getByTestId } = renderInProvider( + , + greetConfig + ); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(greetSpy.calledOnce).to.be.true; + expect(greetSpy.firstCall.args[0]).to.deep.equal({ name: 'World' }); + }); + + it('resolves $state references in params before invoking the handler', async () => { + const doc: Document = { + name: 'action-state-params', + root: 'r', + elements: { + r: { + type: 'Button', + props: { label: 'Greet' }, + on: { press: { action: 'greet', params: { name: { $state: '/userName' } } } }, + children: [], + }, + }, + state: { userName: 'Alice' }, + }; + + const { getByTestId } = renderInProvider( + , + greetConfig + ); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(greetSpy.calledOnce).to.be.true; + expect(greetSpy.firstCall.args[0]).to.deep.equal({ name: 'Alice' }); + }); + + it('does not call the handler when no binding is set for the event', async () => { + const doc: Document = { + name: 'no-binding', + root: 'r', + elements: { + r: { type: 'Button', props: { label: 'Inert' }, children: [] }, + }, + }; + + const { getByTestId } = renderInProvider( + , + greetConfig + ); + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect(greetSpy.called).to.be.false; + }); + }); + }); + + // --------------------------------------------------------------------------- + // List rendering with repeat + // --------------------------------------------------------------------------- + + describe('list rendering with repeat', () => { + const renderList = (doc: Document) => + renderInProvider(, interactiveConfig); + + it('renders one child per item in the state array', () => { + const doc: Document = { + name: 'list-test', + root: 'list', + elements: { + list: { + type: 'Box', + props: {}, + repeat: { statePath: '/items', key: 'id' }, + children: ['item'], + }, + item: { type: 'Text', props: { content: { $item: 'name' } }, children: [] }, + }, + state: { + items: [ + { id: '1', name: 'Alice' }, + { id: '2', name: 'Bob' }, + { id: '3', name: 'Charlie' }, + ], + }, + }; + + const texts = renderList(doc) + .getAllByTestId('text-el') + .map((n) => n.textContent); + expect(texts).to.deep.equal(['Alice', 'Bob', 'Charlie']); + }); + + it('renders nothing when the state array is empty', () => { + const doc: Document = { + name: 'empty-list', + root: 'list', + elements: { + list: { + type: 'Box', + props: {}, + repeat: { statePath: '/items', key: 'id' }, + children: ['item'], + }, + item: { type: 'Text', props: { content: { $item: 'name' } }, children: [] }, + }, + state: { items: [] }, + }; + + const { container } = renderList(doc); + expect(container.querySelectorAll('[data-testid="text-el"]').length).to.equal(0); + }); + + it('filters items via a $item visibility condition on the child element', () => { + // Visibility on the CHILD (inside the repeat scope) filters individual items. + const doc: Document = { + name: 'filtered-list', + root: 'list', + elements: { + list: { + type: 'Box', + props: {}, + repeat: { statePath: '/tasks', key: 'id' }, + children: ['item'], + }, + item: { + type: 'Text', + props: { content: { $item: 'title' } }, + visible: { $item: 'active', eq: true }, + children: [], + }, + }, + state: { + tasks: [ + { id: '1', title: 'Buy groceries', active: true }, + { id: '2', title: 'Read book', active: false }, + { id: '3', title: 'Go running', active: true }, + ], + }, + }; + + const texts = renderList(doc) + .getAllByTestId('text-el') + .map((n) => n.textContent); + expect(texts).to.deep.equal(['Buy groceries', 'Go running']); + }); + }); + + // --------------------------------------------------------------------------- + // $bindState — two-way binding + // --------------------------------------------------------------------------- + + describe('$bindState — two-way binding', () => { + it('reflects the initial state value in the bound prop', () => { + const doc: Document = { + name: 'bindstate-initial', + root: 'r', + elements: { + r: { type: 'Checkbox', props: { checked: { $bindState: '/isChecked' } }, children: [] }, + }, + state: { isChecked: true }, + }; + + const { getByTestId } = renderInProvider( + , + bindConfig + ); + expect((getByTestId('checkbox-el') as HTMLInputElement).checked).to.be.true; + }); + + it('writes back to state when the component changes the bound value', async () => { + const doc: Document = { + name: 'bindstate-writeback', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['checkbox', 'display'] }, + checkbox: { + type: 'Checkbox', + props: { checked: { $bindState: '/isChecked' } }, + children: [], + }, + display: { + type: 'Text', + props: { + content: { $cond: { $state: '/isChecked' }, $then: 'checked', $else: 'unchecked' }, + }, + children: [], + }, + }, + state: { isChecked: false }, + }; + + const { getByTestId } = renderInProvider( + , + bindConfig + ); + expect(getByTestId('text-el').textContent).to.equal('unchecked'); + expect((getByTestId('checkbox-el') as HTMLInputElement).checked).to.be.false; + + await act(async () => { + fireEvent.click(getByTestId('checkbox-el')); + }); + + expect(getByTestId('text-el').textContent).to.equal('checked'); + expect((getByTestId('checkbox-el') as HTMLInputElement).checked).to.be.true; + }); + + it('reflects an external setState into the bound prop (read direction)', async () => { + const doc: Document = { + name: 'bindstate-external', + root: 'root', + elements: { + root: { type: 'Box', props: {}, children: ['btn', 'checkbox'] }, + btn: { + type: 'Button', + props: { label: 'Check' }, + on: { press: { action: 'setState', params: { statePath: '/isChecked', value: true } } }, + children: [], + }, + checkbox: { + type: 'Checkbox', + props: { checked: { $bindState: '/isChecked' } }, + children: [], + }, + }, + state: { isChecked: false }, + }; + + const { getByTestId } = renderInProvider( + , + bindWithButtonConfig + ); + expect((getByTestId('checkbox-el') as HTMLInputElement).checked).to.be.false; + + await act(async () => { + fireEvent.click(getByTestId('btn')); + }); + + expect((getByTestId('checkbox-el') as HTMLInputElement).checked).to.be.true; + }); + }); +}); + diff --git a/packages/react/src/components/DesignLibrary/StudioComponentWrapper.tsx b/packages/react/src/components/DesignLibrary/StudioComponentWrapper.tsx new file mode 100644 index 0000000000..505e385ef9 --- /dev/null +++ b/packages/react/src/components/DesignLibrary/StudioComponentWrapper.tsx @@ -0,0 +1,37 @@ +'use client'; +import React, { JSX, useMemo } from 'react'; +import { createNCC } from '../../atoms'; +import { useSitecore } from '../SitecoreProvider'; +import { Document } from '@sitecore-content-sdk/content/atoms'; + +/** + * Props accepted by the `StudioComponentWrapper` used to render a Studio component layout on the client. Expects a pre-fetched `document` containing the component layout data. + * @internal + */ +type StudioComponentWrapperProps = { + document: Document | null; +}; + +/** + * Client component that renders a pre-fetched Studio (NCC) component layout. + * + * Expects `document` to be provided (fetched server-side by + * `StudioComponentServerWrapper`, from Design Library document updates, or any other + * preview path that supplies a layout `Document`). Renders `null` when no layout + * is available. + * @param {StudioComponentWrapperProps} props component props + * @internal + */ +export const StudioComponentWrapper = (props: StudioComponentWrapperProps): JSX.Element | null => { + const { atomsConfig } = useSitecore(); + + const NCComponent = useMemo(() => { + if (!props.document || !atomsConfig) return null; + + return createNCC(props.document, atomsConfig.registry); + }, [props.document, atomsConfig]); + + if (!NCComponent) return null; + + return ; +}; diff --git a/packages/react/src/components/DesignLibrary/index.ts b/packages/react/src/components/DesignLibrary/index.ts index 6c9aef016c..84f5bda114 100644 --- a/packages/react/src/components/DesignLibrary/index.ts +++ b/packages/react/src/components/DesignLibrary/index.ts @@ -1,3 +1,4 @@ export { DesignLibrary } from './DesignLibrary'; +export { DesignLibraryLowCodeComponent } from './DesignLibraryLowCodeComponent'; export { DesignLibraryErrorBoundary } from './DesignLibraryErrorBoundary'; export { DynamicComponent, ImportMapImport } from './models'; diff --git a/packages/react/src/components/Placeholder/placeholder-utils.test.tsx b/packages/react/src/components/Placeholder/placeholder-utils.test.tsx index 935484453e..9e242c41a5 100644 --- a/packages/react/src/components/Placeholder/placeholder-utils.test.tsx +++ b/packages/react/src/components/Placeholder/placeholder-utils.test.tsx @@ -183,7 +183,7 @@ describe('placeholder-utils', () => { }); it('should extract styles from DetailedRenderingParams object', () => { - const rendering = ({ + const rendering = { componentName: 'TestComponent', uid: 'test-uid', params: { @@ -191,7 +191,7 @@ describe('placeholder-utils', () => { Value: { value: 'White-Background' }, }, }, - } as unknown) as ComponentRendering; + } as unknown as ComponentRendering; const result = getSXAParams(rendering); @@ -201,14 +201,14 @@ describe('placeholder-utils', () => { }); it('should combine object GridParameters and Styles params', () => { - const rendering = ({ + const rendering = { componentName: 'TestComponent', uid: 'test-uid', params: { GridParameters: { Value: { value: 'col-lg-6' } }, Styles: { Value: { value: 'White-Background' } }, }, - } as unknown) as ComponentRendering; + } as unknown as ComponentRendering; const result = getSXAParams(rendering); @@ -216,7 +216,6 @@ describe('placeholder-utils', () => { styles: 'col-lg-6 White-Background', }); }); - }); describe('getChildComponentProps', () => { @@ -384,6 +383,40 @@ describe('placeholder-utils', () => { expect(consoleWarnStub.calledOnce).to.be.true; }); + it('should return StudioComponentServerWrapper when ComponentRef is in params', () => { + const rendering: ComponentRendering = { + componentName: 'Sample', + uid: 'test-uid', + params: { + ComponentRef: 'api/media/v2/delivery/abc/component/def/default', + fieldNames: 'default', + }, + }; + + const result = getComponentForRendering(rendering, 'test-placeholder', componentMap); + + expect(result?.component).to.be.a('function'); + expect(result?.isEmpty).to.be.false; + expect(result?.componentType).to.equal('server'); + }); + + it('should return StudioComponentServerWrapper when ComponentRef is in params without fieldNames', () => { + const rendering: ComponentRendering = { + componentName: 'Sample', + uid: 'test-uid', + params: { + ComponentRef: + 'api/media/v2/delivery/abc/component/def/default|api/media/v2/delivery/abc/component/def/sample', + }, + }; + + const result = getComponentForRendering(rendering, 'test-placeholder', componentMap); + + expect(result?.component).to.be.a('function'); + expect(result?.isEmpty).to.be.false; + expect(result?.componentType).to.equal('server'); + }); + it('should return null when componentMap is not provided', () => { const rendering: ComponentRendering = { componentName: 'TestComponent', diff --git a/packages/react/src/components/Placeholder/placeholder-utils.tsx b/packages/react/src/components/Placeholder/placeholder-utils.tsx index ae3bcb8310..b352827b18 100644 --- a/packages/react/src/components/Placeholder/placeholder-utils.tsx +++ b/packages/react/src/components/Placeholder/placeholder-utils.tsx @@ -20,6 +20,7 @@ import { FEAAS_COMPONENT_RENDERING_NAME, FEAAS_WRAPPER_RENDERING_NAME, } from '../FEaaS'; +import { StudioComponentServerWrapper } from '../DesignLibrary/StudioComponentServerWrapper'; import { ChildComponentProps, PlaceholderProps, ComponentForRendering } from './models'; /** @@ -169,6 +170,19 @@ export const getComponentForRendering = ( }; } + if (renderingDefinition.params?.ComponentRef) { + return { + component: (props: ChildComponentProps) => ( + + ), + isEmpty: false, + componentType: 'server', + }; + } + let component = null; if (!componentMap || componentMap.size === 0) { console.warn( @@ -251,4 +265,3 @@ export const getComponentForRendering = ( isEmpty: false, }; }; - diff --git a/packages/react/src/components/SitecoreProvider.test.tsx b/packages/react/src/components/SitecoreProvider.test.tsx index 1527ccaea6..5d750828a0 100644 --- a/packages/react/src/components/SitecoreProvider.test.tsx +++ b/packages/react/src/components/SitecoreProvider.test.tsx @@ -6,6 +6,7 @@ import { SitecoreProvider, useSitecore } from './SitecoreProvider'; import { WithSitecoreProps, withSitecore } from '../enhancers/withSitecore'; import { LayoutServiceData, LayoutServicePageState } from '../index'; import { render } from '@testing-library/react'; +import type { ImportMapImport } from './DesignLibrary/models'; describe('SitecoreProvider', () => { let nestedContext = {}; @@ -14,12 +15,20 @@ describe('SitecoreProvider', () => { anotherProperty?: string; } + const loadImportMapStub = async (): Promise => ({} as ImportMapImport); + const NestedComponent: FC = () => { const { page } = useSitecore(); nestedContext = page; return Page mode is {page.mode.name}; }; + const AtomRegistryProbe: FC = () => { + const { atomsConfig } = useSitecore(); + nestedContext = atomsConfig as unknown; + return probe; + }; + const NestedComponentWithContext = withSitecore()(NestedComponent); const components = new Map(); @@ -58,7 +67,12 @@ describe('SitecoreProvider', () => { it('renders the component with the context', () => { const rendered = render( - + ); @@ -69,7 +83,12 @@ describe('SitecoreProvider', () => { it('updates state when new page is received via props', () => { const rendered = render( - + ); @@ -82,7 +101,12 @@ describe('SitecoreProvider', () => { }; rendered.rerender( - + ); @@ -92,4 +116,26 @@ describe('SitecoreProvider', () => { locale: 'gr', }); }); + + it('exposes atomsConfig on context when provided', () => { + nestedContext = undefined; + const atomsConfig = { + catalog: {} as any, + registry: {} as any, + }; + + render( + + + + ); + + expect(nestedContext).to.deep.equal(atomsConfig); + }); }); diff --git a/packages/react/src/components/SitecoreProvider.tsx b/packages/react/src/components/SitecoreProvider.tsx index f3a529b7df..60822c7541 100644 --- a/packages/react/src/components/SitecoreProvider.tsx +++ b/packages/react/src/components/SitecoreProvider.tsx @@ -5,6 +5,7 @@ import { Page } from '@sitecore-content-sdk/content/client'; import { SitecoreConfig } from '@sitecore-content-sdk/content/config'; import { ComponentMap } from './sharedTypes'; import { ImportMapImport } from './DesignLibrary/models'; +import type { AtomsConfig } from '../atoms/types'; export interface SitecoreProviderProps { /** @@ -23,6 +24,11 @@ export interface SitecoreProviderProps { * The dynamic import for import map to be used in variant generation mode. */ loadImportMap: () => Promise; + /** + * Atoms configuration: catalog and registry for rendering low-code components. + * Pass the catalog from defineAtomsCatalog and the registry result from defineAtomsRegistry. + */ + atomsConfig?: AtomsConfig; children: React.ReactNode; } @@ -46,6 +52,10 @@ export interface SitecoreProviderState { * The dynamic import for import map to be used in variant generation mode. */ loadImportMap: () => Promise; + /** + * Atoms runtime: catalog and registry for rendering low-code components. + */ + atomsConfig?: AtomsConfig; /** * The component map to use for rendering components. */ @@ -88,12 +98,13 @@ export const ImportMapReactContext = React.createContext< * @param {SitecoreProviderProps['page']} props.page - The page data. * @param {SitecoreProviderProps['componentMap']} props.componentMap - The component map. * @param {SitecoreProviderProps['loadImportMap']} props.loadImportMap - The function to load the import map. + * @param {SitecoreProviderProps['atomsConfig']} props.atomsConfig - Atoms config (catalog + registry) for rendering low-code components. * @param {React.ReactNode} props.children - The children to render. * @returns {React.ReactNode} The SitecoreProvider component. * @public */ export const SitecoreProvider = (props: SitecoreProviderProps) => { - const { api, page: propsPage, componentMap, loadImportMap, children } = props; + const { api, page: propsPage, componentMap, loadImportMap, atomsConfig, children } = props; const [page, setPageInternal] = useState(propsPage); @@ -117,8 +128,9 @@ export const SitecoreProvider = (props: SitecoreProviderProps) => { api, componentMap, loadImportMap, + atomsConfig, }), - [page, setPage, api, componentMap, loadImportMap] + [page, setPage, api, componentMap, loadImportMap, atomsConfig] ); return ( diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 2fb0890c7d..82781318a8 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -81,6 +81,7 @@ export { } from './components/FEaaS'; export { DesignLibrary, + DesignLibraryLowCodeComponent, DesignLibraryErrorBoundary, DynamicComponent, ImportMapImport, @@ -109,3 +110,29 @@ export { } from './components/DefaultEmptyFieldEditingComponents'; export { ClientEditingChromesUpdate } from './components/ClientEditingChromesUpdate'; export { SitePathService, SitePathServiceConfig } from '@sitecore-content-sdk/content/site'; +export { + useBoundProp, + defineAtomsCatalog, + defineAtomsRegistry, + withPropMeta, + type PropMeta, + type AtomComponentDefinition, + type AtomActionDefinition, + type AtomsCatalogInput, + type AtomsComponentsMap, + type AtomActionHandler, + type AtomsActionsMap, + type AtomsConfig, + textFieldSchema, + richTextFieldSchema, + dateFieldSchema, + linkFieldSchema, + imageFieldSchema, + fileFieldSchema, + type TextFieldSchema, + type RichTextFieldSchema, + type DateFieldSchema, + type LinkFieldSchema, + type ImageFieldSchema, + type FileFieldSchema, +} from './atoms'; diff --git a/packages/react/src/test-data/atom-component-layouts.ts b/packages/react/src/test-data/atom-component-layouts.ts new file mode 100644 index 0000000000..885c13a6e8 --- /dev/null +++ b/packages/react/src/test-data/atom-component-layouts.ts @@ -0,0 +1,420 @@ +import { Document } from '@sitecore-content-sdk/content/types/atoms'; + +export const productPuicker: Document = { + name: 'ProductPickerPreset', + root: { + id: 'ae262910-c132-431f-a488-d18af9c39e43', + type: 'Stack', + version: 2, + children: [ + { + id: 'eead1514-d55c-4113-8d73-f6e51cce1c01', + type: 'Select', + bindings: { + value: { + bindType: 'expression', + value: '{{state.category}}', + }, + onValueChange: { + bindType: 'event', + arguments: ['value', 'label'], + actions: [ + { + setState: { + category: '{{value}}', + }, + }, + { + call: 'trackSelection', + args: ['{{value}}', '{{label}}'], + }, + ], + }, + }, + children: [ + { + id: 'e0abec67-6c9e-442a-81e5-acc86b4eaffb', + type: 'SelectTrigger', + children: [ + { + id: '80f907ac-ff71-409d-b39a-327e7833814c', + type: 'SelectValue', + staticProps: { + placeholder: 'Select a category', + }, + }, + ], + }, + { + id: '8b11ed97-2af1-4bdf-aaa9-cd3dadc4cd4a', + type: 'SelectContent', + children: [ + { + id: '168db3c5-ce62-4bcc-8044-3c199fd761a3', + type: 'SelectItem', + show: { + and: [ + { + left: '{{props.categories.length}}', + op: 'eq', + right: '{{state.selectedCategories.count}}', + }, + { + left: '{{item.value}}', + op: 'eq', + right: '{{state.selectedCategory}}', + }, + ], + }, + for: { + each: '{{props.categories}}', + as: 'item', + key: '{{item.value}}', + }, + staticProps: { + classname: 'color-balck', + }, + bindings: { + value: { + bindType: 'expression', + value: '{{item.value}}', + }, + }, + children: ['{{item.label}}'], + }, + ], + }, + ], + }, + { + id: '966bf556-04ff-4112-a8ae-00a071606cd4', + type: 'Image', + staticProps: { + alt: 'alt', + width: 300, + }, + bindings: { + src: { + bindType: 'expression', + value: '{{props.imagesByCategory[state.category]}}', + }, + onValueChange: { + bindType: 'event', + arguments: ['event', 'foo'], + actions: [ + { + setState: { + category: '{{event.target.value}}', + }, + }, + { + call: 'analyticsTrack', + args: [ + '{{event.target.value}}', + 'category-selected', + '{{props.imagesByCategory[state.category]}}', + ], + }, + ], + }, + }, + }, + ], + }, + props: { + categories: [ + { + value: 'hats', + label: 'Hats', + }, + { + value: 'shoes', + label: 'Shoes', + }, + { + value: 'bags', + label: 'Bags', + }, + ], + imagesByCategory: { + hats: '/images/hats.png', + shoes: '/images/shoes.png', + bags: '/images/bags.png', + }, + }, + state: {}, +}; + +export const cardsWithDataBinding: Document = { + name: 'CardsPresetWithDataBinding', + root: { + id: '5112db36-b362-4be5-8b78-5f2f21ad6c77', + type: 'Stack', + staticProps: { + gap: 3, + }, + children: [ + { + id: 'd10f1255-412e-4b40-9e8e-0b0643b439d4', + type: 'Card', + for: { + each: '{{props.Teaser}}', + as: 'item', + }, + children: [ + { + id: '7237936e-9cf0-43b7-9445-b10f3b32b94c', + type: 'CardHeader', + children: [ + { + id: '15ff0525-c3d6-4d24-994b-78c11ef19a15', + type: 'CardTitle', + children: ['{{item.title}}'], + }, + { + id: '442df2cc-3aa9-4a0d-9221-f70dbf045ac6', + type: 'CardDescription', + children: ['{{item.description}}'], + }, + ], + }, + { + id: 'bfb8aa23-b397-48ea-930f-190488963b77', + type: 'CardContent', + children: [ + { + id: 'aad40af8-14f6-4752-8310-6eb4bc52b619', + type: 'Image', + staticProps: { + width: 300, + height: 200, + }, + bindings: { + src: { + bindType: 'expression', + value: '{{item.image}}', + }, + alt: { + bindType: 'expression', + value: '{{item.title}}', + }, + }, + }, + ], + }, + ], + }, + ], + }, + props: { + Link_list_intro: { + Title: 'Learn more about SaaS content management', + }, + Teaser: [ + { + badge: 'Article', + button_label: 'Read the article', + button_URL: + 'https://www.sitecore.com/blog/cloud/what-is-cloud-native-saas?utm_websource=products.xm-cloud', + description: + 'Grasping the benefits and differences between SaaS, cloud-computing, cloud-native, and cloud-hosted is important in determining which technology has the scalability you need — and should expect — to support your long-term growth.', + image: + 'https://sitecorecontenthub.stylelabs.cloud/api/public/content/b4ee038a89874af1838812bfd47c3c7c?v=67c7884d', + title: 'SaaS, cloud computing, and cloud-native development — unravel the difference', + }, + { + badge: 'Article', + button_label: 'Read the article', + button_URL: + 'https://www.sitecore.com/knowledge-center/digital-marketing-resources/why-saas?utm_websource=products.xm-cloud', + description: + 'Investing in a SaaS platform can provide benefits for your business, your internal teams, and your customers. Whatever your unique needs, we can empower you to create the experiences that drive competitive advantage and deliver value.', + image: + 'https://sitecorecontenthub.stylelabs.cloud/api/public/content/e253b03b15dd4cd49f84acefcdeef95a?v=3d1436cd', + title: "What's the big deal with a SaaS CMS?", + }, + { + badge: 'Article', + button_label: 'Read the article', + button_URL: + 'https://www.sitecore.com/knowledge-center/digital-marketing-resources/what-is-cloud-content-management?utm_websource=products.xm-cloud', + description: + "Explore cloud-based content management — including its definition, history, benefits, and how to determine when it's right for your organization.", + image: + 'https://sitecorecontenthub.stylelabs.cloud/api/public/content/3a4e4216497c4edb833954241d10bf01?v=93a3d391', + title: 'Why is everyone moving their content to the cloud?', + }, + ], + }, +}; + +export const accordionWithCards: Document = { + name: 'AccordionPreset', + root: { + id: '87a7cbaa-76fb-4086-bbe7-b8f43ea35eb8', + type: 'Accordion', + staticProps: { + type: 'single', + collapsible: true, + }, + children: [ + { + id: 'b1df2e76-c2b5-4420-8ee3-d60127ba3553', + type: 'AccordionItem', + staticProps: { + value: 'item-1', + }, + children: [ + { + id: 'a13e9646-f8f2-4519-a92a-9bf15cbd933c', + type: 'AccordionTrigger', + children: ['Section One'], + }, + { + id: '0122d596-9b63-4ac7-9a3b-be0401962b9e', + type: 'AccordionContent', + children: [ + { + id: '053ca280-5df8-4a77-823b-c10774b56b77', + type: 'Card', + children: [ + { + id: 'f2396b45-1620-4136-9bed-11e9566c8407', + type: 'CardHeader', + children: [ + { + id: '33e3041d-7d75-4e04-a2f0-571020be9791', + type: 'CardTitle', + children: ['First'], + }, + ], + }, + { + id: 'bb33fdb9-c9ad-444f-bd25-0314c1b0202c', + type: 'CardContent', + children: [ + { + id: 'b5e32073-e254-4904-82fd-07718c5761b0', + type: 'Button', + children: ['Click'], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + id: '0444d9e6-fee6-4b5f-862e-2b9488a2fdee', + type: 'AccordionItem', + staticProps: { + value: 'item-2', + }, + children: [ + { + id: '63479846-0e0e-430a-94d3-fb88c07cc13c', + type: 'AccordionTrigger', + children: ['Section Two'], + }, + { + id: 'a3a1a342-a913-45af-866f-3958df717ec8', + type: 'AccordionContent', + children: [ + { + id: 'ce73c89a-e9df-4295-b7d5-ee58f3f8b938', + type: 'Card', + children: [ + { + id: 'a43af69f-57bd-4916-bcf4-49b1092a315c', + type: 'CardHeader', + children: [ + { + id: 'c3283b16-9c95-4820-aec8-ed481cff6a58', + type: 'CardTitle', + children: ['Second'], + }, + ], + }, + { + id: 'b6b20825-464b-423f-8062-45379c687ba8', + type: 'CardContent', + children: [ + { + id: '5f70bed9-91e9-44d6-bba3-63974f95bb8a', + type: 'Button', + children: ['Open'], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, +}; + +export const cardPreset: Document = { + name: 'CardPreset', + root: { + id: '60ed1306-fda6-4e6b-974d-55746f516e37', + type: 'Card', + children: [ + { + id: 'f3b22759-8dd0-4d2c-a295-5ce5069ee771', + type: 'CardHeader', + children: [ + { + id: '5563c8c8-c88f-4357-a0b3-0520a3dcce37', + type: 'CardTitle', + children: ['Profile'], + }, + { + id: 'e51bcfe3-04d1-4ccd-98e7-cd56d97de021', + type: 'CardDescription', + children: ['Manage your profile settings'], + }, + ], + }, + { + id: 'a2437056-2155-4a38-8479-d6aaeb05e33f', + type: 'CardContent', + children: [ + { + id: 'e4d5dba0-2b67-4093-914b-ce83ec7e2c64', + type: 'Input', + staticProps: { + placeholder: 'Your name', + }, + }, + { + id: '3111c1c6-c6eb-422a-8665-077247204a11', + type: 'Button', + children: ['Save'], + }, + ], + }, + { + id: '63accf88-805f-42df-a17a-83f0b4a052aa', + type: 'CardFooter', + children: [ + { + id: 'b3928606-1e64-44b8-ab66-b69f28354787', + type: 'Button', + staticProps: { + variant: 'outline', + }, + children: ['Cancel'], + }, + ], + }, + ], + }, + state: { + name: '', + }, +}; diff --git a/packages/react/src/tests/jsdom-setup.ts b/packages/react/src/tests/jsdom-setup.ts index ea23ec0c31..db25593d6a 100644 --- a/packages/react/src/tests/jsdom-setup.ts +++ b/packages/react/src/tests/jsdom-setup.ts @@ -36,3 +36,10 @@ global.jsdom = jsdom; global.HTMLElement = jsDomWindow.HTMLElement; // makes chai "happy" https://github.com/chaijs/chai/issues/1029 copyProps(jsDomWindow, global); + +// jsdom does not implement requestAnimationFrame; provide a minimal stub +if (typeof global.requestAnimationFrame === 'undefined') { + global.requestAnimationFrame = (cb: FrameRequestCallback) => + setTimeout(cb, 0) as unknown as number; + global.cancelAnimationFrame = (id: number) => clearTimeout(id); +} diff --git a/packages/search/package.json b/packages/search/package.json index ab0c2b61a4..7d49a49aa0 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-content-sdk/search", - "version": "0.3.0", + "version": "0.3.0-beta.1", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -36,8 +36,8 @@ "url": "https://github.com/sitecore/content-sdk/issues" }, "dependencies": { - "@sitecore-content-sdk/analytics-core": "^2.1.0", - "@sitecore-content-sdk/core": "^2.1.0" + "@sitecore-content-sdk/analytics-core": "2.1.0-beta.1", + "@sitecore-content-sdk/core": "2.1.0-beta.1" }, "devDependencies": { "@types/chai": "^5.2.3", diff --git a/yarn.lock b/yarn.lock index 71856f2847..78531180f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -710,12 +710,12 @@ __metadata: linkType: hard "@emnapi/core@npm:^1.1.0": - version: 1.11.0 - resolution: "@emnapi/core@npm:1.11.0" + version: 1.11.1 + resolution: "@emnapi/core@npm:1.11.1" dependencies: "@emnapi/wasi-threads": "npm:1.2.2" tslib: "npm:^2.4.0" - checksum: 10/7c6f7fe38dd16f98d0081d58e23a6184fccffaba6bef113fa4f689478b673c988cb8dd1a93d0d66dfc7bb487d67837827ac0bd1a578fffe4c7db2ba07ae39c78 + checksum: 10/9aba37e0c11a75ef8372fd0a9c6e5396f4e8c1ebdd6fee737414787610a9dc1cd9bf188f525153561ca9363896e1135dd240f1ce28f3470dba3ad7e683e6db1a languageName: node linkType: hard @@ -738,11 +738,11 @@ __metadata: linkType: hard "@emnapi/runtime@npm:^1.1.0, @emnapi/runtime@npm:^1.7.0": - version: 1.11.0 - resolution: "@emnapi/runtime@npm:1.11.0" + version: 1.11.1 + resolution: "@emnapi/runtime@npm:1.11.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10/c0f064646456d836c72cbc71baa9fe128be011ffb3e2ccbffa624cfe77d6b66aab3aa6635fbbab34a187a16d4833ef14016fcef2b189753aaa8f987843e0fc26 + checksum: 10/8f7c622a49314df4d07952110e108e83b0fe129a8ddb9ef1e0ae372d754616169d5b0dd47a0d354a0fea2612abe42cedb582d15916936d1320c6c468acc804cc languageName: node linkType: hard @@ -813,9 +813,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/aix-ppc64@npm:0.28.0" +"@esbuild/aix-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/aix-ppc64@npm:0.28.1" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard @@ -827,9 +827,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/android-arm64@npm:0.28.0" +"@esbuild/android-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm64@npm:0.28.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -841,9 +841,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/android-arm@npm:0.28.0" +"@esbuild/android-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm@npm:0.28.1" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -855,9 +855,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/android-x64@npm:0.28.0" +"@esbuild/android-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-x64@npm:0.28.1" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -869,9 +869,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/darwin-arm64@npm:0.28.0" +"@esbuild/darwin-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-arm64@npm:0.28.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -883,9 +883,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/darwin-x64@npm:0.28.0" +"@esbuild/darwin-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-x64@npm:0.28.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -897,9 +897,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/freebsd-arm64@npm:0.28.0" +"@esbuild/freebsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-arm64@npm:0.28.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -911,9 +911,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/freebsd-x64@npm:0.28.0" +"@esbuild/freebsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-x64@npm:0.28.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -925,9 +925,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-arm64@npm:0.28.0" +"@esbuild/linux-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm64@npm:0.28.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -939,9 +939,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-arm@npm:0.28.0" +"@esbuild/linux-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm@npm:0.28.1" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -953,9 +953,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-ia32@npm:0.28.0" +"@esbuild/linux-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ia32@npm:0.28.1" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -967,9 +967,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-loong64@npm:0.28.0" +"@esbuild/linux-loong64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-loong64@npm:0.28.1" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -981,9 +981,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-mips64el@npm:0.28.0" +"@esbuild/linux-mips64el@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-mips64el@npm:0.28.1" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -995,9 +995,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-ppc64@npm:0.28.0" +"@esbuild/linux-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ppc64@npm:0.28.1" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -1009,9 +1009,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-riscv64@npm:0.28.0" +"@esbuild/linux-riscv64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-riscv64@npm:0.28.1" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -1023,9 +1023,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-s390x@npm:0.28.0" +"@esbuild/linux-s390x@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-s390x@npm:0.28.1" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -1037,9 +1037,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-x64@npm:0.28.0" +"@esbuild/linux-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-x64@npm:0.28.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -1051,9 +1051,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/netbsd-arm64@npm:0.28.0" +"@esbuild/netbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-arm64@npm:0.28.1" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard @@ -1065,9 +1065,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/netbsd-x64@npm:0.28.0" +"@esbuild/netbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-x64@npm:0.28.1" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -1079,9 +1079,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/openbsd-arm64@npm:0.28.0" +"@esbuild/openbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-arm64@npm:0.28.1" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard @@ -1093,9 +1093,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/openbsd-x64@npm:0.28.0" +"@esbuild/openbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-x64@npm:0.28.1" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -1107,9 +1107,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/openharmony-arm64@npm:0.28.0" +"@esbuild/openharmony-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openharmony-arm64@npm:0.28.1" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard @@ -1121,9 +1121,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/sunos-x64@npm:0.28.0" +"@esbuild/sunos-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/sunos-x64@npm:0.28.1" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -1135,9 +1135,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/win32-arm64@npm:0.28.0" +"@esbuild/win32-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-arm64@npm:0.28.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -1149,9 +1149,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/win32-ia32@npm:0.28.0" +"@esbuild/win32-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-ia32@npm:0.28.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -1163,9 +1163,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/win32-x64@npm:0.28.0" +"@esbuild/win32-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-x64@npm:0.28.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -2296,6 +2296,28 @@ __metadata: languageName: node linkType: hard +"@json-render/core@npm:0.19.0": + version: 0.19.0 + resolution: "@json-render/core@npm:0.19.0" + dependencies: + zod: "npm:^4.3.6" + peerDependencies: + zod: ^4.0.0 + checksum: 10/fa6f9733b5275d5e0c4f6fad4c7271e49e962b1e9a18af60b549767404d555795b4d7159c19cb6781e4ea535df9a6dc1d83391d69eacf22e36b04fd44d5c6ae5 + languageName: node + linkType: hard + +"@json-render/react@npm:0.19.0": + version: 0.19.0 + resolution: "@json-render/react@npm:0.19.0" + dependencies: + "@json-render/core": "npm:0.19.0" + peerDependencies: + react: ^19.2.3 + checksum: 10/52f53740204521f04749fb93b6aa4e7181387baab9588c213781017e2345e8ff1711074b2720c9603a43103864b3c128a1c96a089d6132c6d71d50c3817821a9 + languageName: node + linkType: hard + "@manypkg/find-root@npm:^1.1.0": version: 1.1.0 resolution: "@manypkg/find-root@npm:1.1.0" @@ -2334,8 +2356,8 @@ __metadata: linkType: hard "@microsoft/api-extractor@npm:^7.55.0": - version: 7.58.7 - resolution: "@microsoft/api-extractor@npm:7.58.7" + version: 7.58.9 + resolution: "@microsoft/api-extractor@npm:7.58.9" dependencies: "@microsoft/api-extractor-model": "npm:7.33.8" "@microsoft/tsdoc": "npm:~0.16.0" @@ -2343,7 +2365,7 @@ __metadata: "@rushstack/node-core-library": "npm:5.23.1" "@rushstack/rig-package": "npm:0.7.3" "@rushstack/terminal": "npm:0.24.0" - "@rushstack/ts-command-line": "npm:5.3.9" + "@rushstack/ts-command-line": "npm:5.3.10" diff: "npm:~8.0.2" minimatch: "npm:10.2.3" resolve: "npm:~1.22.1" @@ -2352,7 +2374,7 @@ __metadata: typescript: "npm:5.9.3" bin: api-extractor: bin/api-extractor - checksum: 10/a9eaa48119aee851a921b85cdb3acb200ef010d6e3a0fb924fc8c99537a15af059ef126db4883718879d7e3d31d40f990517d7d163ca6f0938524dcc181f0e4a + checksum: 10/30c3a10834074308ce82fc3a78694e4c173bfd8783ac32e2779ca2c785e83e4be81e95ee421948d730eb83b5ce86184c59367d9cc3367832dd3cd61be1209f24 languageName: node linkType: hard @@ -2401,21 +2423,21 @@ __metadata: linkType: hard "@napi-rs/wasm-runtime@npm:^1.1.4": - version: 1.1.4 - resolution: "@napi-rs/wasm-runtime@npm:1.1.4" + version: 1.1.5 + resolution: "@napi-rs/wasm-runtime@npm:1.1.5" dependencies: - "@tybys/wasm-util": "npm:^0.10.1" + "@tybys/wasm-util": "npm:^0.10.2" peerDependencies: "@emnapi/core": ^1.7.1 "@emnapi/runtime": ^1.7.1 - checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af + checksum: 10/57a4b68f05f15b79bf45240ac173d3eaf72620d1b73261e7db407aa7ba8eb68e670fb1612d2ceef6b8cc500970a5ed6995c71c77661027971012ed2459ce307f languageName: node linkType: hard -"@next/env@npm:16.2.7": - version: 16.2.7 - resolution: "@next/env@npm:16.2.7" - checksum: 10/2db7b07fc9b2e5c8bd9b88f6a470f7d6c3b471b0dfcf12b9928ba9635aa98afaf7fd809d37bcd2ec5fb3a8f1a415073735f738533dcb9d7e67a4806fb7f2cb4f +"@next/env@npm:16.2.9": + version: 16.2.9 + resolution: "@next/env@npm:16.2.9" + checksum: 10/5cb54e8a956dbeb72eb5db966a7280fdc08693f4ffbd98c425a43f4d03abfa3c66868c1035b3203572990bdcd46d385eb8edcf41ee31dde5359743264001bce9 languageName: node linkType: hard @@ -2428,58 +2450,58 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-darwin-arm64@npm:16.2.7" +"@next/swc-darwin-arm64@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-darwin-arm64@npm:16.2.9" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-darwin-x64@npm:16.2.7" +"@next/swc-darwin-x64@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-darwin-x64@npm:16.2.9" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-linux-arm64-gnu@npm:16.2.7" +"@next/swc-linux-arm64-gnu@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-linux-arm64-gnu@npm:16.2.9" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-linux-arm64-musl@npm:16.2.7" +"@next/swc-linux-arm64-musl@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-linux-arm64-musl@npm:16.2.9" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-linux-x64-gnu@npm:16.2.7" +"@next/swc-linux-x64-gnu@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-linux-x64-gnu@npm:16.2.9" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-linux-x64-musl@npm:16.2.7" +"@next/swc-linux-x64-musl@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-linux-x64-musl@npm:16.2.9" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-win32-arm64-msvc@npm:16.2.7" +"@next/swc-win32-arm64-msvc@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-win32-arm64-msvc@npm:16.2.9" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:16.2.7": - version: 16.2.7 - resolution: "@next/swc-win32-x64-msvc@npm:16.2.7" +"@next/swc-win32-x64-msvc@npm:16.2.9": + version: 16.2.9 + resolution: "@next/swc-win32-x64-msvc@npm:16.2.9" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3073,177 +3095,177 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.61.1" +"@rollup/rollup-android-arm-eabi@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.62.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-android-arm64@npm:4.61.1" +"@rollup/rollup-android-arm64@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-android-arm64@npm:4.62.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-darwin-arm64@npm:4.61.1" +"@rollup/rollup-darwin-arm64@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.62.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-darwin-x64@npm:4.61.1" +"@rollup/rollup-darwin-x64@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.62.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.61.1" +"@rollup/rollup-freebsd-arm64@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.62.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-freebsd-x64@npm:4.61.1" +"@rollup/rollup-freebsd-x64@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.62.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.61.1" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.62.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.61.1" +"@rollup/rollup-linux-arm-musleabihf@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.62.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.61.1" +"@rollup/rollup-linux-arm64-gnu@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.62.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.61.1" +"@rollup/rollup-linux-arm64-musl@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.62.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loong64-gnu@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.61.1" +"@rollup/rollup-linux-loong64-gnu@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.62.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-loong64-musl@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-loong64-musl@npm:4.61.1" +"@rollup/rollup-linux-loong64-musl@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.62.0" conditions: os=linux & cpu=loong64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.61.1" +"@rollup/rollup-linux-ppc64-gnu@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.62.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-musl@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.61.1" +"@rollup/rollup-linux-ppc64-musl@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.62.0" conditions: os=linux & cpu=ppc64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.61.1" +"@rollup/rollup-linux-riscv64-gnu@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.62.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.61.1" +"@rollup/rollup-linux-riscv64-musl@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.62.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.61.1" +"@rollup/rollup-linux-s390x-gnu@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.62.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.61.1" +"@rollup/rollup-linux-x64-gnu@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.62.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.61.1" +"@rollup/rollup-linux-x64-musl@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.62.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-openbsd-x64@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-openbsd-x64@npm:4.61.1" +"@rollup/rollup-openbsd-x64@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-openbsd-x64@npm:4.62.0" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-openharmony-arm64@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-openharmony-arm64@npm:4.61.1" +"@rollup/rollup-openharmony-arm64@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.62.0" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.61.1" +"@rollup/rollup-win32-arm64-msvc@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.62.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.61.1" +"@rollup/rollup-win32-ia32-msvc@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.62.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-gnu@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-win32-x64-gnu@npm:4.61.1" +"@rollup/rollup-win32-x64-gnu@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.62.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.61.1": - version: 4.61.1 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.61.1" +"@rollup/rollup-win32-x64-msvc@npm:4.62.0": + version: 4.62.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.62.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3321,15 +3343,15 @@ __metadata: languageName: node linkType: hard -"@rushstack/ts-command-line@npm:5.3.9": - version: 5.3.9 - resolution: "@rushstack/ts-command-line@npm:5.3.9" +"@rushstack/ts-command-line@npm:5.3.10": + version: 5.3.10 + resolution: "@rushstack/ts-command-line@npm:5.3.10" dependencies: "@rushstack/terminal": "npm:0.24.0" "@types/argparse": "npm:1.0.38" argparse: "npm:~1.0.9" string-argv: "npm:~0.3.1" - checksum: 10/54c9bef6db8a06de0b10ea12148d94425458828833b5f46a43b9a78fb28a533abe1d636afe5355d4081b717869021b04f74f90f5a3837dee4c20c0af9eb0ef58 + checksum: 10/669da3f5ee4b17f2aac08cabb1386050436d14f8a7af78d1a2013ed801552c526f3037d3370c3940b33fbe55d4bc24c9b5b84e140b250e067a545655d5c2104a languageName: node linkType: hard @@ -3510,12 +3532,12 @@ __metadata: languageName: node linkType: hard -"@sitecore-content-sdk/analytics-core@npm:^2.1.0, @sitecore-content-sdk/analytics-core@workspace:packages/analytics-core": +"@sitecore-content-sdk/analytics-core@npm:2.1.0-beta.1, @sitecore-content-sdk/analytics-core@workspace:packages/analytics-core": version: 0.0.0-use.local resolution: "@sitecore-content-sdk/analytics-core@workspace:packages/analytics-core" dependencies: "@jest/types": "npm:^29.6.3" - "@sitecore-content-sdk/core": "npm:^2.1.0" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" "@stylistic/eslint-plugin": "npm:^5.2.2" "@types/debug": "npm:^4.1.12" "@types/jest": "npm:^29.5.12" @@ -3538,12 +3560,43 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-content-sdk/cli@npm:^2.1.0, @sitecore-content-sdk/cli@workspace:packages/cli": - version: 0.0.0-use.local - resolution: "@sitecore-content-sdk/cli@workspace:packages/cli" +"@sitecore-content-sdk/analytics-core@npm:^2.1.0": + version: 2.1.0 + resolution: "@sitecore-content-sdk/analytics-core@npm:2.1.0" + dependencies: + "@sitecore-content-sdk/core": "npm:^2.1.0" + debug: "npm:^4.4.3" + isbot: "npm:^5.1.39" + checksum: 10/7470251836399e1205477719f493bf238c6267686dd7131334376356377f55d0c7c67fe231c32e8067fc89df67150be063dc020069a2f2af9709d020cbd402d0 + languageName: node + linkType: hard + +"@sitecore-content-sdk/cli@npm:^2.1.0": + version: 2.1.0 + resolution: "@sitecore-content-sdk/cli@npm:2.1.0" dependencies: "@sitecore-content-sdk/content": "npm:^2.1.0" "@sitecore-content-sdk/core": "npm:^2.1.0" + chokidar: "npm:^4.0.3" + dotenv: "npm:^16.5.0" + dotenv-expand: "npm:^12.0.2" + inquirer: "npm:^12.9.6" + resolve: "npm:^1.22.10" + tmp: "npm:^0.2.3" + tsx: "npm:^4.19.4" + yargs: "npm:^17.7.2" + bin: + sitecore-tools: dist/cjs/bin/sitecore-tools.js + checksum: 10/e5d2d999a3ff6b5027da972f4db75266f23be93368bfc6e878012620b136fbfe64203c3998ac894da3557f754a7a696401a54959d4bc240eef78167c17e08a64 + languageName: node + linkType: hard + +"@sitecore-content-sdk/cli@workspace:packages/cli": + version: 0.0.0-use.local + resolution: "@sitecore-content-sdk/cli@workspace:packages/cli" + dependencies: + "@sitecore-content-sdk/content": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" "@stylistic/eslint-plugin": "npm:^5.2.2" "@types/chai": "npm:^5.2.2" "@types/inquirer": "npm:^9.0.9" @@ -3582,12 +3635,13 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-content-sdk/content@npm:^2.1.0, @sitecore-content-sdk/content@workspace:packages/content": +"@sitecore-content-sdk/content@npm:2.1.0-beta.1, @sitecore-content-sdk/content@workspace:packages/content": version: 0.0.0-use.local resolution: "@sitecore-content-sdk/content@workspace:packages/content" dependencies: - "@sitecore-content-sdk/core": "npm:^2.1.0" - "@sitecore-content-sdk/events": "npm:^2.1.0" + "@json-render/core": "npm:0.19.0" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/events": "npm:2.1.0-beta.1" "@stylistic/eslint-plugin": "npm:^5.2.2" "@types/chai": "npm:^5.2.2" "@types/chai-spies": "npm:^1.0.6" @@ -3627,11 +3681,27 @@ __metadata: tsx: "npm:^4.19.4" typescript: "npm:~5.8.3" peerDependencies: - "@sitecore-content-sdk/events": ^2.1.0 + "@sitecore-content-sdk/events": 2.1.0-beta.1 languageName: unknown linkType: soft -"@sitecore-content-sdk/core@npm:^2.1.0, @sitecore-content-sdk/core@workspace:packages/core": +"@sitecore-content-sdk/content@npm:^2.1.0, @sitecore-content-sdk/content@npm:^2.1.1": + version: 2.1.1 + resolution: "@sitecore-content-sdk/content@npm:2.1.1" + dependencies: + "@sitecore-content-sdk/core": "npm:^2.1.0" + chalk: "npm:^4.1.2" + debug: "npm:^4.4.0" + glob: "npm:^11.0.2" + graphql: "npm:^16.11.0" + url-parse: "npm:^1.5.10" + peerDependencies: + "@sitecore-content-sdk/events": ^2.1.0 + checksum: 10/87e20ac725c389ad81f923db84212789ebb5031a190c4e55230604010397198dd59eff8595467c3a456ba19e9c1cc3b57e80e79819969298ba287e474b491257 + languageName: node + linkType: hard + +"@sitecore-content-sdk/core@npm:2.1.0-beta.1, @sitecore-content-sdk/core@workspace:packages/core": version: 0.0.0-use.local resolution: "@sitecore-content-sdk/core@workspace:packages/core" dependencies: @@ -3669,14 +3739,27 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-content-sdk/events@npm:^2.1.0, @sitecore-content-sdk/events@workspace:packages/events": +"@sitecore-content-sdk/core@npm:^2.1.0": + version: 2.1.0 + resolution: "@sitecore-content-sdk/core@npm:2.1.0" + dependencies: + debug: "npm:^4.4.0" + graphql: "npm:^16.11.0" + graphql-request: "npm:^6.1.0" + memory-cache: "npm:^0.2.0" + url-parse: "npm:^1.5.10" + checksum: 10/943e3bef8e2ea13783e7148252746ecfb453c51c1daa74fcde1b17aaed541a1c4272a35bdd065aa4712e656f649795ee59b43dba573b4721517b67ccae84e7d2 + languageName: node + linkType: hard + +"@sitecore-content-sdk/events@npm:2.1.0-beta.1, @sitecore-content-sdk/events@workspace:packages/events": version: 0.0.0-use.local resolution: "@sitecore-content-sdk/events@workspace:packages/events" dependencies: "@jest/globals": "npm:^30.2.0" "@jest/types": "npm:^29.6.3" - "@sitecore-content-sdk/analytics-core": "npm:^2.1.0" - "@sitecore-content-sdk/core": "npm:^2.1.0" + "@sitecore-content-sdk/analytics-core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" "@stylistic/eslint-plugin": "npm:^5.2.2" "@types/debug": "npm:^4.1.12" "@types/jest": "npm:^29.5.12" @@ -3698,17 +3781,55 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-content-sdk/nextjs@npm:^2.1.0, @sitecore-content-sdk/nextjs@workspace:packages/nextjs": - version: 0.0.0-use.local - resolution: "@sitecore-content-sdk/nextjs@workspace:packages/nextjs" +"@sitecore-content-sdk/events@npm:^2.1.0": + version: 2.1.0 + resolution: "@sitecore-content-sdk/events@npm:2.1.0" dependencies: - "@babel/parser": "npm:^7.27.2" "@sitecore-content-sdk/analytics-core": "npm:^2.1.0" - "@sitecore-content-sdk/content": "npm:^2.1.0" + "@sitecore-content-sdk/core": "npm:^2.1.0" + debug: "npm:^4.4.3" + checksum: 10/16bc271c8409d638e4f12145ff5c56b5e5f0225e831b9b640d45aa3b51c0cd88ca3b6e5f8c27f0917d3b3efb0bc981e8772ac46d518d17b1a730efcb67c620ff + languageName: node + linkType: hard + +"@sitecore-content-sdk/nextjs@npm:^2.1.0": + version: 2.1.1 + resolution: "@sitecore-content-sdk/nextjs@npm:2.1.1" + dependencies: + "@babel/parser": "npm:^7.27.2" + "@sitecore-content-sdk/content": "npm:^2.1.1" "@sitecore-content-sdk/core": "npm:^2.1.0" "@sitecore-content-sdk/events": "npm:^2.1.0" - "@sitecore-content-sdk/personalize": "npm:^2.1.0" "@sitecore-content-sdk/react": "npm:^2.1.0" + recast: "npm:^0.23.11" + regex-parser: "npm:^2.3.1" + sync-disk-cache: "npm:^2.1.0" + peerDependencies: + "@sitecore-content-sdk/analytics-core": ^2.1.0 + "@sitecore-content-sdk/events": ^2.1.0 + "@sitecore-content-sdk/personalize": ^2.1.0 + next: ^16.2.0 + react: ^19.2.1 + react-dom: ^19.2.1 + typescript: ^5.4.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/e5c428714a41334afc5c7592609a225b591653abae8d7e5175fbf95b588344c8139a8f9be94e0ca02acc2a13f85baffaa3ff4a1fa43938ee84fad3d21a0c7e36 + languageName: node + linkType: hard + +"@sitecore-content-sdk/nextjs@workspace:packages/nextjs": + version: 0.0.0-use.local + resolution: "@sitecore-content-sdk/nextjs@workspace:packages/nextjs" + dependencies: + "@babel/parser": "npm:^7.27.2" + "@sitecore-content-sdk/analytics-core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/content": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/events": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/personalize": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/react": "npm:2.1.0-beta.1" "@stylistic/eslint-plugin": "npm:^5.2.2" "@testing-library/dom": "npm:^10.4.0" "@testing-library/react": "npm:^16.3.0" @@ -3752,9 +3873,9 @@ __metadata: ts-node: "npm:^10.9.2" typescript: "npm:~5.8.3" peerDependencies: - "@sitecore-content-sdk/analytics-core": ^2.1.0 - "@sitecore-content-sdk/events": ^2.1.0 - "@sitecore-content-sdk/personalize": ^2.1.0 + "@sitecore-content-sdk/analytics-core": 2.1.0-beta.1 + "@sitecore-content-sdk/events": 2.1.0-beta.1 + "@sitecore-content-sdk/personalize": 2.1.0-beta.1 next: ^16.2.0 react: ^19.2.1 react-dom: ^19.2.1 @@ -3765,14 +3886,14 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-content-sdk/personalize@npm:^2.1.0, @sitecore-content-sdk/personalize@workspace:packages/personalize": +"@sitecore-content-sdk/personalize@npm:2.1.0-beta.1, @sitecore-content-sdk/personalize@workspace:packages/personalize": version: 0.0.0-use.local resolution: "@sitecore-content-sdk/personalize@workspace:packages/personalize" dependencies: "@jest/types": "npm:^29.6.3" - "@sitecore-content-sdk/analytics-core": "npm:^2.1.0" - "@sitecore-content-sdk/core": "npm:^2.1.0" - "@sitecore-content-sdk/events": "npm:^2.1.0" + "@sitecore-content-sdk/analytics-core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/events": "npm:2.1.0-beta.1" "@stylistic/eslint-plugin": "npm:^5.2.2" "@types/debug": "npm:^4.1.12" "@types/jest": "npm:^29.5.12" @@ -3794,13 +3915,27 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-content-sdk/react@npm:^2.1.0, @sitecore-content-sdk/react@workspace:packages/react": - version: 0.0.0-use.local - resolution: "@sitecore-content-sdk/react@workspace:packages/react" +"@sitecore-content-sdk/personalize@npm:^2.1.0": + version: 2.1.0 + resolution: "@sitecore-content-sdk/personalize@npm:2.1.0" dependencies: "@sitecore-content-sdk/analytics-core": "npm:^2.1.0" - "@sitecore-content-sdk/content": "npm:^2.1.0" "@sitecore-content-sdk/core": "npm:^2.1.0" + "@sitecore-content-sdk/events": "npm:^2.1.0" + debug: "npm:^4.4.3" + checksum: 10/975bcc3218629485d80130e1863db78673b4c4cd5c19b1449417a3f86bf2ab6b4ef7cf22aea055f70aec9206d1ab6eb435e6c373eb2c6ddf5417b52acd315af1 + languageName: node + linkType: hard + +"@sitecore-content-sdk/react@npm:2.1.0-beta.1, @sitecore-content-sdk/react@workspace:packages/react": + version: 0.0.0-use.local + resolution: "@sitecore-content-sdk/react@workspace:packages/react" + dependencies: + "@json-render/core": "npm:0.19.0" + "@json-render/react": "npm:0.19.0" + "@sitecore-content-sdk/analytics-core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/content": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" "@sitecore-content-sdk/search": "npm:^0.3.0" "@sitecore-feaas/clientside": "npm:^0.6.0" "@stylistic/eslint-plugin": "npm:^5.2.2" @@ -3840,20 +3975,48 @@ __metadata: ts-node: "npm:^10.9.2" typescript: "npm:~5.8.3" peerDependencies: - "@sitecore-content-sdk/analytics-core": ^2.1.0 - "@sitecore-content-sdk/events": ^2.1.0 + "@sitecore-content-sdk/analytics-core": 2.1.0-beta.1 + "@sitecore-content-sdk/events": 2.1.0-beta.1 "@sitecore-feaas/clientside": ^0.6.0 react: ^19.2.1 react-dom: ^19.2.1 languageName: unknown linkType: soft -"@sitecore-content-sdk/search@npm:^0.3.0, @sitecore-content-sdk/search@workspace:packages/search": - version: 0.0.0-use.local - resolution: "@sitecore-content-sdk/search@workspace:packages/search" +"@sitecore-content-sdk/react@npm:^2.1.0": + version: 2.1.0 + resolution: "@sitecore-content-sdk/react@npm:2.1.0" + dependencies: + "@sitecore-content-sdk/content": "npm:^2.1.0" + "@sitecore-content-sdk/core": "npm:^2.1.0" + "@sitecore-content-sdk/search": "npm:^0.3.0" + fast-deep-equal: "npm:^3.1.3" + peerDependencies: + "@sitecore-content-sdk/analytics-core": ^2.1.0 + "@sitecore-content-sdk/events": ^2.1.0 + "@sitecore-feaas/clientside": ^0.6.0 + react: ^19.2.1 + react-dom: ^19.2.1 + checksum: 10/740bc0d1848f76a3d9fda211007df47d46bf1e99d33c5a1ae5e65dc000c76674aa8cdf9f9f8c65a00e11cf42ffd1da93a262d4b494951bdf524b01d1ddb3a9be + languageName: node + linkType: hard + +"@sitecore-content-sdk/search@npm:^0.3.0": + version: 0.3.0 + resolution: "@sitecore-content-sdk/search@npm:0.3.0" dependencies: "@sitecore-content-sdk/analytics-core": "npm:^2.1.0" "@sitecore-content-sdk/core": "npm:^2.1.0" + checksum: 10/a5591861101eadd66c7de666ba17121e2bdde3ea8408df95d6d7f3a1d13144a4dbc5dc0f3654a077fd386f0e27eafbb044eb70af34711c522ee710ee0bc11146 + languageName: node + linkType: hard + +"@sitecore-content-sdk/search@workspace:packages/search": + version: 0.0.0-use.local + resolution: "@sitecore-content-sdk/search@workspace:packages/search" + dependencies: + "@sitecore-content-sdk/analytics-core": "npm:2.1.0-beta.1" + "@sitecore-content-sdk/core": "npm:2.1.0-beta.1" "@types/chai": "npm:^5.2.3" "@types/mocha": "npm:^10.0.10" "@types/proxyquire": "npm:^1.3.31" @@ -4015,7 +4178,7 @@ __metadata: languageName: node linkType: hard -"@tybys/wasm-util@npm:^0.10.1": +"@tybys/wasm-util@npm:^0.10.2": version: 0.10.2 resolution: "@tybys/wasm-util@npm:0.10.2" dependencies: @@ -4185,12 +4348,12 @@ __metadata: linkType: hard "@types/inquirer@npm:^9.0.8, @types/inquirer@npm:^9.0.9": - version: 9.0.9 - resolution: "@types/inquirer@npm:9.0.9" + version: 9.0.10 + resolution: "@types/inquirer@npm:9.0.10" dependencies: "@types/through": "npm:*" rxjs: "npm:^7.2.0" - checksum: 10/015ee6fa65d1d79c070c889906be3eceedd77aa08a4aea3d17b188c9d169b459994244525f054b60de0e28fe9c492df75527a7b63da16e06fcc09e54039ac652 + checksum: 10/2fd61509eb32bf9eef4b9a3e0fc8929d30ce8d2bdbbceb8a4e65c11831187fefcdd08641ec7f2228feac2ec75b1460deb5f4f91ad66c99b614b239edd8e4da3e languageName: node linkType: hard @@ -4310,11 +4473,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 25.9.2 - resolution: "@types/node@npm:25.9.2" + version: 25.9.3 + resolution: "@types/node@npm:25.9.3" dependencies: undici-types: "npm:>=7.24.0 <7.24.7" - checksum: 10/4ec76a3c9d51866cea5c6a5b17d52a2113b387e7fa812303bf20cc2949ff5e2b2bafcdef693c21ff8235d370ec2807961dc1075eb6bbea661916a5bbd84b6511 + checksum: 10/40c5f5c91450689b255dbf8cbd6cf0bb05e1013a353830b15eb9b5c9cda6448510be572d1ecadc38c8a4ac472e61381de9ae28df82094abece59f4c1eccffbc5 languageName: node linkType: hard @@ -4326,11 +4489,11 @@ __metadata: linkType: hard "@types/node@npm:^24.10.4": - version: 24.13.1 - resolution: "@types/node@npm:24.13.1" + version: 24.13.2 + resolution: "@types/node@npm:24.13.2" dependencies: undici-types: "npm:~7.18.0" - checksum: 10/44aef6ada09b68f69b22d8dc6e84e2a40135af97ee4c0c5203c77fc27d7363a1b3659b71e82e4530de7786ed5e4ea6dd42579dcc9708dea9c1ab40e5bdd9c48e + checksum: 10/2b8cb95ada191ecc788fef91d7cdb86dff24f6895b510e1fb495cc001832709fa8fc3e1eeb27d64bda52b3f74f9e91acf42431e98e3e6876de1e53acce7f7602 languageName: node linkType: hard @@ -4492,23 +4655,23 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.60.1, @typescript-eslint/eslint-plugin@npm:^8.39.0": - version: 8.60.1 - resolution: "@typescript-eslint/eslint-plugin@npm:8.60.1" +"@typescript-eslint/eslint-plugin@npm:8.61.1, @typescript-eslint/eslint-plugin@npm:^8.39.0": + version: 8.61.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.61.1" dependencies: "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.60.1" - "@typescript-eslint/type-utils": "npm:8.60.1" - "@typescript-eslint/utils": "npm:8.60.1" - "@typescript-eslint/visitor-keys": "npm:8.60.1" + "@typescript-eslint/scope-manager": "npm:8.61.1" + "@typescript-eslint/type-utils": "npm:8.61.1" + "@typescript-eslint/utils": "npm:8.61.1" + "@typescript-eslint/visitor-keys": "npm:8.61.1" ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.5.0" peerDependencies: - "@typescript-eslint/parser": ^8.60.1 + "@typescript-eslint/parser": ^8.61.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/f3633bb2700bc32299578baeaf6650418656229be256147ba9d1ab09b34ef2b7fed83804ef4d2439e9189dbdcb89399d67bc8fea55262be6caa32114be048538 + checksum: 10/5434b78781f750eb1e2918f960ff3a6a3fae36951591456b1a309695a5c6c027d914038d7c2c71e614611b5c46a3be85b4b004581be0bcfb1be84e741b0e98a8 languageName: node linkType: hard @@ -4528,19 +4691,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.60.1, @typescript-eslint/parser@npm:^8.39.0": - version: 8.60.1 - resolution: "@typescript-eslint/parser@npm:8.60.1" +"@typescript-eslint/parser@npm:8.61.1, @typescript-eslint/parser@npm:^8.39.0": + version: 8.61.1 + resolution: "@typescript-eslint/parser@npm:8.61.1" dependencies: - "@typescript-eslint/scope-manager": "npm:8.60.1" - "@typescript-eslint/types": "npm:8.60.1" - "@typescript-eslint/typescript-estree": "npm:8.60.1" - "@typescript-eslint/visitor-keys": "npm:8.60.1" + "@typescript-eslint/scope-manager": "npm:8.61.1" + "@typescript-eslint/types": "npm:8.61.1" + "@typescript-eslint/typescript-estree": "npm:8.61.1" + "@typescript-eslint/visitor-keys": "npm:8.61.1" debug: "npm:^4.4.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/f9c484c4a3897015328f328a1c6ee778d113dd134201f635c0421cb72efe6e63f3a68524aff0df6e19e76ff93daf5cabd946e67f12f10dddcf19bda534aa68dc + checksum: 10/cdaca9bb78bd6cc7210e88b28c42af11b23a47393a97ed37350e64a3846d036ebd178583fd2a54216974a740b3f6932274bdaf72046e6307ef26aee3ebe35cec languageName: node linkType: hard @@ -4557,16 +4720,16 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/project-service@npm:8.60.1" +"@typescript-eslint/project-service@npm:8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/project-service@npm:8.61.1" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.60.1" - "@typescript-eslint/types": "npm:^8.60.1" + "@typescript-eslint/tsconfig-utils": "npm:^8.61.1" + "@typescript-eslint/types": "npm:^8.61.1" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/fec693dd79c3a1e6a24091127a37af4eb9d9cee8192cf2a434adae48543eadff834bc0623b5b563c8b592b250bc080570f9e7b42807252ea898442c525beeee9 + checksum: 10/51eb5cbd74748d08512db976bbeabdb9352f44e220621dce3bc96837bc309c7d266df49007be196e57950cf9e12e2c574649cbf14aa2e518734ee55ff7d86f2c languageName: node linkType: hard @@ -4580,13 +4743,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/scope-manager@npm:8.60.1" +"@typescript-eslint/scope-manager@npm:8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/scope-manager@npm:8.61.1" dependencies: - "@typescript-eslint/types": "npm:8.60.1" - "@typescript-eslint/visitor-keys": "npm:8.60.1" - checksum: 10/7228c110410ff8cfc01e96d8f17c986f8b4dd447fe3a3291baaab8fe946026ccdf0291865f788f18cf538ab49bfc067fe797708b6b8590104a65f7e69f921cc5 + "@typescript-eslint/types": "npm:8.61.1" + "@typescript-eslint/visitor-keys": "npm:8.61.1" + checksum: 10/69c1d5b403b2e6adbae7e24856628941e912304ac728b6beae959df98092707adf3f60e1d0c9b90badd758d76174e9d44a7eba55608693c976e5cba5cc47593c languageName: node linkType: hard @@ -4599,12 +4762,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.60.1, @typescript-eslint/tsconfig-utils@npm:^8.39.0, @typescript-eslint/tsconfig-utils@npm:^8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.60.1" +"@typescript-eslint/tsconfig-utils@npm:8.61.1, @typescript-eslint/tsconfig-utils@npm:^8.39.0, @typescript-eslint/tsconfig-utils@npm:^8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.61.1" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/afc78b19b856a71dc4e493f931ae44e1a91dc6441a14cb92e4063db880892f3874768f9d347d4b2f45362f2090e4455407c70f42027d77ddc85d6cba95cdb76c + checksum: 10/ee81d01809178d6fd88a478bdf8fb546063c55f01385c6443fe7b93ebe9ba26ecda4f0eb804b2fc0a189dc34a51e89690477fb68cd099cd3952902f376d641c6 languageName: node linkType: hard @@ -4624,19 +4787,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/type-utils@npm:8.60.1" +"@typescript-eslint/type-utils@npm:8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/type-utils@npm:8.61.1" dependencies: - "@typescript-eslint/types": "npm:8.60.1" - "@typescript-eslint/typescript-estree": "npm:8.60.1" - "@typescript-eslint/utils": "npm:8.60.1" + "@typescript-eslint/types": "npm:8.61.1" + "@typescript-eslint/typescript-estree": "npm:8.61.1" + "@typescript-eslint/utils": "npm:8.61.1" debug: "npm:^4.4.3" ts-api-utils: "npm:^2.5.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/6f426263be597063831bf308e52328e8d387af5db955a09cb85fde1c72f5b1b36a365133b9c9a74330e5e948e59bf9a9b82605f4c9c4e3bf9b6cb7f4c37e4b18 + checksum: 10/01c62227479d94ed3745e3bef5a0c870586d75b6ed550e4beb84b25df23de29558e0bdb6ff291e457977254764766c5d252b75a03d4ad592998269dba69a32a6 languageName: node linkType: hard @@ -4647,10 +4810,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.60.1, @typescript-eslint/types@npm:^8.34.1, @typescript-eslint/types@npm:^8.39.0, @typescript-eslint/types@npm:^8.46.4, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/types@npm:8.60.1" - checksum: 10/c603417e621b5b1263c2f60fad9e202d560fd07fce7f40e9a356c0530e5eaf0ff1a9af865237bf93aa18a5a4e2f034ee0cce0fe6c070f08df33e35a099bdea47 +"@typescript-eslint/types@npm:8.61.1, @typescript-eslint/types@npm:^8.34.1, @typescript-eslint/types@npm:^8.39.0, @typescript-eslint/types@npm:^8.46.4, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/types@npm:8.61.1" + checksum: 10/9aa036b27d1874533bf1b931151adaaebb4380cfd14cc71395ab8d2c00c7420218e42811c2b5a68c9fa54cd048e1ab47085afd8b44cd5d736fb5cb8edd300d64 languageName: node linkType: hard @@ -4674,14 +4837,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.60.1" +"@typescript-eslint/typescript-estree@npm:8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.61.1" dependencies: - "@typescript-eslint/project-service": "npm:8.60.1" - "@typescript-eslint/tsconfig-utils": "npm:8.60.1" - "@typescript-eslint/types": "npm:8.60.1" - "@typescript-eslint/visitor-keys": "npm:8.60.1" + "@typescript-eslint/project-service": "npm:8.61.1" + "@typescript-eslint/tsconfig-utils": "npm:8.61.1" + "@typescript-eslint/types": "npm:8.61.1" + "@typescript-eslint/visitor-keys": "npm:8.61.1" debug: "npm:^4.4.3" minimatch: "npm:^10.2.2" semver: "npm:^7.7.3" @@ -4689,7 +4852,7 @@ __metadata: ts-api-utils: "npm:^2.5.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/9c3a56266aadf589bc6e770cd04cb3f55b1ee1507dcacda61866408c656ae4462aa7e11baf39eb939bc4d1e3b843cf58e60f3ebdeb3e75f042ff0f6fb39c311b + checksum: 10/36b8b68e7fe9bdba0bb24b46a43a29e39f0cd45440ea190644e230d10e96b0bab9289027668910ff976100d68a9b3bc222618bf96b99bae6fd0053eb560a1257 languageName: node linkType: hard @@ -4708,18 +4871,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/utils@npm:8.60.1" +"@typescript-eslint/utils@npm:8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/utils@npm:8.61.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.60.1" - "@typescript-eslint/types": "npm:8.60.1" - "@typescript-eslint/typescript-estree": "npm:8.60.1" + "@typescript-eslint/scope-manager": "npm:8.61.1" + "@typescript-eslint/types": "npm:8.61.1" + "@typescript-eslint/typescript-estree": "npm:8.61.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/a75f8714995b6280b4c15ca957bbc6634862453461111e4a2a07b8bc72b51a504484a9b957fc5b7a646c4bf09f1e414a0c52cd3b6798c42fb8c4de83b1b5a364 + checksum: 10/7c8886801f73fc09ecf585b0e8f33799e4a341d51b00db0467f05853f84b808bb98a35e92eab49f28d5d24a1c915959d834214776f0ff6f0cfa5abb3f2e11496 languageName: node linkType: hard @@ -4733,13 +4896,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.60.1": - version: 8.60.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.60.1" +"@typescript-eslint/visitor-keys@npm:8.61.1": + version: 8.61.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.61.1" dependencies: - "@typescript-eslint/types": "npm:8.60.1" + "@typescript-eslint/types": "npm:8.61.1" eslint-visitor-keys: "npm:^5.0.0" - checksum: 10/6d120b4a790477ae0291e69f6457686c71b929cc40519148f6b6c7fbc09604b15821ae8cf1005aa23acec5105b4016db256a68d68f30eda8d6c24d4fdb0ede86 + checksum: 10/7d99c6ae9e91d32b8cc3662ead0e393c912351b5786ece62e1dc198a6b0e9813bb7eae44772970512e7e424a342c86529d9f3dae7c8ab83ac95b5dc33826647a languageName: node linkType: hard @@ -5051,6 +5214,13 @@ __metadata: languageName: node linkType: hard +"abbrev@npm:^5.0.0": + version: 5.0.0 + resolution: "abbrev@npm:5.0.0" + checksum: 10/a32641fb7a8ba0ad6f65efda80a632c965a2567f52c988897bffc47f473c4e9c3f0166de19d939866b1ed58ec50ce36f697d54a476589ca2706f8b5605ed41f0 + languageName: node + linkType: hard + "acorn-globals@npm:^7.0.0": version: 7.0.1 resolution: "acorn-globals@npm:7.0.1" @@ -5080,11 +5250,11 @@ __metadata: linkType: hard "acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.1": - version: 8.16.0 - resolution: "acorn@npm:8.16.0" + version: 8.17.0 + resolution: "acorn@npm:8.17.0" bin: acorn: bin/acorn - checksum: 10/690c673bb4d61b38ef82795fab58526471ad7f7e67c0e40c4ff1e10ecd80ce5312554ef633c9995bfc4e6d170cef165711f9ca9e49040b62c0c66fbf2dd3df2b + checksum: 10/2eea1588075124df569b15995423204055c5575ad992283025dddfcb557a0340de7d75cc1bc25dca8df148c60c4222e576e0e519965f0ec7f86f6085c8428824 languageName: node linkType: hard @@ -5511,9 +5681,9 @@ __metadata: linkType: hard "axe-core@npm:^4.10.0": - version: 4.12.0 - resolution: "axe-core@npm:4.12.0" - checksum: 10/fbc3cc4d0cb24c259cc8e925d8f9862bc1d1b8c651dced134cc0f98064113b47db7159e3a8aca80f2ccffa9d55ee948023ccbbb922ac9fa209172fd4332ceb6f + version: 4.12.1 + resolution: "axe-core@npm:4.12.1" + checksum: 10/08c2ff348524b57dfb68f4e866b1a6363147203b721c734ed00090c16eb911cd015cf71fd96a8bafe42609715f649d21b2e188fae622ac8ef6144b144582f6c4 languageName: node linkType: hard @@ -5656,11 +5826,11 @@ __metadata: linkType: hard "baseline-browser-mapping@npm:^2.10.12, baseline-browser-mapping@npm:^2.9.19": - version: 2.10.34 - resolution: "baseline-browser-mapping@npm:2.10.34" + version: 2.10.37 + resolution: "baseline-browser-mapping@npm:2.10.37" bin: baseline-browser-mapping: dist/cli.cjs - checksum: 10/d0f2e4792b04b2f305e60ca5bf591fa92a2ee39125d8d8e39959e06d8ea68832a026782acd71050f27e44dc676e2de621c87aa698a47976b4e82ce735cd70fc1 + checksum: 10/5bf887e82a238ab2ec216283ab2c44291d5ad917b4c4d21bb88f466be513223bda20d746c2758cdb0be5b3dad1e52ee8b468dbc196096da92673d6904992af66 languageName: node linkType: hard @@ -5907,9 +6077,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001782": - version: 1.0.30001797 - resolution: "caniuse-lite@npm:1.0.30001797" - checksum: 10/30d34d8ede7a99cedec5489cb7a4fc75b0d641afcc436d2b0986aaf9beb056d3f15391e6a08b06b0bd8a84c64793ac8197005b14a96412514706a5715be98796 + version: 1.0.30001799 + resolution: "caniuse-lite@npm:1.0.30001799" + checksum: 10/eb90443f1e4e4ac7cfe3686d43f0d132c0b552d0d896c0520e7306f2ddf743b4dd5380a7b8adc5ca8d250247966a6cf32cb042930dbc1df452e8623ad92c57e2 languageName: node linkType: hard @@ -7010,9 +7180,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.328": - version: 1.5.368 - resolution: "electron-to-chromium@npm:1.5.368" - checksum: 10/2a046854c08256f9b8f7e6615ef71c36a32c1b438fc1048caa1553929d7e9c518a135066d4a226ea2394d42fb336ef01a09fe5270dd72702a1b9fab33d45a53d + version: 1.5.373 + resolution: "electron-to-chromium@npm:1.5.373" + checksum: 10/f255e8de2e97a69b658b7ddbdf5d9fef9663f98665832eb7ccf583d054a13012c7e90331771abe866a5b4fa3583059bfa699fa381c51e7022d58fc5e997e6077 languageName: node linkType: hard @@ -7120,6 +7290,18 @@ __metadata: languageName: node linkType: hard +"es-abstract-get@npm:^1.0.0": + version: 1.0.0 + resolution: "es-abstract-get@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.2" + is-callable: "npm:^1.2.7" + object-inspect: "npm:^1.13.4" + checksum: 10/a2bfa7536529a21c8590670a69c0c4583e531f92dbc420c13f680bf906215f510d9d14b18e2e4a26e8b07100ab28719811dbce822e43fe87305a5a9069cda24e + languageName: node + linkType: hard + "es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0, es-abstract@npm:^1.24.2": version: 1.24.2 resolution: "es-abstract@npm:1.24.2" @@ -7197,8 +7379,8 @@ __metadata: linkType: hard "es-iterator-helpers@npm:^1.2.1": - version: 1.3.2 - resolution: "es-iterator-helpers@npm:1.3.2" + version: 1.3.3 + resolution: "es-iterator-helpers@npm:1.3.3" dependencies: call-bind: "npm:^1.0.9" call-bound: "npm:^1.0.4" @@ -7216,7 +7398,7 @@ __metadata: internal-slot: "npm:^1.1.0" iterator.prototype: "npm:^1.1.5" math-intrinsics: "npm:^1.1.0" - checksum: 10/6b8f9c55c6bb3d5edbae777e892a18e093a7d7a1aa7e8f14da908476b84fbf55769a51356a674819ec95e9655ecdc873a9b7fb5b719320ef67e1b203c77f0bad + checksum: 10/53a45f693088f51d8aeda4034f1be9d7d4fc8505ee58f70bbb237a63729efccf2f96225e15e2b2ac7815104739e6d244019637609ee7c9ee171b8248585ecfae languageName: node linkType: hard @@ -7267,13 +7449,15 @@ __metadata: linkType: hard "es-to-primitive@npm:^1.3.0": - version: 1.3.0 - resolution: "es-to-primitive@npm:1.3.0" + version: 1.3.1 + resolution: "es-to-primitive@npm:1.3.1" dependencies: + es-abstract-get: "npm:^1.0.0" + es-errors: "npm:^1.3.0" is-callable: "npm:^1.2.7" - is-date-object: "npm:^1.0.5" - is-symbol: "npm:^1.0.4" - checksum: 10/17faf35c221aad59a16286cbf58ef6f080bf3c485dff202c490d074d8e74da07884e29b852c245d894eac84f73c58330ec956dfd6d02c0b449d75eb1012a3f9b + is-date-object: "npm:^1.1.0" + is-symbol: "npm:^1.1.1" + checksum: 10/f8ec95e7425583211d7940a455d507832daa04579db50738340b6c59fcf1b9aa832cf6a69891fdafff88b740457c420bdc95867211808f06ae0338d67b09b032 languageName: node linkType: hard @@ -7374,35 +7558,35 @@ __metadata: linkType: hard "esbuild@npm:~0.28.0": - version: 0.28.0 - resolution: "esbuild@npm:0.28.0" - dependencies: - "@esbuild/aix-ppc64": "npm:0.28.0" - "@esbuild/android-arm": "npm:0.28.0" - "@esbuild/android-arm64": "npm:0.28.0" - "@esbuild/android-x64": "npm:0.28.0" - "@esbuild/darwin-arm64": "npm:0.28.0" - "@esbuild/darwin-x64": "npm:0.28.0" - "@esbuild/freebsd-arm64": "npm:0.28.0" - "@esbuild/freebsd-x64": "npm:0.28.0" - "@esbuild/linux-arm": "npm:0.28.0" - "@esbuild/linux-arm64": "npm:0.28.0" - "@esbuild/linux-ia32": "npm:0.28.0" - "@esbuild/linux-loong64": "npm:0.28.0" - "@esbuild/linux-mips64el": "npm:0.28.0" - "@esbuild/linux-ppc64": "npm:0.28.0" - "@esbuild/linux-riscv64": "npm:0.28.0" - "@esbuild/linux-s390x": "npm:0.28.0" - "@esbuild/linux-x64": "npm:0.28.0" - "@esbuild/netbsd-arm64": "npm:0.28.0" - "@esbuild/netbsd-x64": "npm:0.28.0" - "@esbuild/openbsd-arm64": "npm:0.28.0" - "@esbuild/openbsd-x64": "npm:0.28.0" - "@esbuild/openharmony-arm64": "npm:0.28.0" - "@esbuild/sunos-x64": "npm:0.28.0" - "@esbuild/win32-arm64": "npm:0.28.0" - "@esbuild/win32-ia32": "npm:0.28.0" - "@esbuild/win32-x64": "npm:0.28.0" + version: 0.28.1 + resolution: "esbuild@npm:0.28.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.1" + "@esbuild/android-arm": "npm:0.28.1" + "@esbuild/android-arm64": "npm:0.28.1" + "@esbuild/android-x64": "npm:0.28.1" + "@esbuild/darwin-arm64": "npm:0.28.1" + "@esbuild/darwin-x64": "npm:0.28.1" + "@esbuild/freebsd-arm64": "npm:0.28.1" + "@esbuild/freebsd-x64": "npm:0.28.1" + "@esbuild/linux-arm": "npm:0.28.1" + "@esbuild/linux-arm64": "npm:0.28.1" + "@esbuild/linux-ia32": "npm:0.28.1" + "@esbuild/linux-loong64": "npm:0.28.1" + "@esbuild/linux-mips64el": "npm:0.28.1" + "@esbuild/linux-ppc64": "npm:0.28.1" + "@esbuild/linux-riscv64": "npm:0.28.1" + "@esbuild/linux-s390x": "npm:0.28.1" + "@esbuild/linux-x64": "npm:0.28.1" + "@esbuild/netbsd-arm64": "npm:0.28.1" + "@esbuild/netbsd-x64": "npm:0.28.1" + "@esbuild/openbsd-arm64": "npm:0.28.1" + "@esbuild/openbsd-x64": "npm:0.28.1" + "@esbuild/openharmony-arm64": "npm:0.28.1" + "@esbuild/sunos-x64": "npm:0.28.1" + "@esbuild/win32-arm64": "npm:0.28.1" + "@esbuild/win32-ia32": "npm:0.28.1" + "@esbuild/win32-x64": "npm:0.28.1" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -7458,7 +7642,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/49eafc8906cc4a760a1704556bd3b301f808fcdcf2725190383f151741226bf2a2898a03da75a06a896d6217dadc4f3f3168983557ee31bae602e2e37779a83a + checksum: 10/aaa4a922644afffac45e735c99caf343f881e2d36abcc6b6fb53c230bd69940504a5bb6b0041bdd1a690e748ebc681d3308a7d178987c523d74c63c2c280bac8 languageName: node linkType: hard @@ -8257,7 +8441,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:4.0.5, form-data@npm:^4.0.0, form-data@npm:^4.0.5": +"form-data@npm:4.0.5": version: 4.0.5 resolution: "form-data@npm:4.0.5" dependencies: @@ -8270,6 +8454,19 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0, form-data@npm:^4.0.5": + version: 4.0.6 + resolution: "form-data@npm:4.0.6" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.4" + mime-types: "npm:^2.1.35" + checksum: 10/de6614c8537c92fa5fa3ee7e827758f98f5a9c033f348b7de81855ef36e5cb867e75d9f405d9483ab8d724a4a20d4e79926a299fa8dbba38f530eb659f0884e4 + languageName: node + linkType: hard + "fromentries@npm:^1.2.0": version: 1.3.2 resolution: "fromentries@npm:1.3.2" @@ -8360,16 +8557,19 @@ __metadata: linkType: hard "function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8": - version: 1.1.8 - resolution: "function.prototype.name@npm:1.1.8" + version: 1.2.0 + resolution: "function.prototype.name@npm:1.2.0" dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - define-properties: "npm:^1.2.1" + call-bind: "npm:^1.0.9" + call-bound: "npm:^1.0.4" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" functions-have-names: "npm:^1.2.3" - hasown: "npm:^2.0.2" + has-property-descriptors: "npm:^1.0.2" + hasown: "npm:^2.0.4" is-callable: "npm:^1.2.7" - checksum: 10/25b9e5bea936732a6f0c0c08db58cc0d609ac1ed458c6a07ead46b32e7b9bf3fe5887796c3f83d35994efbc4fdde81c08ac64135b2c399b8f2113968d44082bc + is-document.all: "npm:^1.0.0" + checksum: 10/ad662230bc2b9e971625222b462142b34aa23c70ca58fb4fa72e226bb9106a5752be5c7d8986de7ce5cfb959e5317200d70d88d96359605a165ed1c8cb515223 languageName: node linkType: hard @@ -8736,9 +8936,9 @@ __metadata: linkType: hard "graphql@npm:^16.11.0": - version: 16.14.1 - resolution: "graphql@npm:16.14.1" - checksum: 10/8e506bdd2a0dac666e3de82461bd3cf337335b9c0a9a209756a4d20c481393a4f664084474ccc1ccd22a1fae174a69695ce4154a59d2b6e1c0de0b2cfc992ca8 + version: 16.14.2 + resolution: "graphql@npm:16.14.2" + checksum: 10/cd9a2581508f82621112a4dc625714e81ce725ab0a62502d97c14f61fee180e85276ac64bf9094f4fbf237737832ad4bb970aa26088f9dffece56f8919a49a29 languageName: node linkType: hard @@ -8841,7 +9041,7 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.2, hasown@npm:^2.0.3": +"hasown@npm:^2.0.2, hasown@npm:^2.0.3, hasown@npm:^2.0.4": version: 2.0.4 resolution: "hasown@npm:2.0.4" dependencies: @@ -8859,6 +9059,15 @@ __metadata: languageName: node linkType: hard +"heimdalljs@npm:^0.2.6": + version: 0.2.6 + resolution: "heimdalljs@npm:0.2.6" + dependencies: + rsvp: "npm:~3.2.1" + checksum: 10/ae31e21241a30224657c01c05b4c1cba6b370e502e6b9f00de81df7387b2df960b6c526fe7d1e9456b9375bdc64c3178dcae1ad88cd0d68a1ba295935627ac23 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -9323,7 +9532,7 @@ __metadata: languageName: node linkType: hard -"is-date-object@npm:^1.0.5, is-date-object@npm:^1.1.0": +"is-date-object@npm:^1.1.0": version: 1.1.0 resolution: "is-date-object@npm:1.1.0" dependencies: @@ -9342,6 +9551,15 @@ __metadata: languageName: node linkType: hard +"is-document.all@npm:^1.0.0": + version: 1.0.0 + resolution: "is-document.all@npm:1.0.0" + dependencies: + call-bound: "npm:^1.0.4" + checksum: 10/c76fa391105f180e9d34bf219ab1db325b4f883d2d82c789dbf9a628e4213c97411f038f36b7d096d85f5ddc1fda6e22e9d8d7c65b89ad1ee5d4d1e5a2a4c077 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -9558,7 +9776,7 @@ __metadata: languageName: node linkType: hard -"is-symbol@npm:^1.0.4, is-symbol@npm:^1.1.1": +"is-symbol@npm:^1.1.1": version: 1.1.1 resolution: "is-symbol@npm:1.1.1" dependencies: @@ -9658,9 +9876,9 @@ __metadata: linkType: hard "isbot@npm:^5.1.39": - version: 5.1.41 - resolution: "isbot@npm:5.1.41" - checksum: 10/309ac6904dd0d21e731c76960cd947cf3b2695d0c127c01e7bbc3688aeb60349b27371fd6f4174138b06903bf3c626cf97eae726ee8e80a90ca3f55a20dc5790 + version: 5.1.43 + resolution: "isbot@npm:5.1.43" + checksum: 10/bbe588f40bc6a880c6db5fbcf5a56c9062eeb827e0335fe4b3dd0c5ef9e9fc152a78f2414a07b1e68101f1bd59866c513bac29087f758b62abf20acc34dd0462 languageName: node linkType: hard @@ -11310,7 +11528,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:2.1.35, mime-types@npm:^2.1.12": +"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.35": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -11505,6 +11723,17 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^0.5.0": + version: 0.5.6 + resolution: "mkdirp@npm:0.5.6" + dependencies: + minimist: "npm:^1.2.6" + bin: + mkdirp: bin/cmd.js + checksum: 10/0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 + languageName: node + linkType: hard + "mocha@npm:^11.2.2, mocha@npm:^11.7.5": version: 11.7.6 resolution: "mocha@npm:11.7.6" @@ -11619,18 +11848,18 @@ __metadata: linkType: hard "next@npm:^16.2.0": - version: 16.2.7 - resolution: "next@npm:16.2.7" - dependencies: - "@next/env": "npm:16.2.7" - "@next/swc-darwin-arm64": "npm:16.2.7" - "@next/swc-darwin-x64": "npm:16.2.7" - "@next/swc-linux-arm64-gnu": "npm:16.2.7" - "@next/swc-linux-arm64-musl": "npm:16.2.7" - "@next/swc-linux-x64-gnu": "npm:16.2.7" - "@next/swc-linux-x64-musl": "npm:16.2.7" - "@next/swc-win32-arm64-msvc": "npm:16.2.7" - "@next/swc-win32-x64-msvc": "npm:16.2.7" + version: 16.2.9 + resolution: "next@npm:16.2.9" + dependencies: + "@next/env": "npm:16.2.9" + "@next/swc-darwin-arm64": "npm:16.2.9" + "@next/swc-darwin-x64": "npm:16.2.9" + "@next/swc-linux-arm64-gnu": "npm:16.2.9" + "@next/swc-linux-arm64-musl": "npm:16.2.9" + "@next/swc-linux-x64-gnu": "npm:16.2.9" + "@next/swc-linux-x64-musl": "npm:16.2.9" + "@next/swc-win32-arm64-msvc": "npm:16.2.9" + "@next/swc-win32-x64-msvc": "npm:16.2.9" "@swc/helpers": "npm:0.5.15" baseline-browser-mapping: "npm:^2.9.19" caniuse-lite: "npm:^1.0.30001579" @@ -11674,7 +11903,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/f17437a747f67149da27ece48f095ba453f34ef226f05858e17f5cfb3392938831449e674d2ed3d370aa8223f8425629e07c1b6292b798d21d1a8cc1e58100d9 + checksum: 10/07903fdd4cc68beb7bff8f04f85bda52b3ebec6112702d6761e7e5d17fa2843ac12cd7cbdb369bfb5fadd6cd6b3620c22c9865cb46f8e7615134238ac1422672 languageName: node linkType: hard @@ -11725,7 +11954,7 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^12.1.0, node-gyp@npm:latest": +"node-gyp@npm:^12.1.0": version: 12.4.0 resolution: "node-gyp@npm:12.4.0" dependencies: @@ -11745,6 +11974,26 @@ __metadata: languageName: node linkType: hard +"node-gyp@npm:latest": + version: 13.0.0 + resolution: "node-gyp@npm:13.0.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + nopt: "npm:^10.0.0" + proc-log: "npm:^7.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + undici: "npm:^6.25.0" + which: "npm:^7.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10/12b7b0204d07493c347f59734aaee7531f41540c820ad0e40604e96838ab277f33fb1d70500283dbb66ee02182ebad231b6a13c75644d83e6c94c1ef28009c6a + languageName: node + linkType: hard + "node-int64@npm:^0.4.0": version: 0.4.0 resolution: "node-int64@npm:0.4.0" @@ -11768,6 +12017,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^10.0.0": + version: 10.0.1 + resolution: "nopt@npm:10.0.1" + dependencies: + abbrev: "npm:^5.0.0" + bin: + nopt: bin/nopt.js + checksum: 10/8021371365e78a2cbab015cac50d8449aa2cc411f0b8f2edb466c1336c3dfee4e61c5bf5bde22ee7dcea80d5f4510a7a8705ed3646c8d782f28b550c62bc4fdf + languageName: node + linkType: hard + "nopt@npm:^8.0.0": version: 8.1.0 resolution: "nopt@npm:8.1.0" @@ -12607,8 +12867,8 @@ __metadata: linkType: hard "pacote@npm:^21.0.0, pacote@npm:^21.0.2": - version: 21.5.0 - resolution: "pacote@npm:21.5.0" + version: 21.5.1 + resolution: "pacote@npm:21.5.1" dependencies: "@gar/promise-retry": "npm:^1.0.0" "@npmcli/git": "npm:^7.0.0" @@ -12629,7 +12889,7 @@ __metadata: tar: "npm:^7.4.3" bin: pacote: bin/index.js - checksum: 10/5d31a986728ce10dea688887d31b98eaa8f08be15b9458c6d69257c3f576771dfca56475a7c49251675fcb827dfc1647c1dd69b29e84b40dae35efd9ee257307 + checksum: 10/30054b7ea3d50943cf13804ff006ecda8ff2c1f9c22b3a5ee2c9b5170ac374052c3d6a70621219e3dcd69a728b795afe3677488e2d541dd9911be1b35e4d1f8e languageName: node linkType: hard @@ -12883,12 +13143,12 @@ __metadata: linkType: hard "postcss-selector-parser@npm:^7.0.0": - version: 7.1.1 - resolution: "postcss-selector-parser@npm:7.1.1" + version: 7.1.4 + resolution: "postcss-selector-parser@npm:7.1.4" dependencies: cssesc: "npm:^3.0.0" util-deprecate: "npm:^1.0.2" - checksum: 10/bb3c6455b20af26a556e3021e21101d8470252644e673c1612f7348ff8dd41b11321329f0694cf299b5b94863f823480b72d3e2f4bd3a89dc43e2d8c0dbad341 + checksum: 10/c6d36b37defd387d65b5e0b778cd441303d147eb4a4b7135c6a0452fa777310027218db3ff71a19fb831d067bcdf45a776c6ed71d83bd2ced8afc2e3d5b03385 languageName: node linkType: hard @@ -12994,6 +13254,13 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^7.0.0": + version: 7.0.0 + resolution: "proc-log@npm:7.0.0" + checksum: 10/97cd9f4a8a0d84e42ee91e106e5ba5edcb954521e8dbe26ee6ad31396e5c12cc2be5e5b6be7b53fa5a69959afbacd32719106e2d6f45802e34b31d9a3a01ec20 + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -13565,34 +13832,34 @@ __metadata: linkType: hard "rollup@npm:^4.43.0": - version: 4.61.1 - resolution: "rollup@npm:4.61.1" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.61.1" - "@rollup/rollup-android-arm64": "npm:4.61.1" - "@rollup/rollup-darwin-arm64": "npm:4.61.1" - "@rollup/rollup-darwin-x64": "npm:4.61.1" - "@rollup/rollup-freebsd-arm64": "npm:4.61.1" - "@rollup/rollup-freebsd-x64": "npm:4.61.1" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.61.1" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.61.1" - "@rollup/rollup-linux-arm64-gnu": "npm:4.61.1" - "@rollup/rollup-linux-arm64-musl": "npm:4.61.1" - "@rollup/rollup-linux-loong64-gnu": "npm:4.61.1" - "@rollup/rollup-linux-loong64-musl": "npm:4.61.1" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.61.1" - "@rollup/rollup-linux-ppc64-musl": "npm:4.61.1" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.61.1" - "@rollup/rollup-linux-riscv64-musl": "npm:4.61.1" - "@rollup/rollup-linux-s390x-gnu": "npm:4.61.1" - "@rollup/rollup-linux-x64-gnu": "npm:4.61.1" - "@rollup/rollup-linux-x64-musl": "npm:4.61.1" - "@rollup/rollup-openbsd-x64": "npm:4.61.1" - "@rollup/rollup-openharmony-arm64": "npm:4.61.1" - "@rollup/rollup-win32-arm64-msvc": "npm:4.61.1" - "@rollup/rollup-win32-ia32-msvc": "npm:4.61.1" - "@rollup/rollup-win32-x64-gnu": "npm:4.61.1" - "@rollup/rollup-win32-x64-msvc": "npm:4.61.1" + version: 4.62.0 + resolution: "rollup@npm:4.62.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.62.0" + "@rollup/rollup-android-arm64": "npm:4.62.0" + "@rollup/rollup-darwin-arm64": "npm:4.62.0" + "@rollup/rollup-darwin-x64": "npm:4.62.0" + "@rollup/rollup-freebsd-arm64": "npm:4.62.0" + "@rollup/rollup-freebsd-x64": "npm:4.62.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.62.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.62.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.62.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.62.0" + "@rollup/rollup-linux-loong64-gnu": "npm:4.62.0" + "@rollup/rollup-linux-loong64-musl": "npm:4.62.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.62.0" + "@rollup/rollup-linux-ppc64-musl": "npm:4.62.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.62.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.62.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.62.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.62.0" + "@rollup/rollup-linux-x64-musl": "npm:4.62.0" + "@rollup/rollup-openbsd-x64": "npm:4.62.0" + "@rollup/rollup-openharmony-arm64": "npm:4.62.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.62.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.62.0" + "@rollup/rollup-win32-x64-gnu": "npm:4.62.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.62.0" "@types/estree": "npm:1.0.9" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -13650,7 +13917,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/ab011372007dc91036646316c93e30e407f5b98c112be19d7392b96a99c5d72917ec1590df3fe354314f59462ed2a6b465fd5c6a7dd6fffbb919787f7a01755d + checksum: 10/b64ae92d0dd242f1aec42d73b71f9989c485a84833da62160ef38947b0ae3c68e3de15a6ea1663327247b3d6e2311e2162412ef27a6b16fb7ddbba4a837f0673 languageName: node linkType: hard @@ -13689,6 +13956,13 @@ __metadata: languageName: node linkType: hard +"rsvp@npm:~3.2.1": + version: 3.2.1 + resolution: "rsvp@npm:3.2.1" + checksum: 10/c85d086bfd92b8997846221b447e41612edb6e5e7566725058af82d7e67a002e7338da8e44de98d0ebaca1519d049a6b620e0078d9ab1c8d1403131fe48e7f42 + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -13829,11 +14103,11 @@ __metadata: linkType: hard "semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.8.0": - version: 7.8.2 - resolution: "semver@npm:7.8.2" + version: 7.8.4 + resolution: "semver@npm:7.8.4" bin: semver: bin/semver.js - checksum: 10/52221d8f1cadacda3cc3f0a2e7f7146e0442c7f4219acb25970bed055f5d0a6afbba5f22e293b078c2e93fca3dce0a08b088485e8b75d32a165f16c3627091c8 + checksum: 10/a9c139031d4143932adfacfd2165d6489848c3b84c26d5fc2beef88c6d54c01191ef553e3f71049ccc47df85f0df30748907f84005f46f326095003171c5b673 languageName: node linkType: hard @@ -13990,7 +14264,7 @@ __metadata: languageName: node linkType: hard -"side-channel-list@npm:^1.0.0": +"side-channel-list@npm:^1.0.1": version: 1.0.1 resolution: "side-channel-list@npm:1.0.1" dependencies: @@ -14026,15 +14300,15 @@ __metadata: linkType: hard "side-channel@npm:^1.1.0": - version: 1.1.0 - resolution: "side-channel@npm:1.1.0" + version: 1.1.1 + resolution: "side-channel@npm:1.1.1" dependencies: es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.3" - side-channel-list: "npm:^1.0.0" + object-inspect: "npm:^1.13.4" + side-channel-list: "npm:^1.0.1" side-channel-map: "npm:^1.0.1" side-channel-weakmap: "npm:^1.0.2" - checksum: 10/7d53b9db292c6262f326b6ff3bc1611db84ece36c2c7dc0e937954c13c73185b0406c56589e2bb8d071d6fee468e14c39fb5d203ee39be66b7b8174f179afaba + checksum: 10/5fa6393ff6ad25d8b4a38e9ba095481e498c8ebe5ab78481c1455146255a3d18ca37a6f936595cc671a6149134cdc295bbd2fa017620bdc73cbc7380634fa2fc languageName: node linkType: hard @@ -14594,6 +14868,19 @@ __metadata: languageName: node linkType: hard +"sync-disk-cache@npm:^2.1.0": + version: 2.1.0 + resolution: "sync-disk-cache@npm:2.1.0" + dependencies: + debug: "npm:^4.1.1" + heimdalljs: "npm:^0.2.6" + mkdirp: "npm:^0.5.0" + rimraf: "npm:^3.0.0" + username-sync: "npm:^1.0.2" + checksum: 10/13ba687c7322c6480a0595da0510912067a289a60bcf9b0b6ee54149f7d7315cd6410ad36952ba74a507f69405dcc2c2e369c7a8325065e43c434fef63fc36f7 + languageName: node + linkType: hard + "synckit@npm:^0.11.13, synckit@npm:^0.11.8": version: 0.11.13 resolution: "synckit@npm:0.11.13" @@ -15170,17 +15457,17 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.46.0": - version: 8.60.1 - resolution: "typescript-eslint@npm:8.60.1" + version: 8.61.1 + resolution: "typescript-eslint@npm:8.61.1" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.60.1" - "@typescript-eslint/parser": "npm:8.60.1" - "@typescript-eslint/typescript-estree": "npm:8.60.1" - "@typescript-eslint/utils": "npm:8.60.1" + "@typescript-eslint/eslint-plugin": "npm:8.61.1" + "@typescript-eslint/parser": "npm:8.61.1" + "@typescript-eslint/typescript-estree": "npm:8.61.1" + "@typescript-eslint/utils": "npm:8.61.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/e12091ab2540b817c76b0ec6aad92e341f810310bec2b24bc95780aee106049c05363998f6ea52ed066130c8afc41dca1627f56e4c1df1dd519f4d4ca0ce4910 + checksum: 10/33a798da178f8942a5fb188a991a2eaa9047d7ca95178c67df2565531379b2a587c14ff836716a8b11ed3328c34af5e3b3ea985fa4d2a521a1bb605c2f0e0aa4 languageName: node linkType: hard @@ -15267,9 +15554,9 @@ __metadata: linkType: hard "undici@npm:^6.25.0": - version: 6.26.0 - resolution: "undici@npm:6.26.0" - checksum: 10/a1715ee4304f58fecd61e0a8c3bd7064435cfbc98b3ec1414dba5e89de97d436b7e88dd094b06ff8440428bf36b56163fc88972118890826039865edf58bdfcf + version: 6.27.0 + resolution: "undici@npm:6.27.0" + checksum: 10/30c18cdb235edf4dd36f8aa3ace1ffaf44060289a7d62ad44c33180d2d74a224015d25574812f62ce9c625b5beb1b0b766495b650fedf356aca11eed7ce2c816 languageName: node linkType: hard @@ -15414,7 +15701,7 @@ __metadata: languageName: node linkType: hard -"url-parse@npm:^1.5.3": +"url-parse@npm:^1.5.10, url-parse@npm:^1.5.3": version: 1.5.10 resolution: "url-parse@npm:1.5.10" dependencies: @@ -15424,6 +15711,13 @@ __metadata: languageName: node linkType: hard +"username-sync@npm:^1.0.2": + version: 1.0.3 + resolution: "username-sync@npm:1.0.3" + checksum: 10/1a2aaa8629d018daebd8500272a3064041e18ed157eb32d098dab6ea7dc111b2904222c61bb50f25340d378f003aacbefb3fd6313dd42586137532bc38befe8e + languageName: node + linkType: hard + "util-deprecate@npm:1.0.2, util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -15828,6 +16122,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^7.0.0": + version: 7.0.0 + resolution: "which@npm:7.0.0" + dependencies: + isexe: "npm:^4.0.0" + bin: + node-which: bin/which.js + checksum: 10/913a43ac10df37602ba9795a004dd7ab12ba7dd592aca1f08ec333be1fdd6a49bbf119a88c3f8d0ea70eeb6251726e77069251424d73000299a0a840ed000732 + languageName: node + linkType: hard + "why-is-node-running@npm:^2.3.0": version: 2.3.0 resolution: "why-is-node-running@npm:2.3.0" @@ -16144,3 +16449,10 @@ __metadata: checksum: 10/b2144b38807673a4254dae06fe1a212729550609e606289c305e45c585b36fab1dbba44fe6cde90db9b28be465ec63f4c2a50867aeec6672f6bc36b6c9a361a0 languageName: node linkType: hard + +"zod@npm:^4.3.6": + version: 4.4.3 + resolution: "zod@npm:4.4.3" + checksum: 10/804b9a42aa8f35f2b3c5a8dff906291cb749115f83ee2afe3576d70b5b5c53c965365c7f4967690647a9c54af9838ff232a85ff9577a0a36c44b68bc6cdefe36 + languageName: node + linkType: hard