@whyframe/core

The core library for whyframe. Contains features and utilities shared among integration packages.

@whyframe/core exports a Vite plugin, while @whyframe/core/webpack exports a Webpack plugin. They share the same options below:

Options

defaultSrc

  • Type: string

The default iframe src if one is not provided. For supported frameworks, this will default to a generic internal whyframe html. Otherwise, a custom html is required to be passed.

See respective integration guides if a framework requires a custom html.

defaultShowSource

  • Type: boolean
  • Default: false

Whether to attach metadata of the raw source code for all iframes or components by default. Since the source can’t be treeshaken, this is false by default.

For iframes, this can be enabled or disabled individually using the data-why-show-source attribute. For components, this can be configured via its showSource option.

components

  • Type: Component[]

A list of components that contain an iframe that renders what’s passed to the component, e.g. via slots or children.

See Abstracting components for more details.

interface Component {
  /**
   * The name of the component used to detect via `<${name} />`
   */
  name: string
  /**
   * Whether to attach metadata of the raw source code by default. This defaults
   * to the root `defaultShowSource` option, but can be overidden here. When a
   * function is provided, a raw tag string like `<Component foo="bar">` will be
   * passed and the function get decide whether to show the source or not, e.g.
   * loosely checking for a prop to exist.
   */
  showSource?: boolean | ((openTag: string) => boolean)
}

Utilities

@whyframe/core/utils exports common utilities when working with whyframe.

// TODO: generic for name & payload types
export interface Rpc {
  send: (name: string, payload: any) => void
  on: (name: string, callback: (payload: any) => void) => void
  off: (name: string, callback?: (payload: any) => void) => void
  teardown: () => void
}

/**
 * Create a bridge between the current page and the iframe and communicate
 * via a remote procedure call API. The current page should pass a reference
 * to the iframe as the first parameter, while the iframe can skip it.
 */
export declare function createIframeRpc(iframe?: HTMLIFrameElement): Rpc

/**
 * Get the source of the iframe injected by whyframe
 */
export declare function getWhyframeSource(
  iframe: HTMLIFrameElement
): string | undefined

Plugin utilities

@whyframe/core/pluginutils exports utilities when builidng integration plugins.

/**
 * Return an 8 character hash safe to use in urls and ids
 */
export declare function hash(str: string): string

/**
 * Dedent a string to look pretty
 */
export declare function dedent(str: string): string

/**
 * Escape a string to be used as an attribute value
 */
export declare function escapeAttr(value: string): string

Plugin API

Integration plugins would usually interact with a whyframe:api plugin to hook into whyframe’s development and build logic. This can be done with:

const vitePlugin = {
  configResolved(c) {
    api = c.plugins.find((p) => p.name === 'whyframe:api')?.api
  }
}

And the API would have these:

interface Api {
  /**
   * @internal
   */
  _getHashToEntryIds: () => Map<string, string>
  /**
   * @internal
   */
  _getVirtualIdToCode: () => Map<string, string>
  /**
   * Check if a component name contains an iframe.
   */
  getComponent: (componentName: string) => Component | undefined
  /**
   * A utility to check if a module may contain an iframe to quickly skip parsing.
   */
  moduleMayHaveIframe: (id: string, code: string) => boolean
  getDefaultShowSource: () => boolean
  /**
   * Get the main iframe attrs, including `<iframe>` and custom `<Story>`.
   * This should be differentiated via `isComponent`.
   */
  getMainIframeAttrs: (
    entryId: string,
    hash: string,
    source: string | undefined,
    isComponent: boolean
  ) => Attr[]
  /**
   * If you're using a custom `<Story>` component, the `<iframe>` within it
   * needs to be processed differently. In short, it needs to received props
   * of `<Story>` and pass it to the `<iframe>` (aka proxying). This will generate
   * the attrs required for this interaction.
   */
  getProxyIframeAttrs: () => Attr[]
  /**
   * Create an entry for the iframe. This is later imported by `whyframe:app` which
   * invokes this virtual module's `createApp` function. The entry must conform to
   * this export dts:
   * ```ts
   * // can return promise if needed, or nothing at all
   * export const createApp: (el: HTMLElement) => { destroy: () => void }
   * ```
   */
  createEntry: (
    originalId: string,
    hash: string,
    ext: string,
    code: LoadResult
  ) => string
  /**
   * The entry for the iframe would load a virtual component. This creates it.
   * Usually this contains code extracted from the iframe content, plus any other
   * side-effectful code, like imports, outer functions, etc.
   */
  createEntryComponent: (
    originalId: string,
    hash: string,
    ext: string,
    code: LoadResult
  ) => string
  /**
   * The entry may contain metadata to be exposed. They can be imported via `whyframe:iframe`
   * so it can be treeshaken. The third parameter is a function that's lazily called
   * as it's anticipated to be heavy.
   */
  createEntryMetadata: (
    originalId: string,
    iframeName: string | undefined,
    code: () => string | Promise<string>
  ) => string
}