@prairielearn/preact 1.0.4 → 1.0.5

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @prairielearn/preact
2
2
 
3
+ ## 1.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - c01279d: Change internal HTML structure
8
+ - Updated dependencies [d087e37]
9
+ - @prairielearn/compiled-assets@3.3.0
10
+
3
11
  ## 1.0.4
4
12
 
5
13
  ### Patch Changes
@@ -31,16 +31,16 @@ onDocumentReady(() => {
31
31
  }
32
32
  // If you forget to register a component with `registerHydratedComponent`, this is going to hang.
33
33
  const Component = await registry.getComponent(componentName);
34
- const dataElement = el.querySelector('script[data-component-props]');
34
+ const dataElement = el.previousElementSibling;
35
35
  if (!dataElement)
36
36
  throw new Error('No data element found');
37
+ if (!dataElement.hasAttribute('data-component-props')) {
38
+ throw new Error('Data element is missing data-component-props attribute');
39
+ }
37
40
  if (!dataElement.textContent)
38
41
  throw new Error('Data element has no content');
39
42
  const data = superjson.parse(dataElement.textContent);
40
- const rootElement = el.querySelector('div[data-component-root]');
41
- if (!rootElement)
42
- throw new Error('No root element found');
43
- hydrate(_jsx(Component, { ...data }), rootElement);
43
+ hydrate(_jsx(Component, { ...data }), el);
44
44
  },
45
45
  });
46
46
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hydrated-component/index.tsx"],"names":[],"mappings":";AAAA,OAAO,aAAa,CAAC;AAErB,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAsB,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAEvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAE3D,8EAA8E;AAC9E,uCAAuC;AAEvC,MAAM,QAAQ,GAAG,IAAI,0BAA0B,EAAE,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAA6B,EAAE,YAAqB;IAC5F,6EAA6E;IAC7E,sFAAsF;IACtF,MAAM,EAAE,GAAG,YAAY,IAAI,SAAS,CAAC,WAAW,CAAC;IACjD,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;AACvC,CAAC;AAED,eAAe,CAAC,GAAG,EAAE;IACnB,OAAO,CAAC,wBAAwB,EAAE;QAChC,KAAK,CAAC,GAAG,CAAC,EAAE;YACV,MAAM,aAAa,GAAG,EAAE,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC;YACxD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;YACxF,CAAC;YAED,iGAAiG;YACjG,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;YAE7D,MAAM,WAAW,GAAG,EAAE,CAAC,aAAa,CAAC,8BAA8B,CAAC,CAAC;YACrE,IAAI,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;YAC3D,IAAI,CAAC,WAAW,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;YAC7E,MAAM,IAAI,GAAW,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;YAE9D,MAAM,WAAW,GAAG,EAAE,CAAC,aAAa,CAAC,0BAA0B,CAAC,CAAC;YACjE,IAAI,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;YAC3D,OAAO,CAAC,KAAC,SAAS,OAAK,IAAI,GAAI,EAAE,WAAW,CAAC,CAAC;QAChD,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import '../debug.js';\n\nimport { observe } from 'selector-observer';\nimport superjson from 'superjson';\n\nimport { onDocumentReady } from '@prairielearn/browser-utils';\nimport { type ComponentType, hydrate } from '@prairielearn/preact-cjs';\n\nimport { HydratedComponentsRegistry } from './registry.js';\n\n// This file, if imported, will register a selector observer that will hydrate\n// registered components on the client.\n\nconst registry = new HydratedComponentsRegistry();\n\n/**\n * Registers a Preact component for client-side hydration. The component should have a\n * `displayName` property. If it's missing, or the name of the component bundle differs,\n * you can provide a `nameOverride`.\n */\nexport function registerHydratedComponent(component: ComponentType<any>, nameOverride?: string) {\n // Each React component that will be hydrated on the page must be registered.\n // Note that we don't try to use `component.name` since it can be minified or mangled.\n const id = nameOverride ?? component.displayName;\n if (!id) {\n throw new Error('React fragment must have a displayName or nameOverride');\n }\n registry.setComponent(id, component);\n}\n\nonDocumentReady(() => {\n observe('.js-hydrated-component', {\n async add(el) {\n const componentName = el.getAttribute('data-component');\n if (!componentName) {\n throw new Error('js-hydrated-component element must have a data-component attribute');\n }\n\n // If you forget to register a component with `registerHydratedComponent`, this is going to hang.\n const Component = await registry.getComponent(componentName);\n\n const dataElement = el.querySelector('script[data-component-props]');\n if (!dataElement) throw new Error('No data element found');\n if (!dataElement.textContent) throw new Error('Data element has no content');\n const data: object = superjson.parse(dataElement.textContent);\n\n const rootElement = el.querySelector('div[data-component-root]');\n if (!rootElement) throw new Error('No root element found');\n hydrate(<Component {...data} />, rootElement);\n },\n });\n});\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hydrated-component/index.tsx"],"names":[],"mappings":";AAAA,OAAO,aAAa,CAAC;AAErB,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAsB,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAEvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAE3D,8EAA8E;AAC9E,uCAAuC;AAEvC,MAAM,QAAQ,GAAG,IAAI,0BAA0B,EAAE,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAA6B,EAAE,YAAqB;IAC5F,6EAA6E;IAC7E,sFAAsF;IACtF,MAAM,EAAE,GAAG,YAAY,IAAI,SAAS,CAAC,WAAW,CAAC;IACjD,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;AACvC,CAAC;AAED,eAAe,CAAC,GAAG,EAAE;IACnB,OAAO,CAAC,wBAAwB,EAAE;QAChC,KAAK,CAAC,GAAG,CAAC,EAAE;YACV,MAAM,aAAa,GAAG,EAAE,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC;YACxD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;YACxF,CAAC;YAED,iGAAiG;YACjG,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;YAE7D,MAAM,WAAW,GAAG,EAAE,CAAC,sBAAsB,CAAC;YAC9C,IAAI,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;YAC3D,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,sBAAsB,CAAC,EAAE,CAAC;gBACtD,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;YAC5E,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;YAC7E,MAAM,IAAI,GAAW,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;YAE9D,OAAO,CAAC,KAAC,SAAS,OAAK,IAAI,GAAI,EAAE,EAAE,CAAC,CAAC;QACvC,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import '../debug.js';\n\nimport { observe } from 'selector-observer';\nimport superjson from 'superjson';\n\nimport { onDocumentReady } from '@prairielearn/browser-utils';\nimport { type ComponentType, hydrate } from '@prairielearn/preact-cjs';\n\nimport { HydratedComponentsRegistry } from './registry.js';\n\n// This file, if imported, will register a selector observer that will hydrate\n// registered components on the client.\n\nconst registry = new HydratedComponentsRegistry();\n\n/**\n * Registers a Preact component for client-side hydration. The component should have a\n * `displayName` property. If it's missing, or the name of the component bundle differs,\n * you can provide a `nameOverride`.\n */\nexport function registerHydratedComponent(component: ComponentType<any>, nameOverride?: string) {\n // Each React component that will be hydrated on the page must be registered.\n // Note that we don't try to use `component.name` since it can be minified or mangled.\n const id = nameOverride ?? component.displayName;\n if (!id) {\n throw new Error('React fragment must have a displayName or nameOverride');\n }\n registry.setComponent(id, component);\n}\n\nonDocumentReady(() => {\n observe('.js-hydrated-component', {\n async add(el) {\n const componentName = el.getAttribute('data-component');\n if (!componentName) {\n throw new Error('js-hydrated-component element must have a data-component attribute');\n }\n\n // If you forget to register a component with `registerHydratedComponent`, this is going to hang.\n const Component = await registry.getComponent(componentName);\n\n const dataElement = el.previousElementSibling;\n if (!dataElement) throw new Error('No data element found');\n if (!dataElement.hasAttribute('data-component-props')) {\n throw new Error('Data element is missing data-component-props attribute');\n }\n if (!dataElement.textContent) throw new Error('Data element has no content');\n const data: object = superjson.parse(dataElement.textContent);\n\n hydrate(<Component {...data} />, el);\n },\n });\n});\n"]}
package/dist/server.d.ts CHANGED
@@ -8,12 +8,14 @@ import { type ComponentChildren, type VNode } from '@prairielearn/preact-cjs';
8
8
  */
9
9
  export declare function renderHtmlDocument(content: VNode): string;
10
10
  interface HydrateProps {
11
- /** The component to hydrate */
11
+ /** The component to hydrate. */
12
12
  children: ComponentChildren;
13
- /** Optional override for the component's name or displayName */
13
+ /** Optional override for the component's name or displayName. */
14
14
  nameOverride?: string;
15
15
  /** Whether to apply full height styles. */
16
16
  fullHeight?: boolean;
17
+ /** Optional CSS class to apply to the container. */
18
+ class?: string;
17
19
  }
18
20
  /**
19
21
  * A component that renders a Preact component for client-side hydration.
@@ -21,7 +23,7 @@ interface HydrateProps {
21
23
  * This component is intended to be used within a non-interactive Preact component
22
24
  * that will be rendered without hydration through `renderHtml`.
23
25
  */
24
- export declare function Hydrate({ children, nameOverride, fullHeight }: HydrateProps): VNode;
26
+ export declare function Hydrate({ children, nameOverride, class: className, fullHeight, }: HydrateProps): VNode;
25
27
  /**
26
28
  * Renders a Preact component for client-side hydration and returns an HTML-safe string.
27
29
  * This function is intended to be used within a tagged template literal, e.g. html`...`.
@@ -29,6 +31,6 @@ export declare function Hydrate({ children, nameOverride, fullHeight }: HydrateP
29
31
  * @param content - A Preact VNode to render to HTML.
30
32
  * @returns An `HtmlSafeString` containing the rendered HTML.
31
33
  */
32
- export declare function hydrateHtml<T>(content: VNode<T>): HtmlSafeString;
34
+ export declare function hydrateHtml<T>(content: VNode<T>, props?: Omit<HydrateProps, 'children'>): HtmlSafeString;
33
35
  export {};
34
36
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,KAAK,iBAAiB,EAAY,KAAK,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAwBxF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,KAAK,UAEhD;AAED,UAAU,YAAY;IACpB,+BAA+B;IAC/B,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAkB,EAAE,EAAE,YAAY,GAAG,KAAK,CAiF3F;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,cAAc,CAGhE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,KAAK,iBAAiB,EAAY,KAAK,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAwBxF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,KAAK,UAEhD;AAED,UAAU,YAAY;IACpB,gCAAgC;IAChC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,EACtB,QAAQ,EACR,YAAY,EACZ,KAAK,EAAE,SAAS,EAChB,UAAkB,GACnB,EAAE,YAAY,GAAG,KAAK,CAgFtB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EACjB,KAAK,GAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAM,GACzC,cAAc,CAGhB"}
package/dist/server.js CHANGED
@@ -40,7 +40,7 @@ export function renderHtmlDocument(content) {
40
40
  * This component is intended to be used within a non-interactive Preact component
41
41
  * that will be rendered without hydration through `renderHtml`.
42
42
  */
43
- export function Hydrate({ children, nameOverride, fullHeight = false }) {
43
+ export function Hydrate({ children, nameOverride, class: className, fullHeight = false, }) {
44
44
  if (!isValidElement(children)) {
45
45
  throw new Error('<Hydrate> expects a single Preact component as its child');
46
46
  }
@@ -91,11 +91,11 @@ registerHydratedComponent(${componentName});</code></pre>
91
91
  });
92
92
  }
93
93
  const scriptPreloads = compiledScriptPreloadPaths(scriptPath);
94
- return (_jsxs(Fragment, { children: [_jsx("script", { type: "module", src: compiledScriptSrc }), scriptPreloads.map((preloadPath) => (_jsx("link", { rel: "modulepreload", href: preloadPath }, preloadPath))), _jsxs("div", { "data-component": componentName, class: clsx('js-hydrated-component', { 'h-100': fullHeight }), children: [_jsx("script", { type: "application/json",
95
- // eslint-disable-next-line @eslint-react/dom/no-dangerously-set-innerhtml
96
- dangerouslySetInnerHTML: {
97
- __html: escapeJsonForHtml(props),
98
- }, "data-component-props": true }), _jsx("div", { class: fullHeight ? 'h-100' : '', "data-component-root": true, children: _jsx(Component, { ...props }) })] })] }));
94
+ return (_jsxs(Fragment, { children: [_jsx("script", { type: "module", src: compiledScriptSrc }), scriptPreloads.map((preloadPath) => (_jsx("link", { rel: "modulepreload", href: preloadPath }, preloadPath))), _jsx("script", { type: "application/json",
95
+ // eslint-disable-next-line @eslint-react/dom/no-dangerously-set-innerhtml
96
+ dangerouslySetInnerHTML: {
97
+ __html: escapeJsonForHtml(props),
98
+ }, "data-component": componentName, "data-component-props": true }), _jsx("div", { "data-component": componentName, class: clsx('js-hydrated-component', { 'h-100': fullHeight }, className), children: _jsx(Component, { ...props }) })] }));
99
99
  }
100
100
  /**
101
101
  * Renders a Preact component for client-side hydration and returns an HTML-safe string.
@@ -104,8 +104,8 @@ registerHydratedComponent(${componentName});</code></pre>
104
104
  * @param content - A Preact VNode to render to HTML.
105
105
  * @returns An `HtmlSafeString` containing the rendered HTML.
106
106
  */
107
- export function hydrateHtml(content) {
107
+ export function hydrateHtml(content, props = {}) {
108
108
  // Useful for adding Preact components to existing tagged-template pages.
109
- return renderHtml(_jsx(Hydrate, { children: content }));
109
+ return renderHtml(_jsx(Hydrate, { ...props, children: content }));
110
110
  }
111
111
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { isFragment, isValidElement } from 'preact/compat';\nimport superjson from 'superjson';\n\nimport { compiledScriptPath, compiledScriptPreloadPaths } from '@prairielearn/compiled-assets';\nimport { AugmentedError } from '@prairielearn/error';\nimport { type HtmlSafeString, html } from '@prairielearn/html';\nimport { type ComponentChildren, Fragment, type VNode } from '@prairielearn/preact-cjs';\n\nimport { renderHtml } from './index.js';\n\n// Based on https://pkg.go.dev/encoding/json#HTMLEscape\nconst ENCODE_HTML_RULES: Record<string, string> = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n};\nconst MATCH_HTML = /[&><\\u2028\\u2029]/g;\n\n/**\n * Escape a value for use in a JSON string that will be rendered in HTML.\n *\n * @param value - The value to escape.\n * @returns A JSON string with HTML-sensitive characters escaped.\n */\nfunction escapeJsonForHtml(value: any): string {\n return superjson.stringify(value).replaceAll(MATCH_HTML, (c) => ENCODE_HTML_RULES[c] || c);\n}\n\n/**\n * Render an entire Preact page as an HTML document.\n *\n * @param content - A Preact VNode to render to HTML.\n * @returns An HTML string containing the rendered content.\n */\nexport function renderHtmlDocument(content: VNode) {\n return `<!doctype html>\\n${renderHtml(content)}`;\n}\n\ninterface HydrateProps {\n /** The component to hydrate */\n children: ComponentChildren;\n /** Optional override for the component's name or displayName */\n nameOverride?: string;\n /** Whether to apply full height styles. */\n fullHeight?: boolean;\n}\n\n/**\n * A component that renders a Preact component for client-side hydration.\n * All interactive components will need to be hydrated.\n * This component is intended to be used within a non-interactive Preact component\n * that will be rendered without hydration through `renderHtml`.\n */\nexport function Hydrate({ children, nameOverride, fullHeight = false }: HydrateProps): VNode {\n if (!isValidElement(children)) {\n throw new Error('<Hydrate> expects a single Preact component as its child');\n }\n\n if (isFragment(children)) {\n throw new Error('<Hydrate> does not support fragments');\n }\n\n const content = children as VNode;\n const { type: Component, props } = content;\n if (typeof Component !== 'function') {\n throw new Error('<Hydrate> expects a Preact component');\n }\n\n // Note that we don't use `Component.name` here because it can be minified or mangled.\n const componentName = nameOverride ?? Component.displayName;\n if (!componentName) {\n // This is only defined in development, not in production when the function name is minified.\n const componentDevName = Component.name || 'UnknownComponent';\n throw new AugmentedError(\n '<Hydrate> expects a component to have a displayName or nameOverride.',\n {\n info: html`\n <div>\n <p>Make sure to add a displayName to the component:</p>\n <pre><code>export const ${componentDevName} = ...;\n// Add this line:\n${componentDevName}.displayName = '${componentDevName}';</code></pre>\n </div>\n `,\n },\n );\n }\n\n const scriptPath = `esm-bundles/hydrated-components/${componentName}.ts`;\n let compiledScriptSrc = '';\n try {\n compiledScriptSrc = compiledScriptPath(scriptPath);\n } catch (error) {\n throw new AugmentedError(`Could not find script for component \"${componentName}\".`, {\n info: html`\n <div>\n Make sure you create a script at\n <code>esm-bundles/hydrated-components/${componentName}.ts</code> registering the\n component:\n <pre><code>import { registerHydratedComponent } from '@prairielearn/preact/hydrated-component';\n\nimport { ${componentName} } from './path/to/component.js';\n\nregisterHydratedComponent(${componentName});</code></pre>\n </div>\n `,\n cause: error,\n });\n }\n const scriptPreloads = compiledScriptPreloadPaths(scriptPath);\n return (\n <Fragment>\n <script type=\"module\" src={compiledScriptSrc} />\n {scriptPreloads.map((preloadPath) => (\n <link key={preloadPath} rel=\"modulepreload\" href={preloadPath} />\n ))}\n <div\n data-component={componentName}\n class={clsx('js-hydrated-component', { 'h-100': fullHeight })}\n >\n <script\n type=\"application/json\"\n // eslint-disable-next-line @eslint-react/dom/no-dangerously-set-innerhtml\n dangerouslySetInnerHTML={{\n __html: escapeJsonForHtml(props),\n }}\n data-component-props\n />\n <div class={fullHeight ? 'h-100' : ''} data-component-root>\n <Component {...props} />\n </div>\n </div>\n </Fragment>\n );\n}\n\n/**\n * Renders a Preact component for client-side hydration and returns an HTML-safe string.\n * This function is intended to be used within a tagged template literal, e.g. html`...`.\n *\n * @param content - A Preact VNode to render to HTML.\n * @returns An `HtmlSafeString` containing the rendered HTML.\n */\nexport function hydrateHtml<T>(content: VNode<T>): HtmlSafeString {\n // Useful for adding Preact components to existing tagged-template pages.\n return renderHtml(<Hydrate>{content}</Hydrate>);\n}\n"]}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.tsx"],"names":[],"mappings":";AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAuB,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAA0B,QAAQ,EAAc,MAAM,0BAA0B,CAAC;AAExF,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,uDAAuD;AACvD,MAAM,iBAAiB,GAA2B;IAChD,GAAG,EAAE,SAAS;IACd,GAAG,EAAE,SAAS;IACd,GAAG,EAAE,SAAS;IACd,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,SAAS;CACpB,CAAC;AACF,MAAM,UAAU,GAAG,oBAAoB,CAAC;AAExC;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,KAAU;IACnC,OAAO,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAc;IAC/C,OAAO,oBAAoB,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;AACnD,CAAC;AAaD;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,EACtB,QAAQ,EACR,YAAY,EACZ,KAAK,EAAE,SAAS,EAChB,UAAU,GAAG,KAAK,GACL;IACb,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,OAAO,GAAG,QAAiB,CAAC;IAClC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC3C,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,sFAAsF;IACtF,MAAM,aAAa,GAAG,YAAY,IAAI,SAAS,CAAC,WAAW,CAAC;IAC5D,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,6FAA6F;QAC7F,MAAM,gBAAgB,GAAG,SAAS,CAAC,IAAI,IAAI,kBAAkB,CAAC;QAC9D,MAAM,IAAI,cAAc,CACtB,sEAAsE,EACtE;YACE,IAAI,EAAE,IAAI,CAAA;;;sCAGoB,gBAAgB;;EAEpD,gBAAgB,mBAAmB,gBAAgB;;SAE5C;SACF,CACF,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,mCAAmC,aAAa,KAAK,CAAC;IACzE,IAAI,iBAAiB,GAAG,EAAE,CAAC;IAC3B,IAAI,CAAC;QACH,iBAAiB,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,cAAc,CAAC,wCAAwC,aAAa,IAAI,EAAE;YAClF,IAAI,EAAE,IAAI,CAAA;;;kDAGkC,aAAa;;;;WAIpD,aAAa;;4BAEI,aAAa;;OAElC;YACD,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,CAAC;IACD,MAAM,cAAc,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC;IAC9D,OAAO,CACL,MAAC,QAAQ,eACP,iBAAQ,IAAI,EAAC,QAAQ,EAAC,GAAG,EAAE,iBAAiB,GAAI,EAC/C,cAAc,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CACnC,eAAwB,GAAG,EAAC,eAAe,EAAC,IAAI,EAAE,WAAW,IAAlD,WAAW,CAA2C,CAClE,CAAC,EACF,iBACE,IAAI,EAAC,kBAAkB;gBACvB,0EAA0E;gBAC1E,uBAAuB,EAAE;oBACvB,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC;iBACjC,oBACe,aAAa,iCAE7B,EACF,gCACkB,aAAa,EAC7B,KAAK,EAAE,IAAI,CAAC,uBAAuB,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,CAAC,YAExE,KAAC,SAAS,OAAK,KAAK,GAAI,GACpB,IACG,CACZ,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,OAAiB,EACjB,QAAwC,EAAE;IAE1C,yEAAyE;IACzE,OAAO,UAAU,CAAC,KAAC,OAAO,OAAK,KAAK,YAAG,OAAO,GAAW,CAAC,CAAC;AAC7D,CAAC","sourcesContent":["import clsx from 'clsx';\nimport { isFragment, isValidElement } from 'preact/compat';\nimport superjson from 'superjson';\n\nimport { compiledScriptPath, compiledScriptPreloadPaths } from '@prairielearn/compiled-assets';\nimport { AugmentedError } from '@prairielearn/error';\nimport { type HtmlSafeString, html } from '@prairielearn/html';\nimport { type ComponentChildren, Fragment, type VNode } from '@prairielearn/preact-cjs';\n\nimport { renderHtml } from './index.js';\n\n// Based on https://pkg.go.dev/encoding/json#HTMLEscape\nconst ENCODE_HTML_RULES: Record<string, string> = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n};\nconst MATCH_HTML = /[&><\\u2028\\u2029]/g;\n\n/**\n * Escape a value for use in a JSON string that will be rendered in HTML.\n *\n * @param value - The value to escape.\n * @returns A JSON string with HTML-sensitive characters escaped.\n */\nfunction escapeJsonForHtml(value: any): string {\n return superjson.stringify(value).replaceAll(MATCH_HTML, (c) => ENCODE_HTML_RULES[c] || c);\n}\n\n/**\n * Render an entire Preact page as an HTML document.\n *\n * @param content - A Preact VNode to render to HTML.\n * @returns An HTML string containing the rendered content.\n */\nexport function renderHtmlDocument(content: VNode) {\n return `<!doctype html>\\n${renderHtml(content)}`;\n}\n\ninterface HydrateProps {\n /** The component to hydrate. */\n children: ComponentChildren;\n /** Optional override for the component's name or displayName. */\n nameOverride?: string;\n /** Whether to apply full height styles. */\n fullHeight?: boolean;\n /** Optional CSS class to apply to the container. */\n class?: string;\n}\n\n/**\n * A component that renders a Preact component for client-side hydration.\n * All interactive components will need to be hydrated.\n * This component is intended to be used within a non-interactive Preact component\n * that will be rendered without hydration through `renderHtml`.\n */\nexport function Hydrate({\n children,\n nameOverride,\n class: className,\n fullHeight = false,\n}: HydrateProps): VNode {\n if (!isValidElement(children)) {\n throw new Error('<Hydrate> expects a single Preact component as its child');\n }\n\n if (isFragment(children)) {\n throw new Error('<Hydrate> does not support fragments');\n }\n\n const content = children as VNode;\n const { type: Component, props } = content;\n if (typeof Component !== 'function') {\n throw new Error('<Hydrate> expects a Preact component');\n }\n\n // Note that we don't use `Component.name` here because it can be minified or mangled.\n const componentName = nameOverride ?? Component.displayName;\n if (!componentName) {\n // This is only defined in development, not in production when the function name is minified.\n const componentDevName = Component.name || 'UnknownComponent';\n throw new AugmentedError(\n '<Hydrate> expects a component to have a displayName or nameOverride.',\n {\n info: html`\n <div>\n <p>Make sure to add a displayName to the component:</p>\n <pre><code>export const ${componentDevName} = ...;\n// Add this line:\n${componentDevName}.displayName = '${componentDevName}';</code></pre>\n </div>\n `,\n },\n );\n }\n\n const scriptPath = `esm-bundles/hydrated-components/${componentName}.ts`;\n let compiledScriptSrc = '';\n try {\n compiledScriptSrc = compiledScriptPath(scriptPath);\n } catch (error) {\n throw new AugmentedError(`Could not find script for component \"${componentName}\".`, {\n info: html`\n <div>\n Make sure you create a script at\n <code>esm-bundles/hydrated-components/${componentName}.ts</code> registering the\n component:\n <pre><code>import { registerHydratedComponent } from '@prairielearn/preact/hydrated-component';\n\nimport { ${componentName} } from './path/to/component.js';\n\nregisterHydratedComponent(${componentName});</code></pre>\n </div>\n `,\n cause: error,\n });\n }\n const scriptPreloads = compiledScriptPreloadPaths(scriptPath);\n return (\n <Fragment>\n <script type=\"module\" src={compiledScriptSrc} />\n {scriptPreloads.map((preloadPath) => (\n <link key={preloadPath} rel=\"modulepreload\" href={preloadPath} />\n ))}\n <script\n type=\"application/json\"\n // eslint-disable-next-line @eslint-react/dom/no-dangerously-set-innerhtml\n dangerouslySetInnerHTML={{\n __html: escapeJsonForHtml(props),\n }}\n data-component={componentName}\n data-component-props\n />\n <div\n data-component={componentName}\n class={clsx('js-hydrated-component', { 'h-100': fullHeight }, className)}\n >\n <Component {...props} />\n </div>\n </Fragment>\n );\n}\n\n/**\n * Renders a Preact component for client-side hydration and returns an HTML-safe string.\n * This function is intended to be used within a tagged template literal, e.g. html`...`.\n *\n * @param content - A Preact VNode to render to HTML.\n * @returns An `HtmlSafeString` containing the rendered HTML.\n */\nexport function hydrateHtml<T>(\n content: VNode<T>,\n props: Omit<HydrateProps, 'children'> = {},\n): HtmlSafeString {\n // Useful for adding Preact components to existing tagged-template pages.\n return renderHtml(<Hydrate {...props}>{content}</Hydrate>);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/preact",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@prairielearn/browser-utils": "^2.5.1",
21
- "@prairielearn/compiled-assets": "^3.2.9",
21
+ "@prairielearn/compiled-assets": "^3.3.0",
22
22
  "@prairielearn/error": "^2.0.22",
23
23
  "@prairielearn/html": "^4.0.22",
24
24
  "@prairielearn/preact-cjs": "^1.1.6",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@prairielearn/tsconfig": "^0.0.0",
34
- "@types/node": "^22.18.13",
34
+ "@types/node": "^22.19.0",
35
35
  "typescript": "^5.9.3"
36
36
  }
37
37
  }
@@ -39,14 +39,15 @@ onDocumentReady(() => {
39
39
  // If you forget to register a component with `registerHydratedComponent`, this is going to hang.
40
40
  const Component = await registry.getComponent(componentName);
41
41
 
42
- const dataElement = el.querySelector('script[data-component-props]');
42
+ const dataElement = el.previousElementSibling;
43
43
  if (!dataElement) throw new Error('No data element found');
44
+ if (!dataElement.hasAttribute('data-component-props')) {
45
+ throw new Error('Data element is missing data-component-props attribute');
46
+ }
44
47
  if (!dataElement.textContent) throw new Error('Data element has no content');
45
48
  const data: object = superjson.parse(dataElement.textContent);
46
49
 
47
- const rootElement = el.querySelector('div[data-component-root]');
48
- if (!rootElement) throw new Error('No root element found');
49
- hydrate(<Component {...data} />, rootElement);
50
+ hydrate(<Component {...data} />, el);
50
51
  },
51
52
  });
52
53
  });
package/src/server.tsx CHANGED
@@ -40,12 +40,14 @@ export function renderHtmlDocument(content: VNode) {
40
40
  }
41
41
 
42
42
  interface HydrateProps {
43
- /** The component to hydrate */
43
+ /** The component to hydrate. */
44
44
  children: ComponentChildren;
45
- /** Optional override for the component's name or displayName */
45
+ /** Optional override for the component's name or displayName. */
46
46
  nameOverride?: string;
47
47
  /** Whether to apply full height styles. */
48
48
  fullHeight?: boolean;
49
+ /** Optional CSS class to apply to the container. */
50
+ class?: string;
49
51
  }
50
52
 
51
53
  /**
@@ -54,7 +56,12 @@ interface HydrateProps {
54
56
  * This component is intended to be used within a non-interactive Preact component
55
57
  * that will be rendered without hydration through `renderHtml`.
56
58
  */
57
- export function Hydrate({ children, nameOverride, fullHeight = false }: HydrateProps): VNode {
59
+ export function Hydrate({
60
+ children,
61
+ nameOverride,
62
+ class: className,
63
+ fullHeight = false,
64
+ }: HydrateProps): VNode {
58
65
  if (!isValidElement(children)) {
59
66
  throw new Error('<Hydrate> expects a single Preact component as its child');
60
67
  }
@@ -117,21 +124,20 @@ registerHydratedComponent(${componentName});</code></pre>
117
124
  {scriptPreloads.map((preloadPath) => (
118
125
  <link key={preloadPath} rel="modulepreload" href={preloadPath} />
119
126
  ))}
127
+ <script
128
+ type="application/json"
129
+ // eslint-disable-next-line @eslint-react/dom/no-dangerously-set-innerhtml
130
+ dangerouslySetInnerHTML={{
131
+ __html: escapeJsonForHtml(props),
132
+ }}
133
+ data-component={componentName}
134
+ data-component-props
135
+ />
120
136
  <div
121
137
  data-component={componentName}
122
- class={clsx('js-hydrated-component', { 'h-100': fullHeight })}
138
+ class={clsx('js-hydrated-component', { 'h-100': fullHeight }, className)}
123
139
  >
124
- <script
125
- type="application/json"
126
- // eslint-disable-next-line @eslint-react/dom/no-dangerously-set-innerhtml
127
- dangerouslySetInnerHTML={{
128
- __html: escapeJsonForHtml(props),
129
- }}
130
- data-component-props
131
- />
132
- <div class={fullHeight ? 'h-100' : ''} data-component-root>
133
- <Component {...props} />
134
- </div>
140
+ <Component {...props} />
135
141
  </div>
136
142
  </Fragment>
137
143
  );
@@ -144,7 +150,10 @@ registerHydratedComponent(${componentName});</code></pre>
144
150
  * @param content - A Preact VNode to render to HTML.
145
151
  * @returns An `HtmlSafeString` containing the rendered HTML.
146
152
  */
147
- export function hydrateHtml<T>(content: VNode<T>): HtmlSafeString {
153
+ export function hydrateHtml<T>(
154
+ content: VNode<T>,
155
+ props: Omit<HydrateProps, 'children'> = {},
156
+ ): HtmlSafeString {
148
157
  // Useful for adding Preact components to existing tagged-template pages.
149
- return renderHtml(<Hydrate>{content}</Hydrate>);
158
+ return renderHtml(<Hydrate {...props}>{content}</Hydrate>);
150
159
  }