@gradial/aci 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +63 -0
  2. package/dist/astro/index.d.ts +17 -0
  3. package/dist/astro/index.js +21 -0
  4. package/dist/compiler/index.d.ts +2 -0
  5. package/dist/compiler/index.js +2 -0
  6. package/dist/compiler/validate-subset.d.ts +2 -0
  7. package/dist/compiler/validate-subset.js +73 -0
  8. package/dist/compiler/zod-to-jsonschema.d.ts +2 -0
  9. package/dist/compiler/zod-to-jsonschema.js +13 -0
  10. package/dist/content/contract.d.ts +83 -0
  11. package/dist/content/contract.js +104 -0
  12. package/dist/content/index.d.ts +6 -0
  13. package/dist/content/index.js +6 -0
  14. package/dist/content/provider.d.ts +15 -0
  15. package/dist/content/provider.js +36 -0
  16. package/dist/content/routes.d.ts +16 -0
  17. package/dist/content/routes.js +69 -0
  18. package/dist/content/tailwind-validator.d.ts +6 -0
  19. package/dist/content/tailwind-validator.js +31 -0
  20. package/dist/content/types.d.ts +105 -0
  21. package/dist/content/types.js +1 -0
  22. package/dist/content/validation.d.ts +108 -0
  23. package/dist/content/validation.js +184 -0
  24. package/dist/define-component.d.ts +2 -0
  25. package/dist/define-component.js +13 -0
  26. package/dist/define-layout.d.ts +3 -0
  27. package/dist/define-layout.js +6 -0
  28. package/dist/dev/browser.d.ts +7 -0
  29. package/dist/dev/browser.js +2 -0
  30. package/dist/dev/index.d.ts +30 -0
  31. package/dist/dev/index.js +70 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.js +3 -0
  34. package/dist/next/dev-refresh.d.ts +5 -0
  35. package/dist/next/dev-refresh.js +44 -0
  36. package/dist/next/middleware.d.ts +8 -0
  37. package/dist/next/middleware.js +132 -0
  38. package/dist/next/preview.d.ts +7 -0
  39. package/dist/next/preview.js +37 -0
  40. package/dist/next/server.d.ts +58 -0
  41. package/dist/next/server.js +269 -0
  42. package/dist/providers/file.d.ts +18 -0
  43. package/dist/providers/file.js +91 -0
  44. package/dist/sveltekit/index.d.ts +2 -0
  45. package/dist/sveltekit/index.js +18 -0
  46. package/dist/testing/index.d.ts +20 -0
  47. package/dist/testing/index.js +86 -0
  48. package/dist/types/component.d.ts +26 -0
  49. package/dist/types/component.js +1 -0
  50. package/dist/types/config.d.ts +28 -0
  51. package/dist/types/config.js +1 -0
  52. package/dist/types/index.d.ts +6 -0
  53. package/dist/types/index.js +6 -0
  54. package/dist/types/layout.d.ts +12 -0
  55. package/dist/types/layout.js +1 -0
  56. package/dist/types/page.d.ts +45 -0
  57. package/dist/types/page.js +1 -0
  58. package/dist/types/render-mode.d.ts +3 -0
  59. package/dist/types/render-mode.js +1 -0
  60. package/dist/types/renderer.d.ts +71 -0
  61. package/dist/types/renderer.js +1 -0
  62. package/package.json +112 -0
  63. package/src/cli/compile-registry.mjs +199 -0
  64. package/src/cli/verify-renderer.mjs +73 -0
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Agentic Content Infrastructure SDK
2
+
3
+ Agentic Content Infrastructure (ACI) sites implement the frontend contract by
4
+ installing `@gradial/aci`, Zod 4, and TypeScript tooling. The
5
+ SDK exposes component and layout helpers, renderer types, compiler tooling,
6
+ content provider helpers, and framework-specific subpaths from one package.
7
+
8
+ ```bash
9
+ npm install @gradial/aci zod@^4
10
+ npm install -D typescript tsx
11
+ ```
12
+
13
+ Common imports:
14
+
15
+ ```ts
16
+ import { defineComponent, defineLayout, slot } from '@gradial/aci';
17
+ import type { GradialRenderer } from '@gradial/aci';
18
+ import type { KernelPage, KernelSiteConfig } from '@gradial/aci/content';
19
+ import { createContentSchemas, stringField } from '@gradial/aci/content';
20
+ import { FileContentProvider } from '@gradial/aci/providers/file';
21
+ ```
22
+
23
+ Content contract helpers are also available through focused subpaths:
24
+ `@gradial/aci/content/contract`, `@gradial/aci/content/validation`, and
25
+ `@gradial/aci/content/tailwind-validator`.
26
+
27
+ Framework helpers live under `@gradial/aci/astro`,
28
+ `@gradial/aci/next/server`, `@gradial/aci/next/middleware`,
29
+ `@gradial/aci/next/dev-refresh`, and `@gradial/aci/sveltekit`.
30
+
31
+ Testing helpers live under `@gradial/aci/testing`:
32
+
33
+ ```ts
34
+ import { FixtureContentProvider } from '@gradial/aci/testing';
35
+
36
+ const content = new FixtureContentProvider({
37
+ pages: {
38
+ '/': {
39
+ id: 'home',
40
+ $type: 'page',
41
+ status: 'published',
42
+ layout: 'marketing',
43
+ renderMode: 'static',
44
+ metadata: { title: 'Test Home' },
45
+ regions: { main: [] }
46
+ }
47
+ }
48
+ });
49
+
50
+ const input = await content.loadRenderInput('/');
51
+ ```
52
+
53
+ Use this in unit tests when you want the same provider API as
54
+ `FileContentProvider` without creating `.content` fixture files.
55
+
56
+ Publish from this package directory:
57
+
58
+ ```bash
59
+ npm publish --access restricted
60
+ ```
61
+
62
+ The package includes a `prepack` script that builds `dist` before packing or
63
+ publishing.
@@ -0,0 +1,17 @@
1
+ import { type BareMetalContentWatchOptions } from '../dev/index.js';
2
+ interface AstroConfigSetupParams {
3
+ injectScript(stage: 'head-inline' | 'page' | 'page-ssr', content: string): void;
4
+ updateConfig(config: {
5
+ vite?: {
6
+ plugins?: unknown[];
7
+ };
8
+ }): void;
9
+ }
10
+ export interface AstroIntegrationLike {
11
+ name: string;
12
+ hooks: {
13
+ 'astro:config:setup'(params: AstroConfigSetupParams): void;
14
+ };
15
+ }
16
+ export declare function baremetalAstro(options?: BareMetalContentWatchOptions): AstroIntegrationLike;
17
+ export {};
@@ -0,0 +1,21 @@
1
+ import { baremetalContentWatchPlugin, devRefreshPort, devRefreshScript, } from '../dev/index.js';
2
+ export function baremetalAstro(options = {}) {
3
+ return {
4
+ name: 'baremetal-astro',
5
+ hooks: {
6
+ 'astro:config:setup'({ injectScript, updateConfig }) {
7
+ if (process.env.NODE_ENV === 'production') {
8
+ return;
9
+ }
10
+ if (devRefreshPort(options) > 0) {
11
+ injectScript('page', devRefreshScript(options));
12
+ }
13
+ updateConfig({
14
+ vite: {
15
+ plugins: [baremetalContentWatchPlugin(options)],
16
+ },
17
+ });
18
+ },
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,2 @@
1
+ export * from './validate-subset.js';
2
+ export * from './zod-to-jsonschema.js';
@@ -0,0 +1,2 @@
1
+ export * from './validate-subset.js';
2
+ export * from './zod-to-jsonschema.js';
@@ -0,0 +1,2 @@
1
+ import type { ZodTypeAny } from 'zod';
2
+ export declare function validateZodSubset(schema: ZodTypeAny): void;
@@ -0,0 +1,73 @@
1
+ const unsupported = new Set([
2
+ 'ZodEffects',
3
+ 'ZodPipeline',
4
+ 'ZodLazy',
5
+ 'ZodPromise',
6
+ 'ZodFunction',
7
+ 'ZodTransform',
8
+ 'pipe',
9
+ 'lazy',
10
+ 'promise',
11
+ 'function',
12
+ 'transform',
13
+ 'custom',
14
+ ]);
15
+ export function validateZodSubset(schema) {
16
+ const seen = new Set();
17
+ visit(schema, '$', seen);
18
+ }
19
+ function visit(schema, path, seen) {
20
+ if (seen.has(schema)) {
21
+ return;
22
+ }
23
+ seen.add(schema);
24
+ const typeName = schema?._def?.typeName ?? schema?._def?.type;
25
+ if (unsupported.has(String(typeName))) {
26
+ throw new Error(`${path}: unsupported Zod schema feature ${typeName}`);
27
+ }
28
+ const def = schema?._def;
29
+ if (!def) {
30
+ return;
31
+ }
32
+ rejectUnsupportedChecks(def, path);
33
+ for (const child of childSchemas(def)) {
34
+ visit(child.schema, `${path}${child.path}`, seen);
35
+ }
36
+ }
37
+ function rejectUnsupportedChecks(def, path) {
38
+ if (!Array.isArray(def.checks)) {
39
+ return;
40
+ }
41
+ for (const [index, check] of def.checks.entries()) {
42
+ const checkType = check?.def?.type ?? check?._def?.type ?? check?._zod?.def?.check;
43
+ if (String(checkType) === 'custom') {
44
+ throw new Error(`${path}.checks[${index}]: unsupported Zod schema feature custom`);
45
+ }
46
+ }
47
+ }
48
+ function childSchemas(def) {
49
+ const children = [];
50
+ if (def.innerType)
51
+ children.push({ path: '.inner', schema: def.innerType });
52
+ if (def.schema)
53
+ children.push({ path: '.schema', schema: def.schema });
54
+ if (def.valueType)
55
+ children.push({ path: '.value', schema: def.valueType });
56
+ if (def.keyType)
57
+ children.push({ path: '.key', schema: def.keyType });
58
+ if (def.element)
59
+ children.push({ path: '.element', schema: def.element });
60
+ if (Array.isArray(def.options)) {
61
+ def.options.forEach((schema, index) => children.push({ path: `.options[${index}]`, schema }));
62
+ }
63
+ if (Array.isArray(def.items)) {
64
+ def.items.forEach((schema, index) => children.push({ path: `.items[${index}]`, schema }));
65
+ }
66
+ const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
67
+ if (shape && typeof shape === 'object') {
68
+ for (const [name, schema] of Object.entries(shape)) {
69
+ children.push({ path: `.shape.${name}`, schema: schema });
70
+ }
71
+ }
72
+ return children;
73
+ }
@@ -0,0 +1,2 @@
1
+ import { type ZodTypeAny } from 'zod';
2
+ export declare function compileSchema(zodSchema: ZodTypeAny): object;
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import { validateZodSubset } from './validate-subset.js';
3
+ export function compileSchema(zodSchema) {
4
+ validateZodSubset(zodSchema);
5
+ const toJSONSchema = z.toJSONSchema;
6
+ if (typeof toJSONSchema !== 'function') {
7
+ throw new Error('Zod 4 is required: install zod@^4.0.0');
8
+ }
9
+ return toJSONSchema(zodSchema, {
10
+ target: 'draft-2020-12',
11
+ unrepresentable: 'throw',
12
+ });
13
+ }
@@ -0,0 +1,83 @@
1
+ export declare const themeValues: readonly ["auto", "light", "dark"];
2
+ export declare const buttonVariantValues: readonly ["primary", "secondary", "tertiary", "outline", "ghost"];
3
+ export declare const iconPositionValues: readonly ["before", "after"];
4
+ export type ContractField = {
5
+ kind: 'array';
6
+ items: ContractField;
7
+ optional?: boolean;
8
+ } | {
9
+ kind: 'enum';
10
+ values: readonly [string, ...string[]];
11
+ optional?: boolean;
12
+ } | {
13
+ kind: 'atomicBlocks';
14
+ optional?: boolean;
15
+ } | {
16
+ kind: 'boolean';
17
+ optional?: boolean;
18
+ } | {
19
+ kind: 'className';
20
+ optional?: boolean;
21
+ } | {
22
+ kind: 'componentClassName';
23
+ optional?: boolean;
24
+ } | {
25
+ kind: 'image';
26
+ optional?: boolean;
27
+ } | {
28
+ kind: 'object';
29
+ fields: Record<string, ContractField>;
30
+ optional?: boolean;
31
+ } | {
32
+ kind: 'richText';
33
+ optional?: boolean;
34
+ } | {
35
+ kind: 'string';
36
+ minLength?: number;
37
+ optional?: boolean;
38
+ };
39
+ export interface BlockContract {
40
+ component: string;
41
+ displayName: string;
42
+ description: string;
43
+ props: Record<string, ContractField>;
44
+ }
45
+ export interface AtomicBlockContract extends BlockContract {
46
+ }
47
+ export declare const stringField: (options?: {
48
+ minLength?: number;
49
+ optional?: boolean;
50
+ }) => ContractField;
51
+ export declare const booleanField: (options?: {
52
+ optional?: boolean;
53
+ }) => ContractField;
54
+ export declare const imageField: (options?: {
55
+ optional?: boolean;
56
+ }) => ContractField;
57
+ export declare const enumField: (values: readonly [string, ...string[]], options?: {
58
+ optional?: boolean;
59
+ }) => ContractField;
60
+ export declare const arrayField: (items: ContractField, options?: {
61
+ optional?: boolean;
62
+ }) => ContractField;
63
+ export declare const objectField: (fields: Record<string, ContractField>, options?: {
64
+ optional?: boolean;
65
+ }) => ContractField;
66
+ export declare const richTextField: (options?: {
67
+ optional?: boolean;
68
+ }) => ContractField;
69
+ export declare const atomicBlocksField: (options?: {
70
+ optional?: boolean;
71
+ }) => ContractField;
72
+ export declare const classNameField: (options?: {
73
+ optional?: boolean;
74
+ }) => ContractField;
75
+ export declare const componentClassNameField: (options?: {
76
+ optional?: boolean;
77
+ }) => ContractField;
78
+ export declare const defaultAtomicBlockContracts: AtomicBlockContract[];
79
+ export declare const defaultBlockContracts: BlockContract[];
80
+ export declare const blockContracts: BlockContract[];
81
+ export declare const atomicBlockContracts: AtomicBlockContract[];
82
+ export declare const supportedBlockComponents: string[];
83
+ export declare const supportedAtomicBlockComponents: string[];
@@ -0,0 +1,104 @@
1
+ export const themeValues = ['auto', 'light', 'dark'];
2
+ export const buttonVariantValues = ['primary', 'secondary', 'tertiary', 'outline', 'ghost'];
3
+ export const iconPositionValues = ['before', 'after'];
4
+ export const stringField = (options = {}) => ({
5
+ kind: 'string',
6
+ ...options,
7
+ });
8
+ export const booleanField = (options = {}) => ({
9
+ kind: 'boolean',
10
+ ...options,
11
+ });
12
+ export const imageField = (options = {}) => ({
13
+ kind: 'image',
14
+ ...options,
15
+ });
16
+ export const enumField = (values, options = {}) => ({
17
+ kind: 'enum',
18
+ values,
19
+ ...options,
20
+ });
21
+ export const arrayField = (items, options = {}) => ({
22
+ kind: 'array',
23
+ items,
24
+ ...options,
25
+ });
26
+ export const objectField = (fields, options = {}) => ({
27
+ kind: 'object',
28
+ fields,
29
+ ...options,
30
+ });
31
+ export const richTextField = (options = {}) => ({
32
+ kind: 'richText',
33
+ ...options,
34
+ });
35
+ export const atomicBlocksField = (options = {}) => ({
36
+ kind: 'atomicBlocks',
37
+ ...options,
38
+ });
39
+ export const classNameField = (options = {}) => ({
40
+ kind: 'className',
41
+ ...options,
42
+ });
43
+ export const componentClassNameField = (options = {}) => ({
44
+ kind: 'componentClassName',
45
+ ...options,
46
+ });
47
+ const themeFields = {
48
+ theme: enumField(themeValues, { optional: true }),
49
+ className: componentClassNameField({ optional: true }),
50
+ };
51
+ export const defaultAtomicBlockContracts = [
52
+ {
53
+ component: 'button',
54
+ displayName: 'Button',
55
+ description: 'Inline link styled as a button.',
56
+ props: {
57
+ label: stringField({ minLength: 1 }),
58
+ href: stringField({ minLength: 1 }),
59
+ variant: enumField(buttonVariantValues, { optional: true }),
60
+ newWindow: booleanField({ optional: true }),
61
+ icon: stringField({ optional: true }),
62
+ iconPosition: enumField(iconPositionValues, { optional: true }),
63
+ className: componentClassNameField({ optional: true }),
64
+ },
65
+ },
66
+ ];
67
+ export const defaultBlockContracts = [
68
+ {
69
+ component: 'home_hero',
70
+ displayName: 'Home Hero',
71
+ description: 'Full-width hero section with headline, description, CTA, and image.',
72
+ props: {
73
+ eyebrow: stringField({ optional: true }),
74
+ headline: stringField({ minLength: 1 }),
75
+ description: stringField({ optional: true }),
76
+ ctaLabel: stringField({ minLength: 1 }),
77
+ ctaHref: stringField({ minLength: 1 }),
78
+ image: imageField(),
79
+ },
80
+ },
81
+ {
82
+ component: 'container',
83
+ displayName: 'Container',
84
+ description: 'Themed section wrapper that renders a stack of atomic content blocks.',
85
+ props: {
86
+ ...themeFields,
87
+ blocks: atomicBlocksField(),
88
+ },
89
+ },
90
+ {
91
+ component: 'ssr_probe',
92
+ displayName: 'SSR Probe',
93
+ description: 'Server-rendered proof section used by render/publish smoke tests.',
94
+ props: {
95
+ label: stringField({ optional: true }),
96
+ message: stringField({ optional: true }),
97
+ mode: stringField({ optional: true }),
98
+ },
99
+ },
100
+ ];
101
+ export const blockContracts = defaultBlockContracts;
102
+ export const atomicBlockContracts = defaultAtomicBlockContracts;
103
+ export const supportedBlockComponents = defaultBlockContracts.map((block) => block.component);
104
+ export const supportedAtomicBlockComponents = defaultAtomicBlockContracts.map((block) => block.component);
@@ -0,0 +1,6 @@
1
+ export * from './contract.js';
2
+ export * from './provider.js';
3
+ export * from './routes.js';
4
+ export * from './tailwind-validator.js';
5
+ export * from './types.js';
6
+ export * from './validation.js';
@@ -0,0 +1,6 @@
1
+ export * from './contract.js';
2
+ export * from './provider.js';
3
+ export * from './routes.js';
4
+ export * from './tailwind-validator.js';
5
+ export * from './types.js';
6
+ export * from './validation.js';
@@ -0,0 +1,15 @@
1
+ import type { KernelPage, KernelRouteMetadata, KernelSiteConfig, RenderInput } from './types.js';
2
+ export interface ContentProvider<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig> {
3
+ loadSiteConfig(): Promise<TSiteConfig>;
4
+ loadPage(route?: string): Promise<TPage | null>;
5
+ listRoutes(): Promise<string[]>;
6
+ listPublishedRoutes?(): Promise<string[]>;
7
+ resolveRouteMetadata?(route?: string): Promise<KernelRouteMetadata>;
8
+ }
9
+ export interface RenderInputOptions {
10
+ domain?: string;
11
+ locale?: string;
12
+ }
13
+ export declare function loadRenderInput<TPage extends KernelPage = KernelPage, TSiteConfig extends KernelSiteConfig = KernelSiteConfig>(provider: ContentProvider<TPage, TSiteConfig>, route?: string, options?: RenderInputOptions): Promise<RenderInput<TPage, TSiteConfig>>;
14
+ export declare function routeMetadataForContent(siteConfig: KernelSiteConfig, page: KernelPage | null): KernelRouteMetadata;
15
+ export declare function resolveRouteMetadata(provider: ContentProvider, route?: string): Promise<KernelRouteMetadata>;
@@ -0,0 +1,36 @@
1
+ import { normalizeRoute } from './routes.js';
2
+ export async function loadRenderInput(provider, route = '/', options = {}) {
3
+ const normalizedRoute = normalizeRoute(route);
4
+ const [siteConfig, page] = await Promise.all([
5
+ provider.loadSiteConfig(),
6
+ provider.loadPage(normalizedRoute)
7
+ ]);
8
+ return {
9
+ route: normalizedRoute,
10
+ domain: options.domain || siteConfig.domain,
11
+ locale: (options.locale || siteConfig.defaultLocale).toLowerCase(),
12
+ siteConfig,
13
+ page
14
+ };
15
+ }
16
+ export function routeMetadataForContent(siteConfig, page) {
17
+ const title = page?.metadata?.title || siteConfig.seo?.title || siteConfig.title;
18
+ const description = page?.metadata?.description || siteConfig.seo?.description || '';
19
+ return {
20
+ title,
21
+ description,
22
+ canonical: page?.metadata?.canonical,
23
+ siteName: siteConfig.seo?.siteName || siteConfig.title
24
+ };
25
+ }
26
+ export async function resolveRouteMetadata(provider, route = '/') {
27
+ if (provider.resolveRouteMetadata) {
28
+ return provider.resolveRouteMetadata(route);
29
+ }
30
+ const normalizedRoute = normalizeRoute(route);
31
+ const [siteConfig, page] = await Promise.all([
32
+ provider.loadSiteConfig(),
33
+ provider.loadPage(normalizedRoute)
34
+ ]);
35
+ return routeMetadataForContent(siteConfig, page);
36
+ }
@@ -0,0 +1,16 @@
1
+ export declare const DEFAULT_DOMAIN = "www.baremetal.local";
2
+ export declare const DEFAULT_LOCALE = "en-us";
3
+ export interface ContentPathOptions {
4
+ cwd?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ maxWorkspaceDepth?: number;
7
+ }
8
+ export declare function activeDomain(env?: NodeJS.ProcessEnv): string;
9
+ export declare function activeLocale(env?: NodeJS.ProcessEnv): string;
10
+ export declare function normalizeRoute(route: string): string;
11
+ export declare function workspaceRoot(options?: ContentPathOptions): string;
12
+ export declare function contentRoot(options?: ContentPathOptions): string;
13
+ export declare function siteRoot(options?: ContentPathOptions): string;
14
+ export declare function localeRoot(options?: ContentPathOptions): string;
15
+ export declare function routeToPagePath(route: string, options?: ContentPathOptions): string;
16
+ export declare function routeFromRelativePagePath(relativePath: string): string;
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const DEFAULT_DOMAIN = 'www.baremetal.local';
4
+ export const DEFAULT_LOCALE = 'en-us';
5
+ function resolveFrom(cwd, value) {
6
+ return path.isAbsolute(value) ? value : path.resolve(cwd, value);
7
+ }
8
+ export function activeDomain(env = process.env) {
9
+ return env.BARE_METAL_DOMAIN || DEFAULT_DOMAIN;
10
+ }
11
+ export function activeLocale(env = process.env) {
12
+ return (env.BARE_METAL_LOCALE || DEFAULT_LOCALE).toLowerCase();
13
+ }
14
+ export function normalizeRoute(route) {
15
+ const trimmed = String(route || '').trim();
16
+ if (!trimmed || trimmed === '/') {
17
+ return '/';
18
+ }
19
+ return `/${trimmed.replace(/^\/+/, '').replace(/\/+$/, '')}`;
20
+ }
21
+ export function workspaceRoot(options = {}) {
22
+ const cwd = options.cwd || process.cwd();
23
+ const env = options.env || process.env;
24
+ if (env.BARE_METAL_WORKSPACE_ROOT) {
25
+ return resolveFrom(cwd, env.BARE_METAL_WORKSPACE_ROOT);
26
+ }
27
+ let current = cwd;
28
+ const maxDepth = options.maxWorkspaceDepth ?? 6;
29
+ for (let depth = 0; depth < maxDepth; depth += 1) {
30
+ if (fs.existsSync(path.join(current, '.content', 'config', 'site.json'))) {
31
+ return current;
32
+ }
33
+ const parent = path.dirname(current);
34
+ if (parent === current) {
35
+ break;
36
+ }
37
+ current = parent;
38
+ }
39
+ return cwd;
40
+ }
41
+ export function contentRoot(options = {}) {
42
+ const cwd = options.cwd || process.cwd();
43
+ const env = options.env || process.env;
44
+ if (env.BARE_METAL_CONTENT_ROOT) {
45
+ return resolveFrom(cwd, env.BARE_METAL_CONTENT_ROOT);
46
+ }
47
+ return path.join(workspaceRoot(options), '.content');
48
+ }
49
+ export function siteRoot(options = {}) {
50
+ return path.join(contentRoot(options), 'config');
51
+ }
52
+ export function localeRoot(options = {}) {
53
+ return path.join(contentRoot(options), 'pages');
54
+ }
55
+ export function routeToPagePath(route, options = {}) {
56
+ const normalized = normalizeRoute(route);
57
+ if (normalized === '/') {
58
+ return path.join(localeRoot(options), 'home', '_index.json');
59
+ }
60
+ return path.join(localeRoot(options), normalized.slice(1), '_index.json');
61
+ }
62
+ export function routeFromRelativePagePath(relativePath) {
63
+ const normalized = relativePath.split(/[\\/]+/).join('/');
64
+ const slug = normalized.replace(/\/_index\.json$/, '');
65
+ if (slug === 'home') {
66
+ return '/';
67
+ }
68
+ return normalizeRoute(slug);
69
+ }
@@ -0,0 +1,6 @@
1
+ export interface ClassNameValidationResult {
2
+ valid: boolean;
3
+ invalidClasses: string[];
4
+ }
5
+ export declare function validateContentClassName(value: string): ClassNameValidationResult;
6
+ export declare function validateComponentClassName(value: string): ClassNameValidationResult;
@@ -0,0 +1,31 @@
1
+ const MODIFIER_PATTERN = /^(!)?(?:(?:hover|focus|focus-within|focus-visible|active|disabled|visited|checked|first|last|odd|even|sm|md|lg|xl|2xl|dark|light|motion-safe|motion-reduce|portrait|landscape|print|rtl|ltr|open|placeholder|before|after|first-line|first-letter|selection|marker|group-hover|group-focus|peer-hover|peer-focus|has|not|aria-\w+|data-\w+):)*/;
2
+ const RAW_VALUE_PATTERN = /\[(?:#|.*?\d+(?:px|rem|em|vh|vw|dvh|dvw|svh|svw|ch|ex|cap|ic|lh|rlh|cm|mm|in|pt|pc|%))/;
3
+ function parseToken(token) {
4
+ return token.replace(MODIFIER_PATTERN, '');
5
+ }
6
+ function hasArbitraryBracket(bare) {
7
+ return bare.includes('[');
8
+ }
9
+ function hasRawArbitraryValue(bare) {
10
+ if (!hasArbitraryBracket(bare)) {
11
+ return false;
12
+ }
13
+ return RAW_VALUE_PATTERN.test(bare);
14
+ }
15
+ function validate(value, isInvalid) {
16
+ const tokens = value.trim().split(/\s+/).filter(Boolean);
17
+ const invalidClasses = [];
18
+ for (const token of tokens) {
19
+ const bare = parseToken(token);
20
+ if (isInvalid(bare)) {
21
+ invalidClasses.push(token);
22
+ }
23
+ }
24
+ return { valid: invalidClasses.length === 0, invalidClasses };
25
+ }
26
+ export function validateContentClassName(value) {
27
+ return validate(value, hasArbitraryBracket);
28
+ }
29
+ export function validateComponentClassName(value) {
30
+ return validate(value, hasRawArbitraryValue);
31
+ }