Full Code of mwood23/preact-island for AI

master 026c2d0c2d7a cached
49 files
78.9 KB
22.2k tokens
10 symbols
1 requests
Download .txt
Repository: mwood23/preact-island
Branch: master
Commit: 026c2d0c2d7a
Files: 49
Total size: 78.9 KB

Directory structure:
gitextract_f_30itg8/

├── .editorconfig
├── .gitignore
├── .npmignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config/
│   └── setupTests.js
├── example/
│   ├── declaration.d.ts
│   ├── package.json
│   ├── src/
│   │   ├── call-to-action.island.css
│   │   ├── call-to-action.island.tsx
│   │   ├── email-subscribe.island.css
│   │   ├── email-subscribe.island.tsx
│   │   ├── global.d.ts
│   │   ├── index.ts
│   │   ├── pokemon.clean.island.tsx
│   │   ├── pokemon.component.tsx
│   │   ├── pokemon.global.island.tsx
│   │   ├── pokemon.initial-props.island.tsx
│   │   ├── pokemon.inline.island.tsx
│   │   ├── pokemon.island.css
│   │   ├── pokemon.island.tsx
│   │   ├── pokemon.props-selector.island.tsx
│   │   ├── pokemon.replace.island.tsx
│   │   ├── template.html
│   │   └── utils.ts
│   ├── tsconfig.json
│   └── webpack.config.js
├── example-react/
│   ├── declaration.d.ts
│   ├── package.json
│   ├── src/
│   │   ├── call-to-action.island.css
│   │   ├── call-to-action.react.island.tsx
│   │   ├── pokemon.component.tsx
│   │   ├── pokemon.island.css
│   │   ├── pokemon.react.island.tsx
│   │   └── utils.ts
│   └── tsconfig.json
├── jest.config.js
├── package.json
├── src/
│   ├── index.ts
│   ├── island-web-components.tsx
│   ├── island.ts
│   ├── lib.ts
│   └── tests/
│       ├── helpers/
│       │   ├── getById.ts
│       │   └── inlineScript.tsx
│       ├── island.props.test.tsx
│       └── island.selector.test.tsx
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf


================================================
FILE: .gitignore
================================================
*.jks
*.p8
*.p12
*.mobileprovision
*.orig.*
web-build/
node_modules

dist

**/public/build
.netlify

.cache
.next
.env
stats.html

*tar.gz

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/.pnp
.pnp.js

# testing
coverage

# misc
.DS_Store

npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log
ui-debug.log

# Dependencies
jspm_packages

# Serverless
.serverless

# Dynamodb
.dynamodb

# Webpack
.webpack

test-results
test-report
tests-out
playwright-report

# Local folder
local

# Misc
.DS_Store
*.pem
Thumbs.db

size-plugin.json


================================================
FILE: .npmignore
================================================
!dist
example/
coverage/
docs/
jest.config.js
tsconfig.json
config/
.editorconfig
.prettierrc


================================================
FILE: .prettierrc
================================================
{
  "singleQuote": true,
  "trailingComma": "all",
  "semi": false,
  "endOfLine": "auto"
}

  

================================================
FILE: CHANGELOG.md
================================================
## 1.0.4

- Add subtree checking for prop scripts so that it works well in a React rerender context

## 1.0.5

- Bad publish, oopsie! Use 1.0.6.

## 1.0.6

- Fix peer deps so it correctly resolves.

Thanks [@rschristian](https://github.com/rschristian)

## 1.1.0

- 🧩 Experimental web component support including web component portals! This API may change drastically over time so if you use it keep that in mind. If you have ideas on how to improve it file an issue!
- Migrate examples over to webpack and off of Microbundle + Preact CLI. Now the development server injects the scripts just like you would in production to give you the closest environment to.
- Internal island lib now ships with support for the shadow dom (even if you don't want to use the build in island web components API)
- Documentation updates

## 1.1.1

- Fixed bug where rendering a web component multiple times on a page would not function as expected. This was due to the custom element being defined twice causing everything to silently fail.

## 1.1.2

- Fixed bug where data-mount-in did not function with web components

## 1.2.0

- Fix package.json exports field


================================================
FILE: LICENSE
================================================
Copyright 2022 Marcus Wood

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
<div align="center">
  <img src="./docs/preact-island.svg" align="center" />
</div>
<div align="center">
  <h1 align="center">🏝 Preact Island</h1>
  <p align="center">
    A 1.3kB module that helps you ship your own slice of paradise to any website. Especially useful for Shopify apps or CMS websites.
  </p>

[![downloads][downloads-badge]][npmcharts]
[![version][version-badge]][package]
[![Supports Preact and React][preact-badge]][preact]
[![MIT License][license-badge]][license]

</div>

> 🚀 Looking for a zero config starter to build islands fast? Check out [preact-island-starter](https://github.com/mwood23/preact-island-starter)

Sometimes you need to embed a component onto someone else's website. This could be a Shopify widget, email sign up form, CMS comment list, social media share icon, etc. Creating these experiences are tedious and difficult because you aren't in control of the website your code will be executed on.

Preact Island helps you build these experiences by adding a lightweight layer on top of Preact. For <5kB, you get a React style workflow (with hooks!), and a framework for rendering your widget with reactive props.

## Features

- 🚀 Render by selector, inline, or by a specific attribute given on the executed script
- ⚛️ Based on Preact, no special compiler or anything needed to render an island
- 🙏 5 ways to pass in props to your component
- 🧩 Experimental support for web components (including web component portals)
- 🪄 All components are reactive to prop changes causing rerenders (not remounts)
- 👯‍♀️ Create as many instances of your component as you need with a single island
- 🧼 Does not mutate the `window`. Use as many islands as you'd like on one page!
- 🐣 Less than 1.3kB
- ☠️ Supports replacing the target selector
- 🏔 React friendly with `preact/compat`
- 🔧 Manually trigger rerenders with props
- 🐙 Fully tested with Preact testing library
- 👔 Fully typed with TypeScript

## Examples

- [Basic element placement with props](https://codesandbox.io/s/preact-island-element-placement-with-props-8kzmj5)
- [Reactive props](https://codesandbox.io/s/reactive-prop-updates-uqvof7)
- [Multiple islands](https://codesandbox.io/s/multiple-islands-8xvjqw)
- [Props script](https://codesandbox.io/s/props-selector-70ks1g)
- [Replace selector](https://codesandbox.io/s/replace-selector-z2rogw)
- [Multiple host elements](https://codesandbox.io/s/multiple-host-elements-q6ot5q)
- [Clean host element](https://codesandbox.io/s/clean-host-element-i35nlt)
- [Global island](https://codesandbox.io/s/global-island-zqco9p)
- [Inline script](https://codesandbox.io/s/inline-script-1qm5q8)
- [Executed script props](https://codesandbox.io/s/preact-island-element-placement-current-script-props-0dwlyo)
- [Interior script props](https://codesandbox.io/s/interior-script-props-z5rcxg)
- [Mount In script attribute](https://codesandbox.io/s/mount-in-property-z5rcxg)
- [React with Props](https://codesandbox.io/s/preact-island-element-placement-with-props-react-49mjrg)
- [React Portals with Preact Island](https://codesandbox.io/s/preact-island-element-placement-with-props-react-tdss2p)

## Installation

```sh
npm install --save preact-island
```

## Usage

```tsx
import { createIsland } from 'preact-island'

const Widget = () => {
  return <div>awesome widget!</div>
}

const island = createIsland(Widget)
island.render({
  selector: '[data-island="widget"]',
})
```

## API

### createIsland

Creates a new island instance with a passed in component. Returns a bag of props/methods to work with your island.

```tsx
import { createIsland } from 'preact-island'

const Widget = () => {
  return <div>awesome widget!</div>
}

const island = createIsland(Widget)
```

### createIsland().render

Renders an island to the DOM given options.

```ts
const island = createIsland(Widget)
island.render({...})
```

#### options

````ts
  /**
   * A query selector target to create the widget. This is ignored if inline is passed or if a `data-mount-in` attribute
   * is appended onto the executed script.
   *
   * @example '[data-island="widget"]'
   */
  selector?: string
  /**
   * If true, removes all children of the element before rendering the component.
   *
   * @default false
   *
   * @example
   * ```html
   * <div data-island="widget">
   *    <div>some other content</div>
   *    <div>some other content</div>
   *    <div>some other content</div>
   * </div>
   * ```
   *
   * // turns into
   *
   * ```html
   * <div data-island="widget">
   *    <div>your-widget</div>
   * </div
   * ```
   */
  clean?: boolean
  /**
   * If true, replaces the contents of the selector with the component given. If you use replace,
   * you will not be able to add props to the host element (since it will be replaced). You will also
   * not be able to use child props script either (since they will be replaced).
   *
   * Use script tag props or a props selector for handling props when in replace mode.
   *
   * @default false
   *
   * @example
   * ```html
   * <div data-island="widget"></div>
   * ```
   *
   * // turns into
   *
   * ```html
   * <div>your-widget</div>
   * ```
   */
  replace?: boolean

  /**
   * Renders the widget at the current position of the script in the HTML document.
   *
   * @default false
   *
   * @example
   * ```html
   * <div>
   *    <div>some content here</div>
   *    <script src="https://preact-island.netlify.app/islands/pokemon.inline.island.umd.js"></script>
   *    <div>some content here</div>
   * </div>
   * ```
   *
   * // turns into
   *
   * ```html
   * <div>
   *    <div>some content here</div>
   *    <script src="https://preact-island.netlify.app/islands/pokemon.inline.island.umd.js"></script>
   *    <div>your widget</div>
   *    <div>some content here</div>
   * </div>
   * ```
   */
  inline?: boolean
  /**
   * Initial props to pass to the component. These props do not cause updates to the island if changed. Use `createIsland().rerender` instead.
   */
  initialProps?: P
  /**
   * A valid selector to a script tag located in the HTML document with a type of either `text/props` or `application/json`
   * containing props to pass into the component. If there are multiple scripts found with the selector, all props are merged with
   * the last script found taking priority.
   */
  propsSelector?: string
````

### createIsland().rerender

Triggers a rerenders of the island with the new props given.

```ts
const island = createIsland(Widget)
island.render({ selector: '[data-island="widget"]' })
island.rerender({ new: 'props' })
```

### createIsland().destroy

Destroys all instances of the island on the page and disconnects any associated observers.

```ts
const island = createIsland(Widget)
island.render({ selector: '[data-island="widget"]' })
island.destroy()
```

## Selecting Mount Point from Script

You can override the `selector` given to render by passing `data-mount-in` to the script.

[Example](https://codesandbox.io/s/mount-in-property-z5rcxg)

```html
<div data-island="pokemon">
  <script type="text/props">
    {"pokemon": "3"}
  </script>
</div>
<h2>Special mount</h2>
<!-- This takes priority over the other placement -->
<!-- Props are scoped to placement so that's why -->
<!-- Venosaur (pokemon number 3) doesn't appear -->
<div data-island="mount-here-actually"></div>

<script
  async
  data-mount-in='[data-island="mount-here-actually"]'
  src="https://preact-island.netlify.app/islands/pokemon.island.umd.js"
></script>
```

## Passing Props

Props can be passed to your widget various ways. You can choose to pass props multiple different ways to your island where they'll be merged in a defined order.

### Merging Order

Props are merged in the following order (from lowest to highest specificity):

1. Initial props
2. Element props
3. Executed script props
4. Props selector props
5. Interior script props

### Data Props

All props located on an HTML element use `data-`. You can name them any of the following ways:

- `data-background-color` => `backgroundColor`
- `data-prop-background-color` => `backgroundColor`
- `data-props-background-color` => `backgroundColor`

Under the hood all of these element will be transformed to `camelCase` and passed to your component.

### Initial Props

Default props can be passed on render to an island. You can render many islands and give them all different initial props. Initial props
do not cause rerenders if updated. Use `createIsland.rerender` instead.

```ts
import { createIsland } from 'preact-island'

const Widget = () => {
  return <div>awesome widget!</div>
}

const island = createIsland(Widget)

island.render({
  selector: '[data-island="widget"]',
  initialProps: {
    color: '#000000',
  },
})

island.render({
  selector: '[data-island="widget"]',
  initialProps: {
    color: '#ffffff',
  },
})

// Will render two instances of the island with different color props
```

### Element Props

Props can be placed on host elements and passed to your component. These props are reactive and will cause rerenders on changes.

[Example](https://codesandbox.io/s/preact-island-element-placement-with-props-8kzmj5)

```html
<div data-island="pokemon" data-pokemon="130"></div>
<script
  async
  src="https://preact-island.netlify.app/islands/pokemon.island.umd.js"
></script>
```

### Executed Script Props

Props can be placed on the script tag that's evaluated to the create your island. These props are reactive and will cause rerenders on changes.

[Example](https://codesandbox.io/s/preact-island-element-placement-current-script-props-0dwlyo)

```html
<div data-island="pokemon"></div>
<script
  data-pokemon="130"
  async
  src="https://preact-island.netlify.app/islands/pokemon.island.umd.js"
></script>
```

### Props Selector Props

Props can be placed inside of a `<script>` and targeted with a given selector. These props are reactive and will cause rerenders on changes. If there are multiple scripts found with the selector, all props are merged with the last script found taking priority.

[Example](https://codesandbox.io/s/props-selector-70ks1g)

```tsx
const island = createIsland(Pokemon)
island.render({
  selector: '[data-island="pokemon"]',
  // Make sure you set this in your island's render method!
  propsSelector: '[data-island-props="test-island"]',
})
```

```html
<div data-island="pokemon"></div>

<!-- Can be located anywhere in the document! -->
<script data-island-props="test-island" type="text/props">
  {"pokemon": "3"}
</script>

<script
  async
  src="https://preact-island.netlify.app/islands/pokemon.props-selector.island.umd.js"
></script>
```

### Interior Script Props

Props can be placed inside of a `<script>` and nested instead of a selector. These props are reactive and will cause rerenders on changes. If
multiple interior script props are found, all props are merged with the last script found taking priority.

[Example](https://codesandbox.io/s/interior-script-props-z5rcxg)

```html
<div data-island="pokemon">
  <script type="text/props">
    {"pokemon": "3"}
  </script>
</div>

<script
  async
  src="https://preact-island.netlify.app/islands/pokemon.island.umd.js"
></script>
```

## React Compatibility

- [React with Props](https://codesandbox.io/s/preact-island-element-placement-with-props-react-49mjrg)
- [React Portals with Preact Island](https://codesandbox.io/s/preact-island-element-placement-with-props-react-tdss2p)

Preact Island fully supports React using [preact/compat](https://preactjs.com/guide/v10/switching-to-preact). This allows you to bring your existing React components over to Preact to get great performance gains without needing to rewrite your components. Check out the `example-react` folder to a demo repo that reproduces some of the Preact islands as React islands.

Depending on what you import from React, using Preact + Preact Island can result in a **15x smaller bundle** for the same functionality and no code changes needed on your end.

### Bundle Sizes

#### React

![react only](./docs/bundle-react-only.png)

#### React + Preact/compat

![react with preact/compat](./docs/bundle-react-with-compat.png)

#### Preact

![preact](./docs/bundle-preact.png)

## Adding Styles

You can add styles to your island just like any other component. If you're island will be running on someone else's website be mindful of the global CSS scope! Prefix all of your classes with a name `island__` or use CSS modules to make sure those styles don't leak. Do not use element selectors like `p` or `h2`.

### Including Styles

Preact Island takes no opinions on how CSS is included for your islands. There are two main options:

**Inline the CSS into the bundle**

This is what the `/example` islands do.

```tsx
import { createIsland } from 'preact-island'
import style from './email-subscribe.island.css'

document.head.insertAdjacentHTML('beforeend', `<style>${style}</style>`)

const Widget = () => {
  return (
    <div className="email__container">
      <p className="email__title">Join our newsletter</p>
      {/* ... */}
    </div>
  )
}

const island = createIsland(Widget)
island.render({
  selector: '[data-island="email-subscribe"]',
})
```

```html
<script
  src="https://your-domain/snippets/fancy-widget.island.umd.js"
  async
></script>
```

**Pros:**

- The consumer of the script doesn't need to include an external stylesheet
- There's only one request for rendering the entire widget

**Cons:**

- Bloats the bundle
- The CSS file itself won't be able to be cached

**Use an external stylesheet**

```html
<!-- Start island -->
<link
  href="https://your-domain/snippets/fancy-widget.island.css"
  rel="stylesheet"
/>
<script
  src="https://your-domain/snippets/fancy-widget.island.umd.js"
  async
></script>
<!-- End island -->
```

**Pros:**

- The CSS can be cached in the browser
- Doesn't bloat the JS bundle

**Cons:**

- Unless your script creates an element to automatically request the stylesheet the consumer of your script will need to add two things not one

### CSS Libraries

It's not recommending to use CSS libraries when developing islands since they're meant to be small and ran everywhere. Some libraries come with opinionated CSS resets and other global CSS styles that could break the consuming website of your island. They are also going to be large.

If you need a CSS library, use something that has a just in time compilation step like [Tailwind](https://tailwindcss.com/docs/installation) to minimize the excess CSS. Or you can use something like [Vanilla-Extract](https://vanilla-extract.style/) to build your own zero runtime cost Tailwind.

## Building Your Islands

Any modern bundler will work with Preact Island. If you are looking for a script that will run on a webpage you need the `UMD` format. The `/example` project has a demo setup using `webpack`. It works extremely well if you have multiple islands because it can produce multi-entry point bundles.

### Naming Conventions

Make sure to name your bundles `kebab-case` since they'll be served over HTTP. Case sensitive URLs can be fiddly depending on browser!

## Hosting Your Islands

You can host your files on anywhere you would typically host websites. Vercel, Cloudflare Workers, and Netlify all work great. Netlify recently [changed their prices](https://answers.netlify.com/t/upcoming-changes-to-netlify-plans/52482/158) causing it to be prohibitively expensive depending on your team size.

> Make sure to use your own domain when hosting these assets. If you use their subdomain like `something.mynetlify.com` and embed those onto third party websites you may get locked in.

## The Callsite for Your Island

When you are consuming the bundled snippet it's important not to block rendering on a consuming page. When a browser loads a webpage and sees a `script` tag it executes that script immediately blocking render. Islands should be independent of the consuming page so it is safe to use the `async` property. See [async vs defer](https://javascript.info/script-async-defer) for more information.

> The only exception to this is if you are putting the island on the window and want to run a script after it. Check out [global island](https://codesandbox.io/s/global-island-zqco9p) for an example.

Do this:

```html
<script
  src="https://your-domain/snippets/fancy-widget.island.umd.js"
  async
></script>
```

Not this:

```html
<script src="https://your-domain/snippets/fancy-widget.island.umd.js"></script>
```

## Differences to Preact Habitat

This library was heavily inspired by [Preact Habitat](https://github.com/zouhir/preact-habitat).

Key differences:

- Components rerender based on prop changes. This can be a `data-prop` attribute change on a host element, inside of a props script tag, or event on the executed script.
- You can add props to script element itself and they're passed to the component
- You can add props to a script tag that lives anywhere in the document and sync it up to the component
- You can replace the target container instead of rendering inside of it if you choose
- No double loading when mounting the component
- Calling render multiple times create many components
- There is a `rerender` method that allows you to manually alter props for the component
- All dataset attributes (`data-` on an element) are passed as props
- There is no `clientSpecified` flag. If you declare a `data-mount-in` prop on a script it will take priority over the `selector` given at `render`.

## Alternatives

- [StencilJS](https://stenciljs.com/): A web components toolchain that feels sort of like React and Angular put together. The JSX core is lifted from Preact's internals. It has a lot of nice features like automatic documentation and other nice to haves since it has its own compiler. It feels tailored towards design system and doesn't have the flexibility of prop injection that Preact Island does. It's also another tool to adopt with it's own patterns. All you have to do with Preact Island is bring your component.
- [Lit](https://lit.dev/docs/): A web components framework by Google. Does not require a compilation step and weights in around 5Kb (roughly the same as Preact Island + Preact). Doesn't use a virtual dom for diffing and feels like a nice layer on top of web components. Does not support JSX or a React style workflow.

## Credits

A huge thank you to [zouhir](https://github.com/zouhir) who is the author of [preact-habitat](https://github.com/zouhir/preact-habitat). This library is heavily inspired by his work on that library.

Artwork by [vik4graphic](https://lottiefiles.com/vik4graphic)

## License

[MIT](LICENSE) - Copyright (c) [Marcus Wood](https://www.marcuswood.io/)

[version-badge]: https://img.shields.io/npm/v/preact-island.svg?style=flat-square
[package]: https://www.npmjs.com/package/preact-island
[downloads-badge]: https://img.shields.io/npm/dm/preact-island.svg?style=flat-square
[npmcharts]: http://npmcharts.com/compare/preact-island
[license-badge]: https://img.shields.io/npm/l/preact-island.svg?style=flat-square
[license]: https://github.com/mwood23/preact-island/blob/master/LICENSE
[preact-badge]: https://img.shields.io/badge/%E2%9A%9B%EF%B8%8F-preact-6F2FBF.svg?style=flat-square
[preact]: https://preactjs.com
[module-formats-badge]: https://img.shields.io/badge/module%20formats-umd%2C%20cjs%2C%20es-green.svg?style=flat-square
[github-star]: https://github.com/mwood23/preact-island/stargazers


================================================
FILE: config/setupTests.js
================================================
import 'regenerator-runtime/runtime'
import '@testing-library/jest-dom'

import preact from 'preact'
import * as hook from 'preact/hooks'
global.preact = preact
global._hooks = hook


================================================
FILE: example/declaration.d.ts
================================================
declare module '*.css'


================================================
FILE: example/package.json
================================================
{
  "name": "preact-island-examples",
  "version": "0.1.0",
  "description": "",
  "license": "MIT",
  "main": "dist/index.js",
  "umd:main": "dist/index.umd.js",
  "module": "dist/index.module.js",
  "source": "src/index.tsx",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "webpack --env prod",
    "dev": "webpack serve --env dev"
  },
  "dependencies": {
    "clsx": "^1.2.1"
  },
  "devDependencies": {
    "@babel/preset-env": "^7.18.9",
    "@babel/preset-react": "^7.18.6",
    "@babel/preset-typescript": "^7.18.6",
    "@types/webpack": "^5.28.0",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "html-webpack-plugin": "^5.5.0",
    "terser-webpack-plugin": "^5.3.5",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.9.3"
  }
}


================================================
FILE: example/src/call-to-action.island.css
================================================
.cta_button {
  background-color: none;
  border: none;
  background-color: #294eab;
  border-radius: 5px;
  padding: 10px;
  font-weight: bold;
  color: white;
  cursor: pointer;
  font-family: inherit;
}

.cta__modal-dimmer {
  position: fixed;
  display: none;
  z-index: 90;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.6);
}

.cta__modal-dimmer--visible {
  display: block;
  animation: show 0.2s;
  animation-fill-mode: forwards;
}

.cta__modal {
  position: fixed;
  outline: none;
  z-index: 100;
  background-color: white;
  display: none !important;
  width: 380px;
  border-radius: 24px;
  padding: 34px 21px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-family: inherit;
  overflow-y: auto;
  height: 650px;
  text-align: center;
}

.cta__modal--visible {
  display: block !important;
  animation: show 0.3s;
  animation-fill-mode: forwards;
}

.cta__modal img {
  width: 100%;
  margin-bottom: 1rem;
}


================================================
FILE: example/src/call-to-action.island.tsx
================================================
import { createIslandWebComponent, WebComponentPortal } from '../../src'
import { useState } from 'preact/hooks'
import style from './call-to-action.island.css'
import cx from 'clsx'

const Widget = ({ backgroundColor }: { backgroundColor?: string }) => {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button
        className="cta_button"
        style={{ backgroundColor: backgroundColor }}
        onClick={() => setIsOpen(true)}
      >
        All expenses paid island vacation. Click to enter!
      </button>

      {isOpen && (
        <WebComponentPortal style={style} name="bounty-modal">
          <div className={cx('cta__modal', isOpen && 'cta__modal--visible')}>
            <img src="https://github.com/mwood23/preact-island/raw/master/docs/preact-island.svg" />
            <p>Portals work with web component islands too!</p>
            <button className="cta_button" onClick={() => setIsOpen(false)}>
              close
            </button>
          </div>
        </WebComponentPortal>
      )}
      {isOpen && (
        <WebComponentPortal style={style} name="bounty-dimmer">
          <div
            className={cx(
              'cta__modal-dimmer',
              isOpen && 'cta__modal-dimmer--visible',
            )}
            onClick={() => setIsOpen(false)}
          />
        </WebComponentPortal>
      )}
    </div>
  )
}

const name = 'call-to-action'
const island = createIslandWebComponent(name, Widget)
island.render({
  selector: name,
})
island.injectStyles(style)


================================================
FILE: example/src/email-subscribe.island.css
================================================
.email__container {
  border-radius: 5px;
  border: 1px solid #eae7e7;
  padding: 1rem;
}

.email__title {
  font-family: inherit;
  display: block;
  margin-bottom: 1rem;
  font-weight: bold;
  color: #333333;
  font-size: 1.3rem;
}

.email__input {
  font-family: inherit;
  display: block;
  margin-bottom: 1rem;
}

.email__input input {
  display: block;
  font-family: inherit;
  background-color: #efefef;
  padding: 5px 10px;
  border: none;
  border-radius: 5px;
}

.email__submit {
  background-color: none;
  border: none;
  background-color: #2f3a54;
  border-radius: 5px;
  padding: 10px;
  font-weight: bold;
  color: white;
  cursor: pointer;
  font-family: inherit;
}


================================================
FILE: example/src/email-subscribe.island.tsx
================================================
import { createIsland } from '../../src'
import { useState } from 'preact/hooks'
import { injectCSS } from './utils'
import style from './email-subscribe.island.css'

injectCSS(style)

const Widget = ({
  showEmail = true,
  showName = true,
  ...rest
}: {
  showEmail: boolean
  showName: boolean
}) => {
  const [value, setValue] = useState({ name: '', email: '' })
  return (
    <div className="email__container">
      <p className="email__title">Join our newsletter</p>
      <form
        onSubmit={() => {
          alert(`Submitted with: ${value.name}, ${value.email}`)
        }}
      >
        {showName && (
          <label className="email__input">
            Name
            <input
              name="name"
              onInput={(e: any) =>
                setValue((x) => ({ ...x, name: e.target.value }))
              }
            />
          </label>
        )}

        {showEmail && (
          <label className="email__input">
            Email
            <input
              name="email"
              onInput={(e: any) =>
                setValue((x) => ({ ...x, email: e.target.value }))
              }
            />
          </label>
        )}
        <button className="email__submit">Sign up</button>
      </form>
    </div>
  )
}

const island = createIsland(Widget)
island.render({
  selector: '[data-island="email-subscribe"]',
})


================================================
FILE: example/src/global.d.ts
================================================
declare module '*css'


================================================
FILE: example/src/index.ts
================================================
// This file is only used for developing the snippet in the sandbox
import './call-to-action.island'
import './email-subscribe.island'
import './pokemon.island'


================================================
FILE: example/src/pokemon.clean.island.tsx
================================================
import { createIsland } from '../../dist/index.module'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)
island.render({
  selector: '[data-island="pokemon"]',
  clean: true,
})


================================================
FILE: example/src/pokemon.component.tsx
================================================
import { useEffect, useState } from 'preact/hooks'

export const Pokemon = ({ pokemon }: { pokemon: string }) => {
  const [pokemonData, setPokemonData] = useState<null | any>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  useEffect(() => {
    if (!pokemon) return

    setLoading(true)
    fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
      .then((d) => d.json())
      .then((d) => {
        setLoading(false)
        setPokemonData(d)
        setError('')
      })
      .catch((e) => setError(e.message || 'Something went wrong!'))
  }, [pokemon])

  if (error) {
    return <div className="pokemon__container">{error}</div>
  }

  if (loading) {
    return <div className="pokemon__container">Loading...</div>
  }

  if (!pokemonData) {
    return (
      <div className="pokemon__container">Select a pokemon to see info</div>
    )
  }

  return (
    <div className="pokemon__container">
      <img
        className="pokemon__image"
        src={pokemonData.sprites.front_default}
        alt={pokemonData.name}
      />
      <p className="pokemon__info">
        <b>Name:</b> {pokemonData.name}
      </p>
      <p className="pokemon__info">
        <b>Number:</b> {pokemonData.id}
      </p>
    </div>
  )
}


================================================
FILE: example/src/pokemon.global.island.tsx
================================================
import { createIsland } from '../../dist/index.module'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)

// @ts-expect-error - We're mutating the window to add this
window._pokemon = island


================================================
FILE: example/src/pokemon.initial-props.island.tsx
================================================
import { createIsland } from '../../src'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)
island.render({
  selector: '[data-island="pokemon"]',
  initialProps: {
    pokemon: 3,
  },
})


================================================
FILE: example/src/pokemon.inline.island.tsx
================================================
import { createIsland } from '../../src'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)
island.render({
  inline: true,
})


================================================
FILE: example/src/pokemon.island.css
================================================
.pokemon__container {
  border-radius: 5px;
  border: 1px solid #eae7e7;
  padding: 1rem;
  display: inline-block;
}

.pokemon__image {
  width: 200px;
  height: 200px;
  text-align: center;
}

.pokemon__info {
  color: #2b2e34;
  margin-bottom: 5px;
}


================================================
FILE: example/src/pokemon.island.tsx
================================================
import { createIsland } from '../../src'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)
island.render({
  selector: '[data-island="pokemon"]',
})


================================================
FILE: example/src/pokemon.props-selector.island.tsx
================================================
import { createIsland } from '../../src'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)
island.render({
  selector: '[data-island="pokemon"]',
  propsSelector: '[data-island-props="test-island"]',
})


================================================
FILE: example/src/pokemon.replace.island.tsx
================================================
import { createIsland } from '../../src'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)
island.render({
  selector: '[data-island="pokemon"]',
  replace: true,
})


================================================
FILE: example/src/template.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Widgets</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
      rel="stylesheet"
    />
    <style>
      body {
        font-family: 'Inter', sans-serif;
      }

      .preview {
        width: 100%;
        max-width: 1100px;
        margin: 80px auto;
        border: 1px dashed rgba(0, 0, 0, 0.2);
        position: relative;
        padding: 1rem;
      }

      .preview::before {
        content: 'WIDGET PREVIEW';
        position: absolute;
        display: block;
        top: -18px;
        font-size: 11px;
        color: rgba(0, 0, 0, 0.5);
      }
    </style>
    <script async="" src="/call-to-action.island.umd.js" data-mount-in=".mount-call-to-action-here"></script>
  </head>
  <body>
    <div class="preview">
      <call-to-action></call-to-action>
    </div>
    <div data-island="email-subscribe" class="preview"></div>
    <div data-island="pokemon" class="preview">
      <script type="text/props">
        { "pokemon": "122" }
      </script>
    </div>
    <div class="preview">
      <div class="mount-call-to-action-here">
    </div>
  </body>
</html>


================================================
FILE: example/src/utils.ts
================================================
export const injectCSS = (style: string) => {
  document.head.insertAdjacentHTML('beforeend', `<style>${style}</style>`)
}


================================================
FILE: example/tsconfig.json
================================================
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "allowJs": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "module": "esnext",
    "types": ["node"],
    "lib": ["es2017", "dom", "DOM.Iterable"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "noPropertyAccessFromIndexSignature": false,
    "baseUrl": "."
  },
  "exclude": [
    "config/setupTests.ts",
    "jest.config.ts",
    "**/*.spec.ts",
    "**/*.test.ts",
    "**/*.spec.tsx",
    "**/*.test.tsx",
    "**/*.spec.js",
    "**/*.test.js",
    "**/*.spec.jsx",
    "**/*.test.jsx"
  ],
  "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}


================================================
FILE: example/webpack.config.js
================================================
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')

module.exports = ({ dev, prod }) => {
  const isDev = dev === true
  const isProd = prod === true

  /** @type { import('webpack').Configuration } */
  const config = {
    mode: isProd ? 'production' : 'development',
    target: 'web',
    resolve: {
      extensions: ['.js', '.json', '.ts', '.tsx'],
      /**
       * From the docs to make Webpack compile Preact:
       * https://preactjs.com/guide/v10/getting-started#aliasing-in-webpack
       */
      alias: {
        react: 'preact/compat',
        'react-dom/test-utils': 'preact/test-utils',
        'react-dom': 'preact/compat', // Must be below test-utils
        'react/jsx-runtime': 'preact/jsx-runtime',
      },
    },
    devServer: {
      port: 6464,
      hot: false,
    },
    devtool: false,
    entry: {
      'call-to-action': './src/call-to-action.island.tsx',
      'email-subscribe': './src/email-subscribe.island.tsx',
      pokemon: './src/pokemon.island.tsx',
    },
    output: {
      path: path.join(__dirname, 'dist/islands'),
      filename: '[name].island.umd.js',
      libraryTarget: 'umd',
    },
    module: {
      rules: [
        {
          test: /\.(js|ts|tsx)$/,
          exclude: [/node_modules/],
          use: [
            {
              loader: 'babel-loader',
              options: {
                babelrc: false,
                presets: [
                  '@babel/preset-typescript',
                  ['@babel/preset-react', { runtime: 'automatic' }],
                  [
                    '@babel/preset-env',
                    { targets: { node: 16 }, modules: false },
                  ],
                ],
              },
            },
          ],
        },
        {
          test: /\.css$/i,
          use: ['css-loader'],
        },
        {
          test: /\.(png|jpe?g|gif)$/i,
          use: [
            {
              loader: 'file-loader',
            },
          ],
        },
      ],
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: 'src/template.html',
        /**
         * Islands are served from /islands in dist so we don't pollute the root domain since these islands are
         * embedded into websites we do not control.
         *
         * In dev mode, we serve islands and the index.html from the root since it's dev mode. For production,
         * the index.html file is served from the root.
         */
        publicPath: isDev ? '/' : '/islands',
        filename: isDev ? 'index.html' : '../index.html',
      }),
    ],
    stats: 'errors-warnings',
    optimization: {
      minimize: true,
      minimizer: [new TerserPlugin()],
    },
  }

  return config
}


================================================
FILE: example-react/declaration.d.ts
================================================
declare module '*.css'


================================================
FILE: example-react/package.json
================================================
{
  "name": "preact-island-react-examples",
  "version": "0.1.0",
  "description": "",
  "source": "src/index.tsx",
  "main": "dist/index.js",
  "module": "dist/index.module.js",
  "umd:main": "dist/index.umd.js",
  "scripts": {
    "build": "NODE_OPTIONS=--max-old-space-size=8192 microbundle src/*.island.tsx build --no-sourcemap --output dist/islands --external none --css inline --tsconfig tsconfig.json --alias react=preact/compat,react-dom=preact/compat,react-dom/test-utils=preact/compat,react/jsx-runtime=preact/compat"
  },
  "files": [
    "dist"
  ],
  "license": "MIT",
  "devDependencies": {
    "@types/react": "^17.0.1",
    "@types/react-dom": "^17.0.1",
    "clsx": "^1.1.1",
    "microbundle": "^0.15.0",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  }
}


================================================
FILE: example-react/src/call-to-action.island.css
================================================
.cta_button {
  background-color: none;
  border: none;
  background-color: #294eab;
  border-radius: 5px;
  padding: 10px;
  font-weight: bold;
  color: white;
  cursor: pointer;
  font-family: inherit;
}

.cta__modal-dimmer {
  position: fixed;
  display: none;
  z-index: 90;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.6);
}

.cta__modal-dimmer--visible {
  display: block;
  animation: show 0.2s;
  animation-fill-mode: forwards;
}

.cta__modal {
  position: fixed;
  outline: none;
  z-index: 100;
  background-color: white;
  display: none !important;
  width: 380px;
  border-radius: 24px;
  padding: 34px 21px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-family: inherit;
  overflow-y: auto;
  height: 650px;
  text-align: center;
}

.cta__modal--visible {
  display: block !important;
  animation: show 0.3s;
  animation-fill-mode: forwards;
}

.cta__modal img {
  width: 100%;
  margin-bottom: 1rem;
}


================================================
FILE: example-react/src/call-to-action.react.island.tsx
================================================
import { createIsland } from '../../dist/index.module'
import { useState } from 'react'
import { createPortal } from 'react-dom'
import style from './call-to-action.island.css'
import { injectCSS } from './utils'
import cx from 'clsx'

injectCSS(style)

const Widget = ({ backgroundColor }: { backgroundColor?: string }) => {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button
        className="cta_button"
        style={{ backgroundColor: backgroundColor }}
        onClick={() => setIsOpen(true)}
      >
        All expenses paid island vacation. Click to enter!
      </button>

      {isOpen &&
        createPortal(
          <div className={cx('cta__modal', isOpen && 'cta__modal--visible')}>
            <img src="https://github.com/mwood23/preact-island/raw/master/docs/preact-island.svg" />
            <p>Portals work with islands too!</p>
            <button className="cta_button" onClick={() => setIsOpen(false)}>
              close
            </button>
          </div>,
          document.body,
        )}
      {isOpen &&
        createPortal(
          <div
            className={cx(
              'cta__modal-dimmer',
              isOpen && 'cta__modal-dimmer--visible',
            )}
            onClick={() => setIsOpen(false)}
          />,
          document.body,
        )}
    </div>
  )
}

const island = createIsland(Widget)
island.render({
  selector: '[data-island="call-to-action"]',
})


================================================
FILE: example-react/src/pokemon.component.tsx
================================================
import { useEffect, useState } from 'react'

export const Pokemon = ({ pokemon }: { pokemon: string }) => {
  const [pokemonData, setPokemonData] = useState<null | any>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  useEffect(() => {
    if (!pokemon) return

    setLoading(true)
    fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)
      .then((d) => d.json())
      .then((d) => {
        setLoading(false)
        setPokemonData(d)
        setError('')
      })
      .catch((e) => setError(e.message || 'Something went wrong!'))
  }, [pokemon])

  if (error) {
    return <div className="pokemon__container">{error}</div>
  }

  if (loading) {
    return <div className="pokemon__container">Loading...</div>
  }

  if (!pokemonData) {
    return (
      <div className="pokemon__container">Select a pokemon to see info</div>
    )
  }

  return (
    <div className="pokemon__container">
      <img
        className="pokemon__image"
        src={pokemonData.sprites.front_default}
        alt={pokemonData.name}
      />
      <p className="pokemon__info">
        <b>Name:</b> {pokemonData.name}
      </p>
      <p className="pokemon__info">
        <b>Number:</b> {pokemonData.id}
      </p>
    </div>
  )
}


================================================
FILE: example-react/src/pokemon.island.css
================================================
.pokemon__container {
  border-radius: 5px;
  border: 1px solid #eae7e7;
  padding: 1rem;
  display: inline-block;
}

.pokemon__image {
  width: 200px;
  height: 200px;
  text-align: center;
}

.pokemon__info {
  color: #2b2e34;
  margin-bottom: 5px;
}


================================================
FILE: example-react/src/pokemon.react.island.tsx
================================================
import { createIsland } from '../../dist/index.module'
import { Pokemon } from './pokemon.component'
import style from './pokemon.island.css'
import { injectCSS } from './utils'

injectCSS(style)

const island = createIsland(Pokemon)
island.render({
  selector: '[data-island="pokemon"]',
})


================================================
FILE: example-react/src/utils.ts
================================================
export const injectCSS = (style: string) => {
  document.head.insertAdjacentHTML('beforeend', `<style>${style}</style>`)
}


================================================
FILE: example-react/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ES2015",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "preserveWatchOutput": true,
    "jsx": "react-jsx",
    "jsxFactory": "",
    "jsxFragmentFactory": "",
    "noEmit": true
  },
  "include": ["src", "declaration.d.ts"],
  "exclude": ["**/tests/**"]
}


================================================
FILE: jest.config.js
================================================
export default {
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
  preset: 'jest-preset-preact',
  setupFilesAfterEnv: ['<rootDir>/config/setupTests.js'],
  testEnvironment: 'jsdom',
  globals: {
    'ts-jest': {
      isolatedModules: true,
    },
  },
}


================================================
FILE: package.json
================================================
{
  "name": "preact-island",
  "version": "1.2.0",
  "type": "module",
  "description": "🏝 Create your own slice of paradise on any website.",
  "source": "src/index.ts",
  "main": "./dist/index.cjs",
  "module": "./dist/index.module.js",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.modern.js",
      "require": "./dist/index.cjs",
      "umd": "./dist/index.umd.js"
    }
  },
  "unpkg": "./dist/index.umd.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build:netlify": "npm run build && cd ./example && npm install && npm run build",
    "build:netlify:react": "npm run build && cd ./example-react && npm install && npm run build",
    "build": "microbundle --sourcemap false",
    "dev:lib": "microbundle watch",
    "dev:example": "cd example && npm run dev",
    "dev": "run-p dev:*",
    "test": "jest"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mwood23/preact-island.git"
  },
  "keywords": [
    "preact",
    "habitat",
    "shopify",
    "cms widgets"
  ],
  "author": "Marcus Wood",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/mwood23/preact-island/issues"
  },
  "homepage": "https://github.com/mwood23/preact-island#readme",
  "eslintConfig": {
    "parser": "@typescript-eslint/parser",
    "extends": [
      "preact",
      "plugin:@typescript-eslint/recommended"
    ],
    "ignorePatterns": [
      "build/"
    ]
  },
  "dependencies": {},
  "devDependencies": {
    "@types/jest": "^27.5.1",
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/user-event": "^14.2.0",
    "microbundle": "^0.15.0",
    "eslint": "^8.15.0",
    "eslint-config-preact": "^1.3.0",
    "prettier": "^2.6.2",
    "jest": "^27.2.5",
    "preact": "^10.7.2",
    "preact-render-to-string": "^5.1.4",
    "npm-run-all": "^4.1.5",
    "@testing-library/preact": "^2.0.1",
    "jest-preset-preact": "^4.0.2",
    "typescript": "^4.6.4"
  },
  "peerDependencies": {
    "preact": ">=10"
  }
}


================================================
FILE: src/index.ts
================================================
export * from './island'
export * from './island-web-components'


================================================
FILE: src/island-web-components.tsx
================================================
import { ComponentType, VNode } from 'preact'
import { createPortal, FC, useEffect, useMemo } from 'preact/compat'
import { createIsland, Island } from './island'

export type InitialPropsWebComponent = { [x: string]: any }

export const createIslandWebComponent = <P extends InitialPropsWebComponent>(
  /**
   * Must be a valid web component element name. See spec for more details:
   * https://html.spec.whatwg.org/#valid-custom-element-name
   */
  elementName: string,
  /**
   * Component you would like to render inside of the web component
   */
  widget: ComponentType<P>,
) => {
  if (customElements.get(elementName) == null) {
    class Element extends HTMLElement {
      constructor() {
        super()

        // Create a shadow root
        this.attachShadow({ mode: 'open' })
      }
    }

    customElements.define(elementName, Element)
  }

  const island = createIsland(widget)

  return {
    ...island,
    render: (
      args: Omit<
        Parameters<Island<P>['render']>[0],
        'replace' | 'clean' | 'inline'
      >,
    ) => island.render({ elementName, ...args }),
    injectStyles: (style: string) => {
      island._roots.forEach((root) => {
        const styleElement = document.createElement('style')
        styleElement.innerHTML = style

        // @ts-ignore
        root.parentNode.prepend(styleElement)
      })
    },
  }
}

export const WebComponentPortal: FC<{
  name: string
  container?: Element
  children: VNode<{}>
  style?: string | HTMLStyleElement
}> = ({ name, container = document.body, children, style }) => {
  const portalTarget = useMemo(() => {
    if (customElements.get(name) == null) {
      class Element extends HTMLElement {
        constructor() {
          super()

          // Create a shadow root
          const shadowRoot = this.attachShadow({ mode: 'open' })

          if (!style) return

          if (style instanceof HTMLStyleElement) {
            shadowRoot.prepend(style)
          } else {
            const styleElement = document.createElement('style')
            styleElement.innerHTML = style

            shadowRoot.prepend(styleElement)
          }
        }
      }

      customElements.define(name, Element)
    }

    const customElement = document.createElement(name)
    return container.appendChild(customElement)
  }, [container, style])

  useEffect(() => {
    return () => {
      portalTarget.remove()
    }
  }, [portalTarget])

  // @ts-ignore - Internal types wrong
  return createPortal(children, portalTarget.shadowRoot)
}


================================================
FILE: src/island.ts
================================================
import { getHostElements, mount, RootFragment, renderIsland } from './lib'
import { render, ComponentType } from 'preact'

export type InitialProps = { [x: string]: any }

export type Island<P extends InitialProps> = {
  /**
   * A WeakMap that yields the mutation observers associated with a particular root. Used for cleaning up observers
   * on destroy.
   */
  _rootsToObservers: WeakMap<RootFragment, MutationObserver>
  /**
   * An array of the root fragments (a fake DOM element) containing one or more
   * DOM nodes, which can then be passed as the `parent` argument to Preact's `render()` method.
   */
  _roots: RootFragment[]
  /**
   * A reference to the executed script that called `createIsland`. This is used for listening to prop
   * changes on that script and causing rerenders of the island.
   */
  _executedScript: HTMLOrSVGScriptElement | null
  /**
   * Renders the created island at the given selector. Calling multiple times appends elements at the given selectors.
   */
  render: (props: {
    /**
     * A query selector target to create the widget. This is ignored if inline is passed or if a `data-mount-in` attribute
     * is appended onto the executed script.
     *
     * @example '[data-island="widget"]'
     */
    selector?: string
    /**
     * If true, removes all children of the element before rendering the component.
     *
     * @default false
     *
     * @example
     * ```html
     * <div data-island="widget">
     *    <div>some other content</div>
     *    <div>some other content</div>
     *    <div>some other content</div>
     * </div>
     * ```
     *
     * // turns into
     *
     * ```html
     * <div data-island="widget">
     *    <div>your-widget</div>
     * </div
     * ```
     */
    clean?: boolean
    /**
     * If true, replaces the contents of the selector with the component given. If you use replace,
     * you will not be able to add props to the host element (since it will be replaced). You will also
     * not be able to use child props script either (since they will be replaced).
     *
     * Use script tag props or a props selector for handling props when in replace mode.
     *
     * @default false
     *
     * @example
     * ```html
     * <div data-island="widget"></div>
     * ```
     *
     * // turns into
     *
     * ```html
     * <div>your-widget</div>
     * ```
     */
    replace?: boolean

    /**
     * Renders the widget at the current position of the script in the HTML document.
     *
     * @default false
     *
     * @example
     * ```html
     * <div>
     *    <div>some content here</div>
     *    <script src="https://preact-island.netlify.app/islands/pokemon.inline.island.umd.js"></script>
     *    <div>some content here</div>
     * </div>
     * ```
     *
     * // turns into
     *
     * ```html
     * <div>
     *    <div>some content here</div>
     *    <script src="https://preact-island.netlify.app/islands/pokemon.inline.island.umd.js"></script>
     *    <div>your widget</div>
     *    <div>some content here</div>
     * </div>
     * ```
     */
    inline?: boolean
    /**
     * Initial props to pass to the component. These props do not cause updates to the island if changed. Use `createIsland().rerender` instead.
     */
    initialProps?: Partial<P>
    /**
     * A valid selector to a script tag located in the HTML document with a type of either `text/props` or `application/json`
     * containing props to pass into the component. If there are multiple scripts found with the selector, all props are merged with
     * the last script found taking priority.
     */
    propsSelector?: string

    /**
     * Passed in if using for web components
     */
    elementName?: string
  }) => void

  /**
   * Contains the current props used to render the island.
   */
  props: P
  /**
   * Triggers a rerenders of the island with the new props given.
   */
  rerender: (props: P) => void
  /**
   * Destroys all instances of the island on the page and disconnects any associated observers.
   */
  destroy: () => void
}

export const createIsland = <P extends InitialProps>(
  widget: ComponentType<P>,
) => {
  const island: Island<P> = {
    _rootsToObservers: new WeakMap(),
    _roots: [],
    _executedScript: document.currentScript,
    // @ts-ignore
    props: {},
    render: ({
      selector,
      clean = false,
      replace = false,
      inline = false,
      initialProps = {},
      propsSelector,
      elementName,
    }) => {
      let rendered = false

      const load = () => {
        /**
         * We listen for multiple events to render so soon as we do it once
         * successfully we break early for others.
         */
        if (rendered === true) return
        const hostElements = getHostElements({
          selector,
          inline,
          elementName,
        })

        // Do nothing if no host elements returned
        if (hostElements.length === 0) return

        const { rootFragments } = mount<P>({
          island,
          widget,
          clean,
          hostElements,
          replace,
          // @ts-ignore Not sure how to fix this error
          initialProps,
          propsSelector,
        })

        island._roots = island._roots.concat(rootFragments)
        rendered = true
      }

      load()
      document.addEventListener('DOMContentLoaded', load)
      document.addEventListener('load', load)
    },
    rerender: (newProps) => {
      island._roots.forEach((rootFragment) => {
        renderIsland({
          island,
          widget,
          rootFragment,
          props: { ...island.props, ...newProps },
        })
      })
    },
    destroy: () => {
      island._roots.forEach((rootFragment) => {
        island._rootsToObservers.get(rootFragment)?.disconnect()
        render(null, rootFragment)
      })
    },
  }

  return island
}


================================================
FILE: src/lib.ts
================================================
import { ComponentType, h, render } from 'preact'
import { InitialProps, Island } from './island'

type HostElement = HTMLElement | ShadowRoot

export const isInShadow = (node: HostElement | HTMLOrSVGScriptElement) => {
  return node.getRootNode() instanceof ShadowRoot
}

export const isShadowRoot = (x: unknown): x is ShadowRoot => {
  return x instanceof ShadowRoot
}

export const formatProp = (str: string) => {
  return `${str.charAt(0).toLowerCase()}${str.slice(1)}`
}

export const getPropsFromElement = (
  element: HostElement | HTMLOrSVGScriptElement,
) => {
  // In a shadow dom we replace the host element because it's within the shadow root. However,
  // we want the props of the autonomous custom element.
  const targetElement = isInShadow(element)
    ? (element.getRootNode() as any).host
    : element

  const { dataset } = targetElement

  const props: { [x: string]: any } = {}

  for (var d in dataset) {
    // We don't pull props for inherited attributes
    if (dataset.hasOwnProperty(d) === false) return

    // data-prop or data-props works!
    const propName = formatProp(d.split(/(props?)/).pop() || '')

    if (propName) {
      props[propName] = dataset[d]
    }
  }

  return props
}

export const isValidPropsScript = (element: Element) => {
  return (
    // element.tagName.toLowerCase() === 'script' &&
    ['text/props', 'application/json'].includes(
      element.getAttribute('type') || '',
    )
  )
}

export const getInteriorPropsScriptsForElement = (element: HostElement) => {
  // getElementsByTagName does not exist on shadow roots and within a shadow root
  // the caller can't place in props scripts
  if (isShadowRoot(element)) return []

  return Array.from(element.getElementsByTagName('script')).filter(
    isValidPropsScript,
  )
}

export const getPropsScriptsBySelector = (selector: string) => {
  return Array.from(document.querySelectorAll(selector)).filter(
    isValidPropsScript,
    // Checked by filter call
  ) as HTMLOrSVGScriptElement[]
}

export const getPropsFromScripts = (scripts: HTMLOrSVGScriptElement[]) => {
  let interiorScriptProps: any = {}
  scripts.forEach((script) => {
    // Swallow any potential errors so we don't throw on someone else's page
    try {
      interiorScriptProps = {
        ...interiorScriptProps,
        ...JSON.parse(script.innerHTML),
      }
    } catch (e: any) {}
  })
  return interiorScriptProps
}

/**
 * Get the props from a host element's data attributes
 * @param  {Element} The host element
 * @return {Object}  props object to be passed to the component
 */
export const generateHostElementProps = <P extends InitialProps>(
  island: Island<P>,
  element: HostElement,
  initialProps = {},
  propsSelector: string | undefined | null,
): P => {
  const elementProps = getPropsFromElement(element)

  const currentScriptProps = island._executedScript
    ? getPropsFromElement(island._executedScript)
    : {}
  const interiorScriptProps = getPropsFromScripts(
    getInteriorPropsScriptsForElement(element),
  )

  const propsSelectorProps = propsSelector
    ? getPropsFromScripts(getPropsScriptsBySelector(propsSelector))
    : {}

  return {
    ...initialProps,
    ...elementProps,
    ...currentScriptProps,
    ...propsSelectorProps,
    ...interiorScriptProps,
  }
}

export const getHostElements = ({
  selector,
  inline,
  elementName,
}: {
  selector?: string
  inline: boolean
  /**
   * Passed if targeting web components so that mount in can create web components inside of the host elements
   */
  elementName?: string
}): HostElement[] => {
  const currentScript = document.currentScript

  if (inline && currentScript?.parentNode) {
    // @ts-ignore Not sure on this one
    return [currentScript.parentNode]
  }

  // Next, try to get the selector from the current script
  const maybeSelector = currentScript?.dataset.mountIn

  if (maybeSelector) {
    return Array.from(
      document.querySelectorAll<HTMLElement>(maybeSelector),
    ).map((n) => {
      if (elementName != null) {
        const targetElement = document.createElement(elementName)
        const node = n.appendChild(targetElement)
        return node.shadowRoot != null ? node.shadowRoot : node
      }

      return n
    })
  }

  if (selector) {
    return Array.from(document.querySelectorAll<HTMLElement>(selector)).map(
      (n) => (n.shadowRoot != null ? n.shadowRoot : n),
    )
  }

  return []
}

/**
 * A Preact 11+ implementation of the `replaceNode` parameter from Preact 10.
 *
 * This creates a "Persistent Fragment" (a fake DOM element) containing one or more
 * DOM nodes, which can then be passed as the `parent` argument to Preact's `render()` method.
 *
 * Lifted from: https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
 */
export type RootFragment = any

export function createRootFragment(
  parent: HostElement,
  replaceNode: HostElement | HostElement[],
): RootFragment {
  replaceNode = ([] as HostElement[]).concat(replaceNode)
  var s = replaceNode[replaceNode.length - 1].nextSibling
  function insert(c: HTMLElement, r: HTMLElement) {
    parent.insertBefore(c, r || s)
  }
  // Mutating the parent to add a preact property
  // @ts-expect-error We're mutating the parent to add these properties for Preact
  return (parent.__k = {
    nodeType: 1,
    parentNode: parent,
    firstChild: replaceNode[0],
    childNodes: replaceNode,
    insertBefore: insert,
    appendChild: insert,
    removeChild: function (c: HTMLElement) {
      parent.removeChild(c)
    },
  })
}

export const watchForPropChanges = <P extends InitialProps>({
  island,
  hostElement,
  initialProps,
  onNewProps,
  propsSelector,
}: {
  island: Island<P>
  hostElement: HostElement
  initialProps: any
  onNewProps: (props: P) => void
  propsSelector: string | undefined | null
}) => {
  const observer = new MutationObserver(function (mutations) {
    mutations.forEach(function () {
      onNewProps(
        generateHostElementProps(
          island,
          hostElement,
          initialProps,
          propsSelector,
        ),
      )
    })
  })

  const config = { attributes: true, childList: true, characterData: true }

  if (island._executedScript) {
    observer.observe(island._executedScript, config)
  }

  getInteriorPropsScriptsForElement(hostElement).forEach((script) => {
    observer.observe(script, { ...config, subtree: true })
  })

  if (propsSelector) {
    getPropsScriptsBySelector(propsSelector).forEach((script) => {
      observer.observe(script, { ...config, subtree: true })
    })
  }

  /**
   * If the host element is a shadow root we want to observe on the host of it.
   *
   * Example:
   * <preact-element data-prop-foo="bar">
   *    #shadow-root (open)
   * </preact-element>
   *
   * We want to observe the custom autonomous element, not the shadow root!
   */
  observer.observe(
    isShadowRoot(hostElement) ? hostElement.host! : hostElement,
    config,
  )

  return observer
}

export const renderIsland = <P extends InitialProps>({
  island,
  widget,
  rootFragment,
  props,
}: {
  island: Island<P>
  widget: ComponentType<P>
  rootFragment: RootFragment
  props: P
}) => {
  island.props = props
  render(h(widget, props), rootFragment)
}

export const mount = <P extends InitialProps>({
  island,
  widget,
  hostElements,
  clean,
  replace,
  initialProps,
  propsSelector,
}: {
  island: Island<P>
  widget: ComponentType<P>
  hostElements: Array<HostElement>
  clean: boolean
  replace: boolean
  initialProps: P
  propsSelector?: string
}) => {
  const rootFragments: any = []

  hostElements.forEach((hostElement) => {
    const props = generateHostElementProps<P>(
      island,
      hostElement,
      initialProps,
      propsSelector,
    )
    if (clean) {
      hostElement.replaceChildren()
    }

    let rootFragment: any
    if (replace) {
      rootFragment = createRootFragment(
        hostElement.parentElement || document.body,
        hostElement,
      )
    } else {
      const renderNode = document.createElement('div')
      hostElement.appendChild(renderNode)
      rootFragment = createRootFragment(hostElement, renderNode)
    }

    rootFragments.push(rootFragment)

    renderIsland({ island, widget, rootFragment, props })

    const observer = watchForPropChanges<P>({
      island,
      hostElement,
      initialProps,
      onNewProps: (newProps) => {
        renderIsland({ island, widget, rootFragment, props: newProps })
      },
      propsSelector,
    })

    island._rootsToObservers.set(rootFragment, observer)
  })

  return { rootFragments }
}


================================================
FILE: src/tests/helpers/getById.ts
================================================
export const getById = (x: string) => {
  const element = document.getElementById(x)
  if (element == null) {
    throw new Error(`Could not find element with id: ${x}!`)
  }

  return element
}


================================================
FILE: src/tests/helpers/inlineScript.tsx
================================================
import { createIsland } from '../../island'
import {
  mount,
  getHostElements,
  formatProp,
  getInteriorPropsScriptsForElement,
  getPropsFromScripts,
  generateHostElementProps,
  getPropsFromElement,
  createRootFragment,
  watchForPropChanges,
  isValidPropsScript,
  renderIsland,
  isInShadow,
  isShadowRoot,
} from '../../lib'
import { h, FunctionComponent } from 'preact'

/**
 * Use this helper when you want to test what it's like to use an inline script to use preact-island. This imports and stubs everything the Babel transforms under
 * the hood for Jest to run the things. I'm not sure a better way to test this unless it's importing the dist files but then that requires a build step every time
 * we run tests.
 */
export const InlineScript: FunctionComponent<{
  widget: any
  renderCode: string
  id?: string
}> = ({ widget, renderCode, ...rest }) => {
  return (
    <script
      {...rest}
      dangerouslySetInnerHTML={{
        __html: `
(function() {
const _preact = preact
const _objectSpread = Object.assign
const _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })();
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }


const mount = ${mount};
const getHostElements = ${getHostElements};
const formatProp = ${formatProp};
const getInteriorPropsScriptsForElement = ${getInteriorPropsScriptsForElement};
const getPropsFromScripts = ${getPropsFromScripts};
const generateHostElementProps = ${generateHostElementProps};
const getPropsFromElement = ${getPropsFromElement};
const createRootFragment = ${createRootFragment};
const watchForPropChanges = ${watchForPropChanges};
const isValidPropsScript = ${isValidPropsScript};
const renderIsland = ${renderIsland};
const isInShadow = ${isInShadow};
const isShadowRoot = ${isShadowRoot};

const _lib = {
  mount: ${mount},
  getHostElements: ${getHostElements},
  formatProp: ${formatProp},
  getInteriorPropsScriptsForElement: ${getInteriorPropsScriptsForElement},
  getPropsFromScripts: ${getPropsFromScripts},
  generateHostElementProps: ${generateHostElementProps},
  getPropsFromElement: ${getPropsFromElement},
  createRootFragment: ${createRootFragment},
  watchForPropChanges: ${watchForPropChanges},
  isValidPropsScript: ${isValidPropsScript},
  renderIsland: ${renderIsland}
}

const createIsland = ${createIsland}
const island = createIsland(${widget})
${renderCode}
})()
`,
      }}
    ></script>
  )
}


================================================
FILE: src/tests/island.props.test.tsx
================================================
import { h } from 'preact'
import { createIsland } from '../island'
import { useState } from 'preact/hooks'
import { render, waitFor } from '@testing-library/preact'
import userEvent from '@testing-library/user-event'
import { InlineScript } from './helpers/inlineScript'
import { getById } from './helpers/getById'

const Widget = (props: any) => {
  const [toggle, setToggle] = useState(false)

  return (
    <div data-testid="widget">
      <button
        data-testid="toggleButton"
        onClick={() => {
          setToggle((x) => !x)
        }}
      >
        toggle
      </button>
      {toggle ? <span data-testid="toggled">toggled</span> : null}
      <span data-testid="widgetProps">{JSON.stringify(props)}</span>
    </div>
  )
}

it('should render at the given selector and trigger a rerender (not remount) when new props are passed to the host', async () => {
  const user = userEvent.setup()
  const r = render(
    <div
      data-island="island"
      data-testid="island-host"
      data-prop-test="bananas"
    ></div>,
  )

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
  })

  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"bananas"}',
    ),
  )

  /**
   * We set some local state to the rendered widget to make sure when we update the props that the localized state
   * continues to exist!
   */
  const buttonToggleNode = r.getByTestId('toggleButton')
  user.click(buttonToggleNode)
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())

  const islandHost = r.getByTestId('island-host')
  islandHost.dataset.propTest = 'apples'

  // By asserting that toggled is still in the document it shows we didn't accidentally remount
  // the component on prop changes
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())
  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"apples"}',
    ),
  )
})

it('should render at the given selector and trigger a rerender (not remount) when rerender is called with new props', async () => {
  const user = userEvent.setup()
  const r = render(
    <div
      data-island="island"
      data-testid="island-host"
      data-prop-test="bananas"
    ></div>,
  )

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
  })

  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"bananas"}',
    ),
  )

  /**
   * We set some local state to the rendered widget to make sure when we update the props that the localized state
   * continues to exist!
   */
  const buttonToggleNode = r.getByTestId('toggleButton')
  user.click(buttonToggleNode)
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())

  island.rerender({ test: 'apples' })

  // By asserting that toggled is still in the document it shows we didn't accidentally remount
  // the component on prop changes
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())
  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"apples"}',
    ),
  )
})

it('should render at the given selector and trigger a rerender (not remount) when new props are passed to the child script props tag', async () => {
  const user = userEvent.setup()
  const r = render(
    <div data-island="island" data-testid="island-host">
      <script type="application/json" data-testid="script-props">
        {'{"test": "bananas"}'}
      </script>
    </div>,
  )

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
  })

  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"bananas"}',
    ),
  )

  /**
   * We set some local state to the rendered widget to make sure when we update the props that the localized state
   * continues to exist!
   */
  const buttonToggleNode = r.getByTestId('toggleButton')
  user.click(buttonToggleNode)
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())

  const scriptProps = r.getByTestId('script-props')
  scriptProps.innerHTML = '{"test": "apples"}'

  // By asserting that toggled is still in the document it shows we didn't accidentally remount
  // the component on prop changes
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())
  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"apples"}',
    ),
  )
})

it('should render at the given selector and trigger a rerender (not remount) when new props are passed to the targeted props script', async () => {
  const user = userEvent.setup()
  const r = render(
    <div>
      <div data-island="island" data-testid="island-host"></div>
      <script
        type="application/json"
        data-island-props="test-island"
        id="inline-script-test"
      >
        {'{"test": "bananas"}'}
      </script>
    </div>,
  )

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
    propsSelector: '[data-island-props="test-island"]',
  })

  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"bananas"}',
    ),
  )

  /**
   * We set some local state to the rendered widget to make sure when we update the props that the localized state
   * continues to exist!
   */
  const buttonToggleNode = r.getByTestId('toggleButton')
  user.click(buttonToggleNode)
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())

  const scriptProps = getById('inline-script-test')
  scriptProps.innerHTML = '{"test": "apples"}'

  // By asserting that toggled is still in the document it shows we didn't accidentally remount
  // the component on prop changes
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())
  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"apples"}',
    ),
  )
})

it('should render at the given selector and trigger a rerender (not remount) when new props are passed to the injected script', async () => {
  const user = userEvent.setup()
  const r = render(
    <div>
      <div data-island="island" data-testid="island-host"></div>
      <InlineScript
        widget={Widget}
        data-prop-test="bananas"
        renderCode={`
island.render({
  selector: '[data-island="island"]',
  })
        `}
        id="inline-script"
      />
    </div>,
  )

  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"bananas"}',
    ),
  )

  /**
   * We set some local state to the rendered widget to make sure when we update the props that the localized state
   * continues to exist!
   */
  const buttonToggleNode = r.getByTestId('toggleButton')
  user.click(buttonToggleNode)
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())

  const scriptProps = getById('inline-script')
  scriptProps.dataset.test = 'apples'

  // By asserting that toggled is still in the document it shows we didn't accidentally remount
  // the component on prop changes
  await waitFor(() => expect(r.getByTestId('toggled')).toBeInTheDocument())
  await waitFor(() =>
    expect(r.getByTestId('widgetProps').textContent).toEqual(
      '{"island":"island","testid":"island-host","test":"apples"}',
    ),
  )
})


================================================
FILE: src/tests/island.selector.test.tsx
================================================
import { h } from 'preact'
import { createIsland } from '../island'
import { render, waitFor, within } from '@testing-library/preact'
import { InlineScript } from './helpers/inlineScript'

const Widget = (props: any) => {
  return <div data-testid="widget">{JSON.stringify(props)}</div>
}

it('should render at the given selector', async () => {
  const r = render(<div data-island="island"></div>)

  await waitFor(() => expect(r.queryByTestId('widget')).not.toBeInTheDocument())

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
  })

  await waitFor(() =>
    expect(r.container).toMatchInlineSnapshot(`
      <div>
        <div
          data-island="island"
        >
          <div
            data-testid="widget"
          >
            {"island":"island"}
          </div>
        </div>
      </div>
    `),
  )
})

it('should render inline if prop is passed and take highest priority', async () => {
  const r = render(
    <div data-testid="parent-node">
      <InlineScript
        widget={Widget}
        renderCode={`
island.render({
  inline: true
})
  `}
      />
    </div>,
  )

  await waitFor(() =>
    expect(r.getByTestId('parent-node').lastChild).toMatchInlineSnapshot(`
      <div
        data-testid="widget"
      >
        {"testid":"parent-node"}
      </div>
    `),
  )
})

it('should render using the data-mount-in prop and take priority over a selector if passed', async () => {
  const r = render(
    <div data-testid="parent-node">
      <div data-testid="selector" data-island="selector"></div>
      <div
        data-testid="inlineScriptMountIn"
        data-island="inlineScriptMountIn"
      ></div>
      <InlineScript
        widget={Widget}
        data-mount-in={`[data-island="inlineScriptMountIn"]`}
        renderCode={`
island.render({
  selector: '[data-island="island"]'
})
  `}
      />
    </div>,
  )

  await waitFor(() =>
    expect(
      within(r.getByTestId('selector')).queryByTestId('widget'),
    ).not.toBeInTheDocument(),
  )

  await waitFor(() =>
    expect(
      within(r.getByTestId('inlineScriptMountIn')).queryByTestId('widget'),
    ).toBeInTheDocument(),
  )
})

it('should pass down default props', async () => {
  const r = render(<div data-island="island"></div>)
  const initialProps = {
    apples: 'yes',
    bananas: 1,
    kiwis: true,
  }

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
    initialProps,
  })

  await waitFor(() =>
    expect(r.getByTestId('widget').innerHTML).toEqual(
      JSON.stringify({ ...initialProps, island: 'island' }),
    ),
  )
})

it('should render twice if multiple nodes are found from the selector', async () => {
  const r = render(
    <div>
      <div data-island="island"></div>
      <div data-island="island"></div>
    </div>,
  )
  const initialProps = {
    apples: 'yes',
    bananas: 1,
    kiwis: true,
  }

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
    initialProps,
  })

  // NOTE: Notice there is a different between this one and the other test.
  // This one render inside of two data-island elements whereas the other
  // renders twice inside of the same data widget host element!
  await waitFor(() =>
    expect(r.container).toMatchInlineSnapshot(`
      <div>
        <div>
          <div
            data-island="island"
          >
            <div
              data-testid="widget"
            >
              {"apples":"yes","bananas":1,"kiwis":true,"island":"island"}
            </div>
          </div>
          <div
            data-island="island"
          >
            <div
              data-testid="widget"
            >
              {"apples":"yes","bananas":1,"kiwis":true,"island":"island"}
            </div>
          </div>
        </div>
      </div>
    `),
  )
})

it('should render multiple times if render is called twice', async () => {
  const r = render(<div data-island="island"></div>)
  const initialProps = {
    apples: 'yes',
    bananas: 1,
    kiwis: true,
  }

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
    initialProps,
  })
  island.render({
    selector: '[data-island="island"]',
    initialProps,
  })

  await waitFor(() =>
    expect(r.container).toMatchInlineSnapshot(`
      <div>
        <div
          data-island="island"
        >
          <div
            data-testid="widget"
          >
            {"apples":"yes","bananas":1,"kiwis":true,"island":"island"}
          </div>
          <div
            data-testid="widget"
          >
            {"apples":"yes","bananas":1,"kiwis":true,"island":"island"}
          </div>
        </div>
      </div>
    `),
  )
})

it('should replace the node at given selector if prop is given', async () => {
  const r = render(<div data-island="island"></div>)

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
    replace: true,
  })

  await waitFor(() =>
    expect(r.container).toMatchInlineSnapshot(`
      <div>
        <div
          data-testid="widget"
        >
          {"island":"island"}
        </div>
      </div>
    `),
  )
})

it('should destroy mounted islands when destroy called', async () => {
  const r = render(<div data-island="island"></div>)

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
  })

  await waitFor(() =>
    expect(r.container).toMatchInlineSnapshot(`
      <div>
        <div
          data-island="island"
        >
          <div
            data-testid="widget"
          >
            {"island":"island"}
          </div>
        </div>
      </div>
    `),
  )

  island.destroy()

  await waitFor(() =>
    expect(r.container).toMatchInlineSnapshot(`
      <div>
        <div
          data-island="island"
        />
      </div>
    `),
  )
})

it('should destroy mounted islands when destroy called with replace: true', async () => {
  const r = render(<div data-island="island"></div>)

  const island = createIsland(Widget)
  island.render({
    selector: '[data-island="island"]',
    replace: true,
  })

  await waitFor(() =>
    expect(r.container).toMatchInlineSnapshot(`
      <div>
        <div
          data-testid="widget"
        >
          {"island":"island"}
        </div>
      </div>
    `),
  )

  island.destroy()

  await waitFor(() => expect(r.container).toMatchInlineSnapshot(`<div />`))
})


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ES2015",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "preserveWatchOutput": true,
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": ""
  },
  "include": ["src"],
  "exclude": ["**/tests/**"]
}
Download .txt
gitextract_f_30itg8/

├── .editorconfig
├── .gitignore
├── .npmignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config/
│   └── setupTests.js
├── example/
│   ├── declaration.d.ts
│   ├── package.json
│   ├── src/
│   │   ├── call-to-action.island.css
│   │   ├── call-to-action.island.tsx
│   │   ├── email-subscribe.island.css
│   │   ├── email-subscribe.island.tsx
│   │   ├── global.d.ts
│   │   ├── index.ts
│   │   ├── pokemon.clean.island.tsx
│   │   ├── pokemon.component.tsx
│   │   ├── pokemon.global.island.tsx
│   │   ├── pokemon.initial-props.island.tsx
│   │   ├── pokemon.inline.island.tsx
│   │   ├── pokemon.island.css
│   │   ├── pokemon.island.tsx
│   │   ├── pokemon.props-selector.island.tsx
│   │   ├── pokemon.replace.island.tsx
│   │   ├── template.html
│   │   └── utils.ts
│   ├── tsconfig.json
│   └── webpack.config.js
├── example-react/
│   ├── declaration.d.ts
│   ├── package.json
│   ├── src/
│   │   ├── call-to-action.island.css
│   │   ├── call-to-action.react.island.tsx
│   │   ├── pokemon.component.tsx
│   │   ├── pokemon.island.css
│   │   ├── pokemon.react.island.tsx
│   │   └── utils.ts
│   └── tsconfig.json
├── jest.config.js
├── package.json
├── src/
│   ├── index.ts
│   ├── island-web-components.tsx
│   ├── island.ts
│   ├── lib.ts
│   └── tests/
│       ├── helpers/
│       │   ├── getById.ts
│       │   └── inlineScript.tsx
│       ├── island.props.test.tsx
│       └── island.selector.test.tsx
└── tsconfig.json
Download .txt
SYMBOL INDEX (10 symbols across 3 files)

FILE: src/island-web-components.tsx
  type InitialPropsWebComponent (line 5) | type InitialPropsWebComponent = { [x: string]: any }
  class Element (line 19) | class Element extends HTMLElement {
    method constructor (line 20) | constructor() {
    method constructor (line 62) | constructor() {
  class Element (line 61) | class Element extends HTMLElement {
    method constructor (line 20) | constructor() {
    method constructor (line 62) | constructor() {

FILE: src/island.ts
  type InitialProps (line 4) | type InitialProps = { [x: string]: any }
  type Island (line 6) | type Island<P extends InitialProps> = {

FILE: src/lib.ts
  type HostElement (line 4) | type HostElement = HTMLElement | ShadowRoot
  type RootFragment (line 172) | type RootFragment = any
  function createRootFragment (line 174) | function createRootFragment(
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (88K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newli"
  },
  {
    "path": ".gitignore",
    "chars": 597,
    "preview": "*.jks\n*.p8\n*.p12\n*.mobileprovision\n*.orig.*\nweb-build/\nnode_modules\n\ndist\n\n**/public/build\n.netlify\n\n.cache\n.next\n.env\ns"
  },
  {
    "path": ".npmignore",
    "chars": 94,
    "preview": "!dist\nexample/\ncoverage/\ndocs/\njest.config.js\ntsconfig.json\nconfig/\n.editorconfig\n.prettierrc\n"
  },
  {
    "path": ".prettierrc",
    "chars": 95,
    "preview": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"semi\": false,\n  \"endOfLine\": \"auto\"\n}\n\n  "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1148,
    "preview": "## 1.0.4\n\n- Add subtree checking for prop scripts so that it works well in a React rerender context\n\n## 1.0.5\n\n- Bad pub"
  },
  {
    "path": "LICENSE",
    "chars": 1051,
    "preview": "Copyright 2022 Marcus Wood\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this softwar"
  },
  {
    "path": "README.md",
    "chars": 19408,
    "preview": "<div align=\"center\">\n  <img src=\"./docs/preact-island.svg\" align=\"center\" />\n</div>\n<div align=\"center\">\n  <h1 align=\"ce"
  },
  {
    "path": "config/setupTests.js",
    "chars": 182,
    "preview": "import 'regenerator-runtime/runtime'\nimport '@testing-library/jest-dom'\n\nimport preact from 'preact'\nimport * as hook fr"
  },
  {
    "path": "example/declaration.d.ts",
    "chars": 23,
    "preview": "declare module '*.css'\n"
  },
  {
    "path": "example/package.json",
    "chars": 799,
    "preview": "{\n  \"name\": \"preact-island-examples\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/ind"
  },
  {
    "path": "example/src/call-to-action.island.css",
    "chars": 975,
    "preview": ".cta_button {\n  background-color: none;\n  border: none;\n  background-color: #294eab;\n  border-radius: 5px;\n  padding: 10"
  },
  {
    "path": "example/src/call-to-action.island.tsx",
    "chars": 1538,
    "preview": "import { createIslandWebComponent, WebComponentPortal } from '../../src'\nimport { useState } from 'preact/hooks'\nimport "
  },
  {
    "path": "example/src/email-subscribe.island.css",
    "chars": 683,
    "preview": ".email__container {\n  border-radius: 5px;\n  border: 1px solid #eae7e7;\n  padding: 1rem;\n}\n\n.email__title {\n  font-family"
  },
  {
    "path": "example/src/email-subscribe.island.tsx",
    "chars": 1376,
    "preview": "import { createIsland } from '../../src'\nimport { useState } from 'preact/hooks'\nimport { injectCSS } from './utils'\nimp"
  },
  {
    "path": "example/src/global.d.ts",
    "chars": 22,
    "preview": "declare module '*css'\n"
  },
  {
    "path": "example/src/index.ts",
    "chars": 161,
    "preview": "// This file is only used for developing the snippet in the sandbox\nimport './call-to-action.island'\nimport './email-sub"
  },
  {
    "path": "example/src/pokemon.clean.island.tsx",
    "chars": 307,
    "preview": "import { createIsland } from '../../dist/index.module'\nimport { Pokemon } from './pokemon.component'\nimport style from '"
  },
  {
    "path": "example/src/pokemon.component.tsx",
    "chars": 1281,
    "preview": "import { useEffect, useState } from 'preact/hooks'\n\nexport const Pokemon = ({ pokemon }: { pokemon: string }) => {\n  con"
  },
  {
    "path": "example/src/pokemon.global.island.tsx",
    "chars": 320,
    "preview": "import { createIsland } from '../../dist/index.module'\nimport { Pokemon } from './pokemon.component'\nimport style from '"
  },
  {
    "path": "example/src/pokemon.initial-props.island.tsx",
    "chars": 317,
    "preview": "import { createIsland } from '../../src'\nimport { Pokemon } from './pokemon.component'\nimport style from './pokemon.isla"
  },
  {
    "path": "example/src/pokemon.inline.island.tsx",
    "chars": 255,
    "preview": "import { createIsland } from '../../src'\nimport { Pokemon } from './pokemon.component'\nimport style from './pokemon.isla"
  },
  {
    "path": "example/src/pokemon.island.css",
    "chars": 253,
    "preview": ".pokemon__container {\n  border-radius: 5px;\n  border: 1px solid #eae7e7;\n  padding: 1rem;\n  display: inline-block;\n}\n\n.p"
  },
  {
    "path": "example/src/pokemon.island.tsx",
    "chars": 278,
    "preview": "import { createIsland } from '../../src'\nimport { Pokemon } from './pokemon.component'\nimport style from './pokemon.isla"
  },
  {
    "path": "example/src/pokemon.props-selector.island.tsx",
    "chars": 332,
    "preview": "import { createIsland } from '../../src'\nimport { Pokemon } from './pokemon.component'\nimport style from './pokemon.isla"
  },
  {
    "path": "example/src/pokemon.replace.island.tsx",
    "chars": 295,
    "preview": "import { createIsland } from '../../src'\nimport { Pokemon } from './pokemon.component'\nimport style from './pokemon.isla"
  },
  {
    "path": "example/src/template.html",
    "chars": 1433,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Widgets</title>\n    <meta name=\"viewpo"
  },
  {
    "path": "example/src/utils.ts",
    "chars": 123,
    "preview": "export const injectCSS = (style: string) => {\n  document.head.insertAdjacentHTML('beforeend', `<style>${style}</style>`)"
  },
  {
    "path": "example/tsconfig.json",
    "chars": 1051,
    "preview": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"allowJs\": true,\n    \"esModuleInte"
  },
  {
    "path": "example/webpack.config.js",
    "chars": 2792,
    "preview": "const path = require('path')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst TerserPlugin = require('ters"
  },
  {
    "path": "example-react/declaration.d.ts",
    "chars": 23,
    "preview": "declare module '*.css'\n"
  },
  {
    "path": "example-react/package.json",
    "chars": 780,
    "preview": "{\n  \"name\": \"preact-island-react-examples\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"source\": \"src/index.tsx\",\n  \"m"
  },
  {
    "path": "example-react/src/call-to-action.island.css",
    "chars": 975,
    "preview": ".cta_button {\n  background-color: none;\n  border: none;\n  background-color: #294eab;\n  border-radius: 5px;\n  padding: 10"
  },
  {
    "path": "example-react/src/call-to-action.react.island.tsx",
    "chars": 1456,
    "preview": "import { createIsland } from '../../dist/index.module'\nimport { useState } from 'react'\nimport { createPortal } from 're"
  },
  {
    "path": "example-react/src/pokemon.component.tsx",
    "chars": 1274,
    "preview": "import { useEffect, useState } from 'react'\n\nexport const Pokemon = ({ pokemon }: { pokemon: string }) => {\n  const [pok"
  },
  {
    "path": "example-react/src/pokemon.island.css",
    "chars": 253,
    "preview": ".pokemon__container {\n  border-radius: 5px;\n  border: 1px solid #eae7e7;\n  padding: 1rem;\n  display: inline-block;\n}\n\n.p"
  },
  {
    "path": "example-react/src/pokemon.react.island.tsx",
    "chars": 292,
    "preview": "import { createIsland } from '../../dist/index.module'\nimport { Pokemon } from './pokemon.component'\nimport style from '"
  },
  {
    "path": "example-react/src/utils.ts",
    "chars": 123,
    "preview": "export const injectCSS = (style: string) => {\n  document.head.insertAdjacentHTML('beforeend', `<style>${style}</style>`)"
  },
  {
    "path": "example-react/tsconfig.json",
    "chars": 626,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "jest.config.js",
    "chars": 279,
    "preview": "export default {\n  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],\n  preset: 'jest-preset-preact',\n"
  },
  {
    "path": "package.json",
    "chars": 2009,
    "preview": "{\n  \"name\": \"preact-island\",\n  \"version\": \"1.2.0\",\n  \"type\": \"module\",\n  \"description\": \"🏝 Create your own slice of para"
  },
  {
    "path": "src/index.ts",
    "chars": 65,
    "preview": "export * from './island'\nexport * from './island-web-components'\n"
  },
  {
    "path": "src/island-web-components.tsx",
    "chars": 2533,
    "preview": "import { ComponentType, VNode } from 'preact'\nimport { createPortal, FC, useEffect, useMemo } from 'preact/compat'\nimpor"
  },
  {
    "path": "src/island.ts",
    "chars": 5898,
    "preview": "import { getHostElements, mount, RootFragment, renderIsland } from './lib'\nimport { render, ComponentType } from 'preact"
  },
  {
    "path": "src/lib.ts",
    "chars": 8591,
    "preview": "import { ComponentType, h, render } from 'preact'\nimport { InitialProps, Island } from './island'\n\ntype HostElement = HT"
  },
  {
    "path": "src/tests/helpers/getById.ts",
    "chars": 195,
    "preview": "export const getById = (x: string) => {\n  const element = document.getElementById(x)\n  if (element == null) {\n    throw "
  },
  {
    "path": "src/tests/helpers/inlineScript.tsx",
    "chars": 3145,
    "preview": "import { createIsland } from '../../island'\nimport {\n  mount,\n  getHostElements,\n  formatProp,\n  getInteriorPropsScripts"
  },
  {
    "path": "src/tests/island.props.test.tsx",
    "chars": 7860,
    "preview": "import { h } from 'preact'\nimport { createIsland } from '../island'\nimport { useState } from 'preact/hooks'\nimport { ren"
  },
  {
    "path": "src/tests/island.selector.test.tsx",
    "chars": 6503,
    "preview": "import { h } from 'preact'\nimport { createIsland } from '../island'\nimport { render, waitFor, within } from '@testing-li"
  },
  {
    "path": "tsconfig.json",
    "chars": 583,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  }
]

About this extraction

This page contains the full source code of the mwood23/preact-island GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 49 files (78.9 KB), approximately 22.2k tokens, and a symbol index with 10 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!