@fusion.dev/helix 0.199.1 → 0.200.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.
- package/api.json +1 -1
- package/custom-elements.json +4817 -226
- package/dist/element-events.generated.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log/group/log-group.css.d.ts +1 -0
- package/dist/log/group/log-group.css.js +2 -0
- package/dist/log/group/log-group.d.ts +24 -0
- package/dist/log/group/log-group.js +12 -0
- package/dist/log/index.d.ts +3 -0
- package/dist/log/index.js +3 -0
- package/dist/log/line/log-line.css.d.ts +1 -0
- package/dist/log/line/log-line.css.js +2 -0
- package/dist/log/line/log-line.d.ts +28 -0
- package/dist/log/line/log-line.js +12 -0
- package/dist/log/root/log-root.css.d.ts +1 -0
- package/dist/log/root/log-root.css.js +2 -0
- package/dist/log/root/log-root.d.ts +47 -0
- package/dist/log/root/log-root.js +16 -0
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/preview/sandbox-preview.d.ts +35 -1
- package/dist/sandbox/preview/sandbox-preview.js +40 -0
- package/dist/sandbox/relay.d.ts +74 -0
- package/dist/sandbox/relay.js +72 -0
- package/examples.json +1 -1
- package/lint.json +1 -1
- package/package.json +6 -6
- package/skills/helix/SKILL.md +2 -2
- package/types/jsx.d.ts +1329 -0
- package/types/svelte.d.ts +3 -0
- package/types/vue.d.ts +306 -1
- package/types/words.d.ts +48 -0
|
@@ -173,6 +173,8 @@ registerElementEvents({
|
|
|
173
173
|
'helix-reorder',
|
|
174
174
|
'input',
|
|
175
175
|
],
|
|
176
|
+
'helix-log': ['helix-before-pin', 'helix-expand', 'helix-pin'],
|
|
177
|
+
'helix-log-group': ['helix-before-close', 'helix-before-open', 'toggle'],
|
|
176
178
|
'helix-map': [
|
|
177
179
|
'change',
|
|
178
180
|
'helix-activate',
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export * from './line-chart/index.js';
|
|
|
60
60
|
export * from './list/index.js';
|
|
61
61
|
export * from './listbox/index.js';
|
|
62
62
|
export * from './loader/index.js';
|
|
63
|
+
export * from './log/index.js';
|
|
63
64
|
export * from './map/index.js';
|
|
64
65
|
export * from './megamenu/index.js';
|
|
65
66
|
export * from './menu/filter/index.js';
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ export * from './line-chart/index.js';
|
|
|
55
55
|
export * from './list/index.js';
|
|
56
56
|
export * from './listbox/index.js';
|
|
57
57
|
export * from './loader/index.js';
|
|
58
|
+
export * from './log/index.js';
|
|
58
59
|
export * from './map/index.js';
|
|
59
60
|
export * from './megamenu/index.js';
|
|
60
61
|
export * from './menu/filter/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { createSheet } from '@fusion.dev/helix-core';
|
|
2
|
+
export const styles = createSheet(`:host {display: block}[part="summary"] {all: unset;box-sizing: border-box;align-items: center;color: var(--helix-color-foreground);column-gap: var(--helix-space-xs);cursor: pointer;display: flex;font-family: var(--helix-font-sans);font-weight: var(--helix-weight-medium);inline-size: 100%;padding-block: var(--helix-space-2xs);padding-inline: var(--log-padding-inline, var(--helix-space-md));white-space: nowrap}[part="summary"]:hover {background-color: color-mix(in oklab, var(--helix-color-foreground) 5%, transparent)}[part="summary"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1)}[part="mark"] {block-size: var(--helix-size-icon-sm);color: var(--helix-color-foreground-subtle);flex: none;font-family: var(--log-font-family, var(--helix-font-mono));inline-size: var(--helix-size-icon-sm);margin-inline-end: calc(var(--helix-space-md) - var(--helix-space-xs));margin-inline-start: calc(max(var(--helix-size-icon-sm), calc(var(--log-digits, 2) * 1ch)) - var(--helix-size-icon-sm));rotate: -90deg;transition: rotate var(--helix-duration-fast) var(--helix-easing-standard)}:host(:dir(rtl)) [part="mark"] {rotate: 90deg}:host(:state(open)) [part="mark"] {rotate: none}[part="status"] {block-size: var(--helix-size-icon-sm);color: var(--helix-color-foreground-subtle);flex: none;inline-size: var(--helix-size-icon-sm)}:host([status="done"]) [part="status"] {color: var(--helix-color-success)}:host([status="failed"]) [part="status"] {color: var(--helix-color-danger)}:host([status="running"]) [part="status"] {color: var(--helix-color-info)}[part="heading"] {min-inline-size: 0;overflow: hidden;text-overflow: ellipsis}[part="duration"] {color: var(--helix-color-foreground-muted);flex: none;font-variant-numeric: tabular-nums;font-weight: var(--helix-weight-regular);margin-inline-start: auto}[part="content"][hidden] {display: none}@media print {:host {print-color-adjust: economy}}`);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HelixHeadlessLogGroup } from '@fusion.dev/helix-headless/log';
|
|
2
|
+
import type { CSSResultGroup } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* @summary One step of a log, which folds its lines under a heading with its status and how long it took.
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* The heading is a row in the sans face with a caret, the status mark in its
|
|
8
|
+
* color, the heading, and the duration at the far end. A step that is `done`
|
|
9
|
+
* takes the success color, one that `failed` the danger color, and one that
|
|
10
|
+
* is `running` the info color.
|
|
11
|
+
*
|
|
12
|
+
* @since 0.199.0
|
|
13
|
+
* @status experimental
|
|
14
|
+
* @documentation https://fusion.dev/helix/components/log
|
|
15
|
+
*/
|
|
16
|
+
export declare class HelixLogGroup extends HelixHeadlessLogGroup {
|
|
17
|
+
static tier: string;
|
|
18
|
+
static styles: CSSResultGroup;
|
|
19
|
+
}
|
|
20
|
+
declare global {
|
|
21
|
+
interface HTMLElementTagNameMap {
|
|
22
|
+
'helix-log-group': HelixLogGroup;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { customElement } from '@fusion.dev/helix-core';
|
|
3
|
+
import { HelixHeadlessLogGroup } from '@fusion.dev/helix-headless/log';
|
|
4
|
+
import { styles } from './log-group.css.js';
|
|
5
|
+
let HelixLogGroup = class HelixLogGroup extends HelixHeadlessLogGroup {
|
|
6
|
+
static { this.tier = 'helix-'; }
|
|
7
|
+
static { this.styles = styles; }
|
|
8
|
+
};
|
|
9
|
+
HelixLogGroup = __decorate([
|
|
10
|
+
customElement('helix-log-group')
|
|
11
|
+
], HelixLogGroup);
|
|
12
|
+
export { HelixLogGroup };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { createSheet } from '@fusion.dev/helix-core';
|
|
2
|
+
export const styles = createSheet(`:host {display: block}[part~="line"] {column-gap: var(--helix-space-md);display: flex;inline-size: var(--_log-line-size, max-content);min-inline-size: 100%;padding-inline: var(--log-padding-inline, var(--helix-space-md))}[part~="line"]:hover {background-color: color-mix(in oklab, var(--helix-color-foreground) 4%, transparent)}[part="number"] {color: var(--helix-color-foreground-muted);flex: none;font-variant-numeric: tabular-nums;min-inline-size: max(var(--helix-size-icon-sm), calc(var(--log-digits, 2) * 1ch));text-align: end;text-decoration: none;user-select: none}a[part="number"]:hover {color: var(--helix-color-foreground);text-decoration: underline}a[part="number"]:focus-visible {border-radius: var(--helix-radii-sm);outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1)}[part="time"] {color: var(--helix-color-foreground-muted);flex: none;font-variant-numeric: tabular-nums;user-select: none}[part="text"] {flex: 1 1 auto;min-inline-size: 0;overflow-wrap: anywhere;white-space: var(--_log-white-space, pre)}[part~="line-debug"] {color: var(--helix-color-foreground-subtle)}[part~="line-warn"] {background-color: color-mix(in oklab, var(--log-warn-color, var(--helix-color-warning)) 10%, transparent);color: var(--log-warn-color, var(--helix-color-warning))}[part~="line-error"] {background-color: color-mix(in oklab, var(--log-error-color, var(--helix-color-danger)) 10%, transparent);color: var(--log-error-color, var(--helix-color-danger))}[part~="line-marked"] {background-color: var(--log-mark-background-color, color-mix(in oklab, var(--helix-color-primary) 12%, transparent))}[part~="run"] {background-color: var(--log-run-background-color, transparent);color: var(--log-run-color, inherit)}[part~="run-bold"] {font-weight: var(--helix-weight-semibold)}[part~="run-dim"] {opacity: 0.7}[part~="run-italic"] {font-style: italic}[part~="run-underline"] {text-decoration-line: underline}[part~="run-strike"] {text-decoration-line: line-through}[part~="run-underline"][part~="run-strike"] {text-decoration-line: underline line-through}[part="match"] {background-color: var(--log-match-background-color, var(--helix-color-warning-surface));border-radius: var(--helix-radii-sm);color: inherit}@media print {[part~="line"] {print-color-adjust: exact}}`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HelixHeadlessLogLine } from '@fusion.dev/helix-headless/log';
|
|
2
|
+
import type { CSSResultGroup } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* @summary One line of a log, with its number, its time, and its text.
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* A row with the number, the time, and the text side by side. A line at
|
|
8
|
+
* `warn` or `error` takes a band of its color, and a line `mark-lines` names
|
|
9
|
+
* takes the band `Code Block` draws behind a marked line.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.199.0
|
|
12
|
+
* @status experimental
|
|
13
|
+
* @documentation https://fusion.dev/helix/components/log
|
|
14
|
+
*
|
|
15
|
+
* @cssproperty --log-error-color - The color of a line at `error`, and of the band behind it. Defaults to the danger color.
|
|
16
|
+
* @cssproperty --log-mark-background-color - The band behind a line `mark-lines` names.
|
|
17
|
+
* @cssproperty --log-match-background-color - The fill behind text that matches the log's `query`.
|
|
18
|
+
* @cssproperty --log-warn-color - The color of a line at `warn`, and of the band behind it. Defaults to the warning color.
|
|
19
|
+
*/
|
|
20
|
+
export declare class HelixLogLine extends HelixHeadlessLogLine {
|
|
21
|
+
static tier: string;
|
|
22
|
+
static styles: CSSResultGroup;
|
|
23
|
+
}
|
|
24
|
+
declare global {
|
|
25
|
+
interface HTMLElementTagNameMap {
|
|
26
|
+
'helix-log-line': HelixLogLine;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { customElement } from '@fusion.dev/helix-core';
|
|
3
|
+
import { HelixHeadlessLogLine } from '@fusion.dev/helix-headless/log';
|
|
4
|
+
import { styles } from './log-line.css.js';
|
|
5
|
+
let HelixLogLine = class HelixLogLine extends HelixHeadlessLogLine {
|
|
6
|
+
static { this.tier = 'helix-'; }
|
|
7
|
+
static { this.styles = styles; }
|
|
8
|
+
};
|
|
9
|
+
HelixLogLine = __decorate([
|
|
10
|
+
customElement('helix-log-line')
|
|
11
|
+
], HelixLogLine);
|
|
12
|
+
export { HelixLogLine };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { createSheet } from '@fusion.dev/helix-core';
|
|
2
|
+
export const styles = createSheet(`:host {--log-ansi-background: var(--log-background-color, var(--helix-color-background));--log-ansi-black: var(--helix-color-foreground);--log-ansi-blue: var(--helix-color-info);--log-ansi-bright-black: var(--helix-color-foreground-subtle);--log-ansi-bright-blue: var(--helix-color-info);--log-ansi-bright-cyan: color-mix(in oklab, var(--helix-color-info), var(--helix-color-success));--log-ansi-bright-green: var(--helix-color-success);--log-ansi-bright-magenta: var(--helix-color-syntax-callable);--log-ansi-bright-red: var(--helix-color-danger);--log-ansi-bright-white: var(--helix-color-foreground);--log-ansi-bright-yellow: var(--helix-color-warning);--log-ansi-cyan: color-mix(in oklab, var(--helix-color-info), var(--helix-color-success));--log-ansi-foreground: var(--log-color, var(--helix-color-foreground));--log-ansi-green: var(--helix-color-success);--log-ansi-magenta: var(--helix-color-syntax-callable);--log-ansi-red: var(--helix-color-danger);--log-ansi-white: var(--helix-color-foreground-muted);--log-ansi-yellow: var(--helix-color-warning);background-color: var(--log-background-color, var(--helix-color-background));border: var(--helix-border-thin) solid var(--log-border-color, var(--helix-color-border));border-radius: var(--log-radius, var(--helix-radii-lg));color: var(--log-color, var(--helix-color-foreground));display: grid;font-family: var(--log-font-family, var(--helix-font-mono));font-size: var(--log-font-size, var(--helix-text-xs));grid-template-columns: minmax(0, 1fr);grid-template-rows: auto auto minmax(0, 1fr) auto;line-height: var(--log-line-height, var(--helix-leading-normal));max-block-size: var(--log-max-block-size, 30rem);min-block-size: 0;overflow: hidden;tab-size: 4}:host([wrap]) {--_log-line-size: auto;--_log-white-space: pre-wrap}::slotted([slot="toolbar"]) {--helix-size-control: var(--helix-size-control-sm);--helix-size-control-loose: calc(var(--helix-size-control) * 1.2);--helix-size-control-tight: calc(var(--helix-size-control) * 0.8);--helix-size-icon: var(--helix-size-icon-sm);--helix-size-icon-loose: calc(var(--helix-size-icon) * 1.2);--helix-size-icon-tight: calc(var(--helix-size-icon) * 0.8);border-block-end: var(--helix-border-thin) solid var(--log-border-color, var(--helix-color-border));font-family: var(--helix-font-sans);grid-row: 1;padding-block: var(--helix-space-xs);padding-inline: var(--log-padding-inline, var(--helix-space-md))}[part="dropped"] {background-color: var(--helix-color-surface-sunken);border-block-end: var(--helix-border-thin) solid var(--log-border-color, var(--helix-color-border));color: var(--helix-color-foreground-muted);font-family: var(--helix-font-sans);grid-row: 2;margin: var(--helix-space-none);padding-block: var(--helix-space-2xs);padding-inline: var(--log-padding-inline, var(--helix-space-md))}[part="dropped"][hidden], [part="resume"][hidden] {display: none}[part="log"] {grid-row: 3;overflow: auto;overscroll-behavior: contain;padding-block: var(--helix-space-xs);scrollbar-color: var(--log-scrollbar-thumb-color, var(--helix-scrollbar-thumb-color)) var(--log-scrollbar-track-color, var(--helix-scrollbar-track-color));scrollbar-width: thin}[part="log"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1)}[part="row"] {min-inline-size: 100%;inline-size: var(--_log-line-size, max-content)}[part="resume"] {display: grid;grid-row: 4;justify-items: center;margin-block-start: calc(var(--helix-space-xl) * -1 - var(--helix-space-xs));padding-block-end: var(--helix-space-xs);pointer-events: none;position: relative}[part="resume"] > * {pointer-events: auto}[part="resume"] slot::slotted(*) {border: var(--helix-border-floating) solid var(--log-border-color, var(--helix-color-border));box-shadow: var(--helix-shadow-floating)}[part="resume-button"] {all: unset;box-sizing: border-box;align-items: center;background-color: var(--helix-color-surface-raised);block-size: var(--helix-size-control-sm);border: var(--helix-border-floating) solid var(--log-border-color, var(--helix-color-border));border-radius: var(--helix-radii-sm);box-shadow: var(--helix-shadow-floating);color: var(--helix-color-foreground);cursor: pointer;display: inline-flex;font-family: var(--helix-font-sans);font-size: var(--helix-text-xs);font-weight: var(--helix-weight-medium);gap: var(--helix-space-2xs);line-height: var(--helix-leading-tight);padding-inline: var(--helix-space-sm);white-space: nowrap}[part="resume-button"]:hover {background-color: color-mix(in oklab, var(--helix-color-foreground) 6%, var(--helix-color-surface-raised))}[part="resume-button"]:focus-visible {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: var(--helix-size-focus-offset)}[part="resume-mark"] {block-size: var(--helix-size-icon-sm);display: inline-flex;inline-size: var(--helix-size-icon-sm)}[part="resume-mark"] svg {block-size: 100%;inline-size: 100%}[part="empty"], [part="loading"] {--_collection-inset-block: var(--helix-space-lg);--_collection-inset-inline: var(--helix-space-md)}@media print {:host {max-block-size: none}[part="log"] {overflow: visible}[part="resume"] {display: none}}`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { HelixHeadlessLog } from '@fusion.dev/helix-headless/log';
|
|
2
|
+
import type { CSSResultGroup } from 'lit';
|
|
3
|
+
import '../group/log-group.js';
|
|
4
|
+
import '../line/log-line.js';
|
|
5
|
+
/**
|
|
6
|
+
* @summary Output that arrives while you watch it, such as a CI run, a deployment, or a build, in a box that follows the newest line.
|
|
7
|
+
*
|
|
8
|
+
* @description
|
|
9
|
+
* A bordered box in the monospaced face, at the size of the page's small
|
|
10
|
+
* text, that scrolls after 30rem (`--log-max-block-size`). Lines keep their
|
|
11
|
+
* spaces and run past the edge of the box, which scrolls sideways, until
|
|
12
|
+
* `wrap` is set.
|
|
13
|
+
*
|
|
14
|
+
* The sixteen ANSI colors come from your theme: red is the danger color,
|
|
15
|
+
* green success, yellow warning, and blue info. Set `--log-ansi-red` and its
|
|
16
|
+
* siblings on the log to choose them yourself.
|
|
17
|
+
*
|
|
18
|
+
* Put a search field or the controls for the log in the `toolbar` slot, in
|
|
19
|
+
* one element such as a `Toolbar` or a `Flex`. It sits above the lines with a
|
|
20
|
+
* rule under it.
|
|
21
|
+
*
|
|
22
|
+
* @since 0.199.0
|
|
23
|
+
* @status experimental
|
|
24
|
+
* @category feedback
|
|
25
|
+
* @documentation https://fusion.dev/helix/components/log
|
|
26
|
+
*
|
|
27
|
+
* @cssproperty --log-background-color - The fill behind the lines. Defaults to the page background.
|
|
28
|
+
* @cssproperty --log-border-color - The edge around the log and the rules inside it.
|
|
29
|
+
* @cssproperty --log-color - The text color of the lines.
|
|
30
|
+
* @cssproperty --log-font-family - The face of the lines. Defaults to the monospaced face.
|
|
31
|
+
* @cssproperty --log-font-size - The size of the lines. Defaults to the small text step.
|
|
32
|
+
* @cssproperty --log-line-height - The height of one line.
|
|
33
|
+
* @cssproperty --log-max-block-size - How tall the log may get before the lines scroll. Defaults to 30rem.
|
|
34
|
+
* @cssproperty --log-padding-inline - Space either side of every line and heading.
|
|
35
|
+
* @cssproperty --log-radius - The corners of the log.
|
|
36
|
+
* @cssproperty --log-scrollbar-thumb-color - The thumb of the scrollbar. Defaults to `--helix-scrollbar-thumb-color`.
|
|
37
|
+
* @cssproperty --log-scrollbar-track-color - The track of the scrollbar. Defaults to `--helix-scrollbar-track-color`.
|
|
38
|
+
*/
|
|
39
|
+
export declare class HelixLog extends HelixHeadlessLog {
|
|
40
|
+
static tier: string;
|
|
41
|
+
static styles: CSSResultGroup;
|
|
42
|
+
}
|
|
43
|
+
declare global {
|
|
44
|
+
interface HTMLElementTagNameMap {
|
|
45
|
+
'helix-log': HelixLog;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { customElement } from '@fusion.dev/helix-core';
|
|
3
|
+
import { HelixHeadlessLog } from '@fusion.dev/helix-headless/log';
|
|
4
|
+
import { styles as collectionStateStyles } from '../../shared/collection/state.css.js';
|
|
5
|
+
import { styles as liveStyles } from '../../shared/live/live.css.js';
|
|
6
|
+
import { styles } from './log-root.css.js';
|
|
7
|
+
import '../group/log-group.js';
|
|
8
|
+
import '../line/log-line.js';
|
|
9
|
+
let HelixLog = class HelixLog extends HelixHeadlessLog {
|
|
10
|
+
static { this.tier = 'helix-'; }
|
|
11
|
+
static { this.styles = [liveStyles, collectionStateStyles, styles]; }
|
|
12
|
+
};
|
|
13
|
+
HelixLog = __decorate([
|
|
14
|
+
customElement('helix-log')
|
|
15
|
+
], HelixLog);
|
|
16
|
+
export { HelixLog };
|
package/dist/sandbox/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export { isRender, LAYOUT_RULES, loggedText, type ServePreviewOptions, servePreview, withoutStylesheets, } from './frame.js';
|
|
16
16
|
export * from './preview/sandbox-preview.js';
|
|
17
|
+
export type { PreviewFetch, PreviewFetched } from './relay.js';
|
|
17
18
|
export * from './root/sandbox-root.js';
|
|
18
19
|
export { type ManifestLike, manifestWords } from './source/editing.js';
|
|
19
20
|
export * from './source/sandbox-source.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HelixElement } from '@fusion.dev/helix-core';
|
|
2
2
|
import type { CSSResultGroup } from 'lit';
|
|
3
3
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
4
|
+
import { type PreviewFetch } from '../relay.js';
|
|
4
5
|
/** The frame saying it has a listener, and where to answer it. */
|
|
5
6
|
export interface PreviewReady {
|
|
6
7
|
type: 'helix-preview-ready';
|
|
@@ -39,7 +40,7 @@ export interface PreviewSize {
|
|
|
39
40
|
size: number;
|
|
40
41
|
type: 'helix-preview-size';
|
|
41
42
|
}
|
|
42
|
-
export type PreviewMessage = PreviewConsole | PreviewPress | PreviewReady | PreviewRender | PreviewSize;
|
|
43
|
+
export type PreviewMessage = PreviewConsole | PreviewFetch | PreviewPress | PreviewReady | PreviewRender | PreviewSize;
|
|
43
44
|
/**
|
|
44
45
|
* @summary A frame that renders sandbox source in a separate document.
|
|
45
46
|
*
|
|
@@ -106,6 +107,21 @@ export type PreviewMessage = PreviewConsole | PreviewPress | PreviewReady | Prev
|
|
|
106
107
|
* reader reads it with the frame. Hide it with `::part(error) { display: none }`
|
|
107
108
|
* where the page draws its own.
|
|
108
109
|
*
|
|
110
|
+
* **`relay` lets a frame on a login-protected site load its own files.** The
|
|
111
|
+
* frame's opaque origin makes every request it sends cross-site, and a login
|
|
112
|
+
* cookie is not sent on one, so behind a login such as a Vercel preview the
|
|
113
|
+
* frame's script and stylesheet come back as the login page. With `relay` set
|
|
114
|
+
* to path prefixes, such as `/sandbox/ /fonts/`, the frame can ask this
|
|
115
|
+
* element for a file under them and gets the body back, fetched from this
|
|
116
|
+
* page with its cookies. The page fetches only a `GET` on its own origin,
|
|
117
|
+
* under those prefixes, in plain characters, following no redirect, and it
|
|
118
|
+
* passes on only a script, a stylesheet, a font, a model, or an image of up
|
|
119
|
+
* to 8 MB. Name only prefixes that hold files the site serves to anyone: an
|
|
120
|
+
* example in the frame is untrusted, and it can ask too. Empty, the default,
|
|
121
|
+
* relays nothing. A frame asks only while its own origin is opaque, so a page
|
|
122
|
+
* elsewhere that frames the same document without a sandbox cannot use the
|
|
123
|
+
* answer to run script on the site.
|
|
124
|
+
*
|
|
109
125
|
* **`controls` draws two buttons in the preview's corner**: one reloads the
|
|
110
126
|
* frame's document, as `reload()` does, and one opens the example in a new
|
|
111
127
|
* tab, as `open()` does. The tab runs it in a frame sandboxed as this one is.
|
|
@@ -211,6 +227,12 @@ export declare class HelixSandboxPreview extends HelixElement {
|
|
|
211
227
|
* `controls`.
|
|
212
228
|
*/
|
|
213
229
|
openLabel: string;
|
|
230
|
+
/**
|
|
231
|
+
* Path prefixes the frame may ask this page to fetch for it, separated by
|
|
232
|
+
* spaces, each starting and ending with a slash, such as `/sandbox/ /fonts/`.
|
|
233
|
+
* Empty relays nothing.
|
|
234
|
+
*/
|
|
235
|
+
relay: string;
|
|
214
236
|
/**
|
|
215
237
|
* The name of the button that reloads the frame's document, under
|
|
216
238
|
* `controls`.
|
|
@@ -252,6 +274,18 @@ export declare class HelixSandboxPreview extends HelixElement {
|
|
|
252
274
|
* imports.
|
|
253
275
|
*/
|
|
254
276
|
private map;
|
|
277
|
+
/** How many files the frame has asked for that are still being fetched. */
|
|
278
|
+
private relaying;
|
|
279
|
+
/**
|
|
280
|
+
* Fetches a file the frame asked for and sends it back to the window that
|
|
281
|
+
* asked, or says it could not.
|
|
282
|
+
*
|
|
283
|
+
* The address is judged by `relayAddress` against this page's origin and the
|
|
284
|
+
* prefixes in `relay`, so a request this page would not make is answered
|
|
285
|
+
* with a refusal and never sent. The frame document's address is the base,
|
|
286
|
+
* so a relative path means what it meant there.
|
|
287
|
+
*/
|
|
288
|
+
private answer;
|
|
255
289
|
/** Says the preview failed with `text`, or, with none, that it has not. */
|
|
256
290
|
private fail;
|
|
257
291
|
/** What the frame is sent as its source: the markup, and the script after it. */
|
|
@@ -6,6 +6,7 @@ import { html, nothing } from 'lit';
|
|
|
6
6
|
import { property, state } from 'lit/decorators.js';
|
|
7
7
|
import { COMPILED, compile } from '../compile.js';
|
|
8
8
|
import { defaultFrame, withScript } from '../frame.js';
|
|
9
|
+
import { isFetch, RELAY_OPEN, relayAddress, relayFetch, relayPrefixes, } from '../relay.js';
|
|
9
10
|
import { styles as sheet } from './sandbox-preview.css.js';
|
|
10
11
|
const SANDBOX = 'allow-scripts allow-forms';
|
|
11
12
|
const RELATIVE = /(\bfrom\s*|\bimport\s*\(?\s*)(["'])(\.{1,2}\/[^"']+)\2/g;
|
|
@@ -17,6 +18,7 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
|
|
|
17
18
|
this.controls = false;
|
|
18
19
|
this.label = 'Preview';
|
|
19
20
|
this.openLabel = 'Open preview in a new tab';
|
|
21
|
+
this.relay = '';
|
|
20
22
|
this.reloadLabel = 'Reload preview';
|
|
21
23
|
this.script = '';
|
|
22
24
|
this.source = '';
|
|
@@ -26,6 +28,7 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
|
|
|
26
28
|
this.compiled = new Map();
|
|
27
29
|
this.compiling = new Set();
|
|
28
30
|
this.map = {};
|
|
31
|
+
this.relaying = 0;
|
|
29
32
|
this.handleFrameLoad = () => {
|
|
30
33
|
this.ready = true;
|
|
31
34
|
this.toggleState('loading', false);
|
|
@@ -36,6 +39,10 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
|
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
41
|
const data = event.data;
|
|
42
|
+
if (isFetch(data)) {
|
|
43
|
+
void this.answer(data, event.source);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
39
46
|
if (data?.type === 'helix-preview-ready') {
|
|
40
47
|
this.handleFrameLoad();
|
|
41
48
|
return;
|
|
@@ -59,6 +66,27 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
|
|
|
59
66
|
}
|
|
60
67
|
static { HelixSandboxPreview_1 = this; }
|
|
61
68
|
static { this.words = 'helix-sandbox-preview'; }
|
|
69
|
+
async answer(request, to) {
|
|
70
|
+
const reply = (fetched) => {
|
|
71
|
+
const message = { ...fetched, id: request.id, type: 'helix-preview-fetched' };
|
|
72
|
+
const transfer = fetched.body instanceof ArrayBuffer ? [fetched.body] : [];
|
|
73
|
+
to.postMessage(message, { targetOrigin: HelixSandboxPreview_1.target, transfer });
|
|
74
|
+
};
|
|
75
|
+
const base = new URL(this.src || '/', location.href).href;
|
|
76
|
+
const url = relayAddress(request.path, base, location.origin, relayPrefixes(this.relay));
|
|
77
|
+
if (!url || this.relaying >= RELAY_OPEN) {
|
|
78
|
+
reply({ ok: false });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.relaying += 1;
|
|
82
|
+
try {
|
|
83
|
+
const fetched = await relayFetch(url, request.as);
|
|
84
|
+
reply(fetched ? { ...fetched, ok: true } : { ok: false });
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.relaying -= 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
62
90
|
fail(text) {
|
|
63
91
|
this.failure = text;
|
|
64
92
|
this.toggleState('failed', text !== undefined);
|
|
@@ -155,6 +183,15 @@ let HelixSandboxPreview = class HelixSandboxPreview extends HelixElement {
|
|
|
155
183
|
else {
|
|
156
184
|
frame.src = this.frame?.src || this.src;
|
|
157
185
|
}
|
|
186
|
+
if (relayPrefixes(this.relay).length > 0) {
|
|
187
|
+
tab.addEventListener('message', (event) => {
|
|
188
|
+
if (event.source === frame.contentWindow &&
|
|
189
|
+
event.origin === 'null' &&
|
|
190
|
+
isFetch(event.data)) {
|
|
191
|
+
void this.answer(event.data, event.source);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
158
195
|
const send = tab.document.createElement('script');
|
|
159
196
|
send.textContent = `const frame = document.currentScript.previousElementSibling
|
|
160
197
|
frame.onload = () => frame.contentWindow.postMessage(${JSON.stringify(this.message)}, '*')`;
|
|
@@ -270,6 +307,9 @@ __decorate([
|
|
|
270
307
|
words,
|
|
271
308
|
property({ attribute: 'open-label' })
|
|
272
309
|
], HelixSandboxPreview.prototype, "openLabel", void 0);
|
|
310
|
+
__decorate([
|
|
311
|
+
property()
|
|
312
|
+
], HelixSandboxPreview.prototype, "relay", void 0);
|
|
273
313
|
__decorate([
|
|
274
314
|
words,
|
|
275
315
|
property({ attribute: 'reload-label' })
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's half of a relay that fetches a frame's own files for it.
|
|
3
|
+
*
|
|
4
|
+
* **A sandboxed frame cannot always load its own files.** It has an opaque
|
|
5
|
+
* origin, so every request it makes is cross-site. Where the site sits behind
|
|
6
|
+
* a login, such as a Vercel preview, the cookie that proves the login is not
|
|
7
|
+
* sent on a cross-site request, and the frame's script, its stylesheet, its
|
|
8
|
+
* fonts, and its models come back as the login page. The page around the frame
|
|
9
|
+
* is on the site's own origin and sends the cookie, so the frame asks it.
|
|
10
|
+
*
|
|
11
|
+
* **What the frame can ask for is narrow, and the page decides it.** A path
|
|
12
|
+
* is fetched only where it resolves to the page's own origin, under one of the
|
|
13
|
+
* prefixes the page named, in plain characters, with a `GET` that follows no
|
|
14
|
+
* redirect, and the answer is passed on only when it is a script, a
|
|
15
|
+
* stylesheet, a font, a model, or an image under the size limit. An example in
|
|
16
|
+
* the frame is untrusted, and with this it can read only what the site already
|
|
17
|
+
* serves to anyone under those prefixes.
|
|
18
|
+
*/
|
|
19
|
+
/** A file the frame asks the page to fetch for it. */
|
|
20
|
+
export interface PreviewFetch {
|
|
21
|
+
/** Whether the frame wants the body as text, for a script or a stylesheet, or as bytes. */
|
|
22
|
+
as: 'bytes' | 'text';
|
|
23
|
+
/** What the answer carries back, so the frame can match it to the request. */
|
|
24
|
+
id: string;
|
|
25
|
+
/** The file, as a path on the site, such as `/sandbox/frame.js?v=1a2b3c4d`. */
|
|
26
|
+
path: string;
|
|
27
|
+
type: 'helix-preview-fetch';
|
|
28
|
+
}
|
|
29
|
+
/** The page's answer to a `PreviewFetch`. */
|
|
30
|
+
export interface PreviewFetched {
|
|
31
|
+
/** The file's body, where the page fetched it. */
|
|
32
|
+
body?: ArrayBuffer | string;
|
|
33
|
+
/** The file's content type, where the page fetched it. */
|
|
34
|
+
contentType?: string;
|
|
35
|
+
/** The `id` of the request it answers. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Whether the page fetched the file. */
|
|
38
|
+
ok: boolean;
|
|
39
|
+
type: 'helix-preview-fetched';
|
|
40
|
+
}
|
|
41
|
+
/** The most bytes the page passes on for one file: four times the largest the site serves. */
|
|
42
|
+
export declare const RELAY_LIMIT: number;
|
|
43
|
+
/** The most requests one frame may have open at once. */
|
|
44
|
+
export declare const RELAY_OPEN = 16;
|
|
45
|
+
/** Whether `data` is a request the frame sent, in the shape the page reads. */
|
|
46
|
+
export declare const isFetch: (data: unknown) => data is PreviewFetch;
|
|
47
|
+
/**
|
|
48
|
+
* The prefixes a `relay` value names: each a path that starts and ends with a
|
|
49
|
+
* slash, such as `/sandbox/`. Anything else in the value is ignored, so `/`
|
|
50
|
+
* alone, which would be the whole site, is not a prefix.
|
|
51
|
+
*/
|
|
52
|
+
export declare const relayPrefixes: (value: string) => string[];
|
|
53
|
+
/**
|
|
54
|
+
* The address the page fetches for `path`, or undefined where it may not.
|
|
55
|
+
*
|
|
56
|
+
* `path` is resolved against `base`, the frame document's address, so a
|
|
57
|
+
* relative path the frame wrote means what it meant there. The answer has to
|
|
58
|
+
* be on `origin`, the page's own, under one of `prefixes`, with no user, no
|
|
59
|
+
* password, and no fragment. The resolved path is held to plain characters, so
|
|
60
|
+
* an encoded slash or dot, which a server may decode after this check, is
|
|
61
|
+
* refused rather than trusted.
|
|
62
|
+
*/
|
|
63
|
+
export declare const relayAddress: (path: string, base: string, origin: string, prefixes: readonly string[]) => URL | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Fetches a file the frame asked for, from an address `relayAddress` allowed,
|
|
66
|
+
* and returns it, or undefined where it is not one to pass on.
|
|
67
|
+
*
|
|
68
|
+
* Same origin only, with the page's cookies, as a `GET` that follows no
|
|
69
|
+
* redirect: a login page redirecting is a refusal, never a file. The body is
|
|
70
|
+
* passed on only for a script, a stylesheet, a font, a model, or an image, and
|
|
71
|
+
* only under `RELAY_LIMIT`, counted on what arrived as well as on what the
|
|
72
|
+
* headers claimed.
|
|
73
|
+
*/
|
|
74
|
+
export declare const relayFetch: (url: URL, as: PreviewFetch['as']) => Promise<Pick<PreviewFetched, 'body' | 'contentType'> | undefined>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const RELAY_LIMIT = 8 * 1024 * 1024;
|
|
2
|
+
export const RELAY_OPEN = 16;
|
|
3
|
+
const PATH = /^\/[A-Za-z0-9._~/-]*$/;
|
|
4
|
+
const QUERY = /^(?:\?[A-Za-z0-9=&._~-]*)?$/;
|
|
5
|
+
const KINDS = /^(?:text\/css|text\/javascript|application\/javascript|application\/x-javascript|font\/[a-z0-9.+-]+|model\/[a-z0-9.+-]+|image\/(?:png|jpeg|webp|avif|gif)|application\/octet-stream)(?:\s*;|$)/;
|
|
6
|
+
export const isFetch = (data) => {
|
|
7
|
+
if (typeof data !== 'object' || data === null) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const { as, id, path, type } = data;
|
|
11
|
+
return (type === 'helix-preview-fetch' &&
|
|
12
|
+
(as === 'bytes' || as === 'text') &&
|
|
13
|
+
typeof id === 'string' &&
|
|
14
|
+
id.length > 0 &&
|
|
15
|
+
id.length <= 64 &&
|
|
16
|
+
typeof path === 'string');
|
|
17
|
+
};
|
|
18
|
+
export const relayPrefixes = (value) => value
|
|
19
|
+
.split(/\s+/)
|
|
20
|
+
.filter((prefix) => PATH.test(prefix) && prefix.length > 2 && prefix.endsWith('/'))
|
|
21
|
+
.filter((prefix) => !prefix.split('/').some((part) => part === '.' || part === '..'));
|
|
22
|
+
export const relayAddress = (path, base, origin, prefixes) => {
|
|
23
|
+
if (path.length === 0 || path.length > 1024 || prefixes.length === 0) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(path, base);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (url.origin !== origin ||
|
|
34
|
+
url.username !== '' ||
|
|
35
|
+
url.password !== '' ||
|
|
36
|
+
url.hash !== '' ||
|
|
37
|
+
!PATH.test(url.pathname) ||
|
|
38
|
+
!QUERY.test(url.search) ||
|
|
39
|
+
url.pathname.split('/').some((part) => part === '.' || part === '..')) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return prefixes.some((prefix) => url.pathname.startsWith(prefix)) ? url : undefined;
|
|
43
|
+
};
|
|
44
|
+
export const relayFetch = async (url, as) => {
|
|
45
|
+
let response;
|
|
46
|
+
try {
|
|
47
|
+
response = await fetch(url, {
|
|
48
|
+
cache: 'default',
|
|
49
|
+
credentials: 'same-origin',
|
|
50
|
+
method: 'GET',
|
|
51
|
+
mode: 'same-origin',
|
|
52
|
+
redirect: 'error',
|
|
53
|
+
referrerPolicy: 'same-origin',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
60
|
+
const claimed = Number(response.headers.get('content-length') ?? '0');
|
|
61
|
+
if (!response.ok || !KINDS.test(contentType) || claimed > RELAY_LIMIT) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const bytes = await response.arrayBuffer();
|
|
65
|
+
if (bytes.byteLength > RELAY_LIMIT) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
body: as === 'text' ? new TextDecoder().decode(bytes) : bytes,
|
|
70
|
+
contentType,
|
|
71
|
+
};
|
|
72
|
+
};
|