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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/domscribe-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ npm install @domscribe/next

Drop-in integration for Next.js 15+ projects.

## Style Capture (RFC 0001)

```js
// next.config.js
import { withDomscribe } from '@domscribe/next';

export default withDomscribe({
overlay: true,
captureStyles: true, // build-time styleSource + runtime componentStyles
})({});
```

`captureStyles: true` enables both style-capture tiers with one flag. The
`runtime` option accepts the full `DomscribeRuntimeOptions` (serialization
constraints, `styleOptions`, per-tier `captureStyles` override).

## Links

Part of [Domscribe](https://github.com/patchorbit/domscribe).
Expand Down
19 changes: 19 additions & 0 deletions packages/domscribe-next/src/auto-init/auto-init.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ describe('auto-init', () => {
delete (globalThis as Record<string, unknown>)[
'__DOMSCRIBE_OVERLAY_OPTIONS__'
];
delete (globalThis as Record<string, unknown>)[
'__DOMSCRIBE_RUNTIME_OPTIONS__'
];
});

it('should initialize runtime and react adapter', async () => {
Expand All @@ -41,6 +44,22 @@ describe('auto-init', () => {
});
});

it('should spread __DOMSCRIBE_RUNTIME_OPTIONS__ into initialize', async () => {
(globalThis as Record<string, unknown>)['__DOMSCRIBE_RUNTIME_OPTIONS__'] = {
captureStyles: true,
serialization: { maxDepth: 3 },
};

await import('./index.js');
await vi.dynamicImportSettled();

expect(mockInitialize).toHaveBeenCalledWith({
captureStyles: true,
serialization: { maxDepth: 3 },
adapter: { name: 'react-adapter' },
});
});

it('should initialize overlay when __DOMSCRIBE_OVERLAY_OPTIONS__ is set', async () => {
(globalThis as Record<string, unknown>)['__DOMSCRIBE_OVERLAY_OPTIONS__'] = {
initialMode: 'collapsed',
Expand Down
8 changes: 7 additions & 1 deletion packages/domscribe-next/src/auto-init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
*/

if (typeof window !== 'undefined') {
// Initialize runtime + React adapter
// Initialize runtime + React adapter. Runtime options (serialization,
// captureStyles, styleOptions, ...) come from the loader preamble global.
Promise.all([import('@domscribe/runtime'), import('@domscribe/react')])
.then(([{ RuntimeManager }, { createReactAdapter }]) => {
const win = globalThis as Record<string, unknown>;
const runtimeOptions =
(win['__DOMSCRIBE_RUNTIME_OPTIONS__'] as Record<string, unknown>) ??
{};
RuntimeManager.getInstance().initialize({
...runtimeOptions,
adapter: createReactAdapter(),
});
})
Expand Down
20 changes: 20 additions & 0 deletions packages/domscribe-next/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,27 @@
*
* @module @domscribe/next/types
*/
import type { DomscribeRuntimeOptions } from '@domscribe/runtime';

export interface DomscribeNextOptions {
/**
* Enable RFC 0001 style capture on both tiers with one flag:
* build-time `styleSource` attribution in the manifest (transform tier)
* and runtime `componentStyles` snapshots (runtime tier).
*
* `runtime.captureStyles` overrides the runtime tier when set explicitly.
*
* @default false
*/
captureStyles?: boolean;

/**
* RuntimeManager configuration (phase, PII redaction, block selectors,
* serialization constraints, style capture). Must be JSON-serializable —
* it is forwarded to the browser via a loader-injected global.
*/
runtime?: DomscribeRuntimeOptions;

/**
* Enable transformation.
* Set to false in production builds.
Expand Down
49 changes: 49 additions & 0 deletions packages/domscribe-next/src/with-domscribe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,59 @@ describe('withDomscribe', () => {
enabled: true,
relay: { port: 5000, host: '0.0.0.0', bodyLimit: 5242880 },
overlay: { initialMode: 'expanded', debug: true },
captureStyles: false,
runtime: { captureStyles: false },
autoInitPath: '/resolved/@domscribe/next/auto-init',
});
});

it('should thread captureStyles and runtime options into the loader', () => {
const result = withDomscribe({
captureStyles: true,
runtime: { serialization: { maxDepth: 3 } },
})({});
const webpackFn = result.webpack as (
config: WebpackConfig,
context: WebpackConfigContext,
) => WebpackConfig;
const config = createMockWebpackConfig();

webpackFn(config, createMockWebpackContext());

const rules = config.module?.rules ?? [];
const use = rules[0].use ?? [];
const loaderEntry = use[0];
expect(loaderEntry.options).toMatchObject({
captureStyles: true,
runtime: {
serialization: { maxDepth: 3 },
captureStyles: true,
},
});
});

it('should let runtime.captureStyles override the top-level flag', () => {
const result = withDomscribe({
captureStyles: true,
runtime: { captureStyles: false },
})({});
const webpackFn = result.webpack as (
config: WebpackConfig,
context: WebpackConfigContext,
) => WebpackConfig;
const config = createMockWebpackConfig();

webpackFn(config, createMockWebpackContext());

const rules = config.module?.rules ?? [];
const use = rules[0].use ?? [];
const loaderEntry = use[0];
expect(loaderEntry.options).toMatchObject({
captureStyles: true,
runtime: { captureStyles: false },
});
});

it('should chain existing webpack function', () => {
const existingWebpack = vi.fn((config: WebpackConfig) => config);
const result = withDomscribe()({
Expand Down
27 changes: 26 additions & 1 deletion packages/domscribe-next/src/with-domscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,34 @@ function applyDevTransforms(
debug = false,
relay = {},
overlay = true,
captureStyles = false,
runtime = {},
} = options;

// One flag drives both RFC 0001 tiers; `runtime.captureStyles` can
// override the runtime tier independently.
const runtimeOptions = {
...runtime,
captureStyles: runtime.captureStyles ?? captureStyles,
};

return {
...nextConfig,
turbopack: buildTurbopackConfig(nextConfig, { debug, relay, overlay }),
turbopack: buildTurbopackConfig(nextConfig, {
debug,
relay,
overlay,
captureStyles,
runtime: runtimeOptions,
}),
webpack: buildWebpackFn(nextConfig, {
include,
exclude,
debug,
relay,
overlay,
captureStyles,
runtime: runtimeOptions,
}),
};
}
Expand All @@ -181,6 +198,8 @@ function buildTurbopackConfig(
debug: boolean;
relay: DomscribeNextOptions['relay'];
overlay: DomscribeNextOptions['overlay'];
captureStyles: boolean;
runtime: DomscribeNextOptions['runtime'];
},
): NextConfig['turbopack'] {
let turbopackLoaderPath: string;
Expand All @@ -197,6 +216,8 @@ function buildTurbopackConfig(
enabled: true,
relay: options.relay as JSONValue,
overlay: options.overlay as JSONValue,
captureStyles: options.captureStyles,
runtime: options.runtime as JSONValue,
autoInitPath: resolveAutoInitPath(),
};

Expand Down Expand Up @@ -270,6 +291,8 @@ function buildWebpackFn(
debug: boolean;
relay: DomscribeNextOptions['relay'];
overlay: DomscribeNextOptions['overlay'];
captureStyles: boolean;
runtime: DomscribeNextOptions['runtime'];
},
): NextConfig['webpack'] {
const existingWebpack = nextConfig.webpack;
Expand All @@ -286,6 +309,8 @@ function buildWebpackFn(
enabled: true,
relay: options.relay as JSONValue,
overlay: options.overlay as JSONValue,
captureStyles: options.captureStyles,
runtime: options.runtime as JSONValue,
autoInitPath: resolveAutoInitPath(),
};

Expand Down
17 changes: 17 additions & 0 deletions packages/domscribe-nuxt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ npm install @domscribe/nuxt

Drop-in Nuxt 3+ module.

## Style Capture (RFC 0001)

```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@domscribe/nuxt'],
domscribe: {
overlay: true,
captureStyles: true, // build-time styleSource + runtime componentStyles
},
});
```

`captureStyles: true` enables both style-capture tiers with one flag. The
`runtime` option accepts the full `DomscribeRuntimeOptions` (serialization
constraints, `styleOptions`, per-tier `captureStyles` override).

## Links

Part of [Domscribe](https://github.com/patchorbit/domscribe).
Expand Down
41 changes: 41 additions & 0 deletions packages/domscribe-nuxt/src/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,47 @@ describe('domscribeModule', () => {
const innerHTML = nuxt.options.app.head.script[0].innerHTML;
expect(innerHTML.split(';').length).toBeGreaterThanOrEqual(3);
});

it('should inject runtime options when captureStyles is enabled', async () => {
const nuxt = createMockNuxt();

await callSetup(
{ debug: false, relay: {}, overlay: false, captureStyles: true },
nuxt,
);

const innerHTML = nuxt.options.app.head.script[0].innerHTML;
expect(innerHTML).toContain('__DOMSCRIBE_RUNTIME_OPTIONS__=');
expect(innerHTML).toContain('"captureStyles":true');
});

it('should let runtime.captureStyles override the top-level flag', async () => {
const nuxt = createMockNuxt();

await callSetup(
{
debug: false,
relay: {},
overlay: false,
captureStyles: true,
runtime: { captureStyles: false, serialization: { maxDepth: 3 } },
},
nuxt,
);

const innerHTML = nuxt.options.app.head.script[0].innerHTML;
expect(innerHTML).toContain('"captureStyles":false');
expect(innerHTML).toContain('"serialization":{"maxDepth":3}');
});

it('should not inject runtime options when none are configured', async () => {
const nuxt = createMockNuxt();

await callSetup({ debug: false, relay: {}, overlay: false }, nuxt);

const innerHTML = nuxt.options.app.head.script[0].innerHTML;
expect(innerHTML).not.toContain('__DOMSCRIBE_RUNTIME_OPTIONS__');
});
});

describe('vite plugin registration', () => {
Expand Down
16 changes: 16 additions & 0 deletions packages/domscribe-nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ export const domscribeModule = defineNuxtModule<DomscribeNuxtOptions>({
);
}

// Runtime options for the client plugin (RFC 0001 style capture etc.).
// One `captureStyles` flag drives both tiers; `runtime.captureStyles`
// can override the runtime tier independently. Only emitted when the
// user configured something — the plugin defaults handle absence.
if (options.runtime !== undefined || options.captureStyles !== undefined) {
const runtimeOptions = {
...options.runtime,
captureStyles:
options.runtime?.captureStyles ?? options.captureStyles ?? false,
};
parts.push(
`window.__DOMSCRIBE_RUNTIME_OPTIONS__=${JSON.stringify(runtimeOptions)}`,
);
}

if (parts.length > 0) {
const head = nuxt.options.app.head;
head.script = head.script || [];
Expand Down Expand Up @@ -150,6 +165,7 @@ export const domscribeModule = defineNuxtModule<DomscribeNuxtOptions>({
debug,
relay: { ...options.relay, autoStart: false },
overlay: options.overlay,
captureStyles: options.captureStyles,
}),
);
},
Expand Down
17 changes: 17 additions & 0 deletions packages/domscribe-nuxt/src/runtime/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ describe('runtime/plugin', () => {
delete (globalThis as unknown as Record<string, unknown>)[
'__DOMSCRIBE_OVERLAY_OPTIONS__'
];
delete (globalThis as unknown as Record<string, unknown>)[
'__DOMSCRIBE_RUNTIME_OPTIONS__'
];
});

it('should register a plugin function via defineNuxtPlugin', () => {
Expand All @@ -81,6 +84,20 @@ describe('runtime/plugin', () => {
});
});

it('should spread __DOMSCRIBE_RUNTIME_OPTIONS__ into initialize', async () => {
(globalThis as unknown as Record<string, unknown>)[
'__DOMSCRIBE_RUNTIME_OPTIONS__'
] = { captureStyles: true, serialization: { maxDepth: 3 } };

await getPluginFn()();

expect(mockInitialize).toHaveBeenCalledWith({
captureStyles: true,
serialization: { maxDepth: 3 },
adapter: { name: 'vue-adapter' },
});
});

it('should initialize overlay when __DOMSCRIBE_OVERLAY_OPTIONS__ is set', async () => {
(globalThis as unknown as Record<string, unknown>)[
'__DOMSCRIBE_OVERLAY_OPTIONS__'
Expand Down
7 changes: 7 additions & 0 deletions packages/domscribe-nuxt/src/runtime/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { RuntimeManager } from '@domscribe/runtime';
import { createVueAdapter } from '@domscribe/vue';

export default defineNuxtPlugin(async () => {
// Runtime options (serialization, captureStyles, styleOptions, ...) are
// injected by the module's head script before any bundle executes.
const win = globalThis as Record<string, unknown>;
const runtimeOptions =
(win['__DOMSCRIBE_RUNTIME_OPTIONS__'] as Record<string, unknown>) ?? {};

RuntimeManager.getInstance().initialize({
...runtimeOptions,
adapter: createVueAdapter({}),
});

Expand Down
Loading
Loading