Skip to content

Commit

Permalink
Runtime and publishing for content sources (#703)
Browse files Browse the repository at this point in the history
* Support defining content sources in gitbook-manifest.yaml

* Add runtime definition for content sources

* Changeset

* Add back complete logic
  • Loading branch information
SamyPesse authored Feb 3, 2025
1 parent 48c9ed9 commit ebfa12c
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-experts-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gitbook/cli': minor
---

Allow publishing integrations with content sources
5 changes: 5 additions & 0 deletions .changeset/honest-dots-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gitbook/runtime': minor
---

Add APIs to define and expose content sources
9 changes: 9 additions & 0 deletions packages/cli/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ const IntegrationManifestConfigurationComponent = z.object({
componentId: z.string(),
});

const IntegrationManifestContentSource = z.object({
id: z.string(),
title: z.string().min(2).max(40),
description: z.string().min(0).max(150).optional(),
icon: z.string().optional(),
configuration: IntegrationManifestConfigurationComponent.optional(),
});

const JSONSchemaBaseSchema = z.object({
title: z.string().max(30).optional(),
description: z.string().max(100).optional(),
Expand Down Expand Up @@ -74,6 +82,7 @@ const IntegrationManifestSchema = z.object({
scopes: z.array(z.nativeEnum(api.IntegrationScope)),
categories: z.array(z.nativeEnum(api.IntegrationCategory)).optional(),
blocks: z.array(IntegrationManifestBlock).optional(),
contentSources: z.array(IntegrationManifestContentSource).optional(),
configurations: z
.object({
account: IntegrationManifestConfiguration.optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export async function publishIntegration(
scopes: manifest.scopes ?? [],
categories: manifest.categories,
blocks: manifest.blocks,
contentSources: manifest.contentSources,
configurations: manifest.configurations,
secrets: manifest.secrets,
visibility: manifest.visibility,
Expand Down
11 changes: 11 additions & 0 deletions packages/runtime/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type PlainObjectValue =
| number
| string
| boolean
| PlainObject
| undefined
| null
| PlainObjectValue[];
export type PlainObject = {
[key: string]: PlainObjectValue;
};
52 changes: 34 additions & 18 deletions packages/runtime/src/components.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
import {
ContentKitBlock,
UIRenderEvent,
ContentKitRenderOutput,
ContentKitContext,
ContentKitDefaultAction,
ContentKitRenderOutput,
UIRenderEvent,
} from '@gitbook/api';

import { RuntimeCallback, RuntimeContext } from './context';

type PlainObjectValue =
| number
| string
| boolean
| PlainObject
| undefined
| null
| PlainObjectValue[];
type PlainObject = {
[key: string]: PlainObjectValue;
import { RuntimeCallback, RuntimeEnvironment, RuntimeContext } from './context';
import { PlainObject } from './common';

/**
* Props for an installation configuration component.
*/
export type InstallationConfigurationProps<Env extends RuntimeEnvironment> = {
installation: {
configuration: Env extends RuntimeEnvironment<infer Config, any> ? Config : never;
};
};

/**
* Props for an installation configuration component.
*/
export type SpaceInstallationConfigurationProps<Env extends RuntimeEnvironment> =
InstallationConfigurationProps<Env> & {
spaceInstallation: {
configuration?: Env extends RuntimeEnvironment<any, infer Config> ? Config : never;
};
};

/**
* Cache configuration for the output of a component.
*/
export interface ComponentRenderCache {
maxAge: number;
}

/**
* Instance of a component, passed to the `render` and `action` function.
*/
export interface ComponentInstance<Props extends PlainObject, State extends PlainObject> {
props: Props;
state: State;
Expand All @@ -40,6 +54,9 @@ export interface ComponentInstance<Props extends PlainObject, State extends Plai
dynamicState<Key extends keyof State>(key: Key): { $state: Key };
}

/**
* Definition of a component. Exported from `createComponent` and should be passed to `components` in the integration.
*/
export interface ComponentDefinition<Context extends RuntimeContext = RuntimeContext> {
componentId: string;
render: RuntimeCallback<[UIRenderEvent], Promise<Response>, Context>;
Expand Down Expand Up @@ -116,7 +133,7 @@ export function createComponent<
dynamicState: (key) => ({ $state: key }),
};

const wrapResponse = (output: ContentKitRenderOutput) => {
const respondWithOutput = (output: ContentKitRenderOutput) => {
return new Response(JSON.stringify(output), {
headers: {
'Content-Type': 'application/json',
Expand All @@ -135,7 +152,7 @@ export function createComponent<

// If the action is complete, return the result directly. No need to render the component.
if (actionResult?.type === 'complete') {
return wrapResponse(actionResult);
return respondWithOutput(actionResult);
}

instance = { ...instance, ...actionResult };
Expand All @@ -144,14 +161,13 @@ export function createComponent<
const element = await component.render(instance, context);

const output: ContentKitRenderOutput = {
// for backward compatibility always default to 'element'
type: 'element',
state: instance.state,
props: instance.props,
element,
};

return wrapResponse(output);
return respondWithOutput(output);
},
};
}
84 changes: 84 additions & 0 deletions packages/runtime/src/contentSources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
ContentComputeDocumentEvent,
ContentComputeRevisionEventResponse,
ContentComputeRevisionEvent,
Document,
} from '@gitbook/api';
import { PlainObject } from './common';
import { RuntimeCallback, RuntimeContext } from './context';

export interface ContentSourceDefinition<Context extends RuntimeContext = RuntimeContext> {
sourceId: string;
compute: RuntimeCallback<
[ContentComputeRevisionEvent | ContentComputeDocumentEvent],
Promise<Response>,
Context
>;
}

/**
* Create a content source. The result should be bind to the integration using `contentSources`.
*/
export function createContentSource<
Props extends PlainObject = {},
Context extends RuntimeContext = RuntimeContext,
>(source: {
/**
* ID of the source, referenced in the YAML file.
*/
sourceId: string;

/**
* Callback to generate the pages.
*/
getRevision: RuntimeCallback<
[
{
props: Props;
},
],
Promise<ContentComputeRevisionEventResponse>,
Context
>;

/**
* Callback to generate the document of a page.
*/
getPageDocument: RuntimeCallback<
[
{
props: Props;
},
],
Promise<Document | void>,
Context
>;
}): ContentSourceDefinition<Context> {
return {
sourceId: source.sourceId,
compute: async (event, context) => {
const output =
event.type === 'content_compute_revision'
? await source.getRevision(
{
props: event.props as Props,
},
context,
)
: {
document: await source.getPageDocument(
{
props: event.props as Props,
},
context,
),
};

return new Response(JSON.stringify(output), {
headers: {
'content-type': 'application/json',
},
});
},
};
}
27 changes: 22 additions & 5 deletions packages/runtime/src/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from './events';
import { Logger } from './logger';
import { ExposableError } from './errors';
import { ContentSourceDefinition } from './contentSources';

const logger = Logger('integrations');

Expand Down Expand Up @@ -39,6 +40,11 @@ interface IntegrationRuntimeDefinition<Context extends RuntimeContext = RuntimeC
* Components to expose in the integration.
*/
components?: Array<ComponentDefinition<Context>>;

/**
* Content sources to expose in the integration.
*/
contentSources?: Array<ContentSourceDefinition<Context>>;
}

/**
Expand All @@ -47,7 +53,7 @@ interface IntegrationRuntimeDefinition<Context extends RuntimeContext = RuntimeC
export function createIntegration<Context extends RuntimeContext = RuntimeContext>(
definition: IntegrationRuntimeDefinition<Context>,
) {
const { events = {}, components = [] } = definition;
const { events = {}, components = [], contentSources = [] } = definition;

/**
* Handle a fetch event sent by the integration dispatcher.
Expand Down Expand Up @@ -161,6 +167,17 @@ export function createIntegration<Context extends RuntimeContext = RuntimeContex
return await component.render(event, context);
}

case 'content_compute_revision':
case 'content_compute_document': {
const contentSource = contentSources.find((c) => c.sourceId === event.sourceId);

if (!contentSource) {
throw new ExposableError(`Content source ${event.sourceId} not found`, 404);
}

return await contentSource.compute(event, context);
}

default: {
const cb = events[event.type];

Expand All @@ -178,10 +195,10 @@ export function createIntegration<Context extends RuntimeContext = RuntimeContex
return new Response('OK', { status: 200 });
}

logger.info(`integration does not handle ${event.type} events`);
return new Response(`Integration does not handle ${event.type} events`, {
status: 200,
});
throw new ExposableError(
`Integration does not handle "${event.type}" events`,
406,
);
}
}
} catch (err) {
Expand Down

0 comments on commit ebfa12c

Please sign in to comment.