@wrdagency/react-islands 2.1.5 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,77 +10,50 @@ Create your island. Here we use an existing component and use our Islands direct
10
10
 
11
11
  ```
12
12
  // ./islands/my-component.tsx
13
- import MyComponent from "./components";
14
- import { createIsland } from "@wrdagency/react-islands";
13
+ import { Island } from "@wrdagency/react-islands";
15
14
 
16
- export const myComponentIsland = createIsland(MyComponent, {
15
+ const MyCompontent: React.FC = () => {
16
+ <p>
17
+ Hello World
18
+ </p>
19
+ }
20
+
21
+ export default new Island(MyComponent, {
17
22
  name: "my-component",
18
23
  });
19
24
  ```
20
25
 
21
- ```
22
- // ./islands/index.ts
23
- export * from "./my-component";
24
- ```
25
-
26
- On the client you can then render each of those islands. They'll automatically hook into the DOM where the selector is matched (similar to a portal) to create an island of reactivity.
27
-
28
- ```
29
- // index.ts
30
- import * as islands from "./islands";
31
- import { hydrateIslands } from "@wrdagency/react-islands";
32
-
33
- hydrateIslands( islands );
34
-
35
- ```
36
-
37
- Create a pre-render script. You can configure your build tool to use this as a seperate entrypoint.
38
-
39
- ```
40
- // prerender.ts
41
-
42
- import path from "node:path";
43
- import { fileURLToPath } from "node:url";
44
- import * as islands from "./islands";
45
- import { prerenderIslands } from "@wrdagency/react-islands/server";
46
-
47
- const __filename = fileURLToPath(import.meta.url);
48
- const __dirname = path.dirname(__filename);
49
- const outDir = path.resolve(__dirname, "ssg");
50
-
51
- prerenderIslands({ islands, outDir });
26
+ Create a configuration file for your islands and use the built-in CLI commands to build, watch, or serve them.
27
+
28
+ ```json
29
+ // islands.config.json
30
+ {
31
+ "islands": {
32
+ "my-component": "./src/islands/my-component.tsx"
33
+ },
34
+ "output": "./dist/",
35
+ "minify": true,
36
+ "ssg": true,
37
+ "typescript": true
38
+ }
52
39
  ```
53
40
 
54
- For our example we're using Vite. We'll build our prerendering script.
41
+ Run the package CLI commands from your project root:
55
42
 
56
43
  ```
57
- npx vite build --ssr ./src/prerender.tsx --outDir ./dist
44
+ npx react-islands build --config islands.config.json
45
+ npx react-islands watch --config islands.config.json
46
+ npx react-islands serve --config islands.config.json
58
47
  ```
59
48
 
60
- And then we can run that compiled script. It'll create all of our statically rendered islands and put them into the `outDir` we specified.
61
-
62
- ```
63
- node ./dist/prerender.js
64
- ```
65
-
66
- For convenience we'd recommend setting up a script in your `package.json` for this like so:
67
-
68
- ```
69
- "scripts": {
70
- "prerender": "npx vite build --ssr ./src/prerender.tsx --outDir ./dist && node ./dist/prerender.js",
71
- },
72
- ```
49
+ The `build` command compiles each island and produces the shared output bundle. The `watch` command rebuilds on file changes. The `serve` command starts a local dev server with hot reload and prints each island URL.
73
50
 
74
51
  ## API
75
52
 
76
- ### `createIsland`
77
-
78
- `(component: React.FC, options: IslandOpts) => Island`
79
-
80
- Creates an island.
81
-
82
53
  #### `IslandOpts`
83
54
 
55
+ When you create an Island you can pass the following options,
56
+
84
57
  | Options | Type | Default | Description |
85
58
  | ------------ | -------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
86
59
  | name | string | Required | The name of the island. Used for the default selector and the filename of pre-rendering. |
@@ -99,12 +72,3 @@ Creates a version of your React component with props already set. Useful for cre
99
72
  `() => boolean`
100
73
 
101
74
  Checks if the current environment is the server. Useful for disabled certain features not available during the prerender step.
102
-
103
- ### `prerenderIslands`
104
-
105
- `(options: PrerenderIslandsOpts) => Promise<void>`
106
-
107
- | Option | Type | Description |
108
- | ------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
109
- | islands | Record<string, Island> | The islands the pre-render. The key of the record is not used, it's just useful to accept a record if we're using `import * as Islands`. |
110
- | ourDir | string | Path of the directory to output the static HTML to. This directory will be emptied before pre-rendering begins. |
package/bin/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import e from"@rollup/plugin-commonjs";import n from"@rollup/plugin-node-resolve";import t from"@rollup/plugin-replace";import o from"@rollup/plugin-terser";import r from"path";import{rollup as s,watch as i}from"rollup";import a from"@rollup/plugin-typescript";import{typescriptPaths as c}from"rollup-plugin-typescript-paths";import l,{readFileSync as u}from"fs";import{exec as p}from"child_process";import{promisify as m}from"util";import d from"yocto-spinner";import f from"command-line-args";import g from"command-line-usage";class h{lines=[];import(e,n){return n||(n=h.packageNameToProperty(e)),this.add(`import * as ${n} from "${e}"`)}createGlobalObject(e){return this.add(e.split(".").map(e=>e.trim()).filter(Boolean).map((e,n,t)=>{const o="window."+t.slice(0,n+1).join(".");return`${o} = ${o} || {}`}))}setGlobalObjectProperty(e,n,t){return this.add(`window.${e}['${n}'] = ${t}`)}static packageNameToProperty(e){return e.replace(/^@/,"").replace(/\//g,"_").replace(/[-_](.)/g,(e,n)=>n.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())}static renderReactComponentToFile(e,n){return`var server = require('react-dom/server');\nvar fs = require('node:fs/promises');\nvar path = require('node:path');\nconst html = server.renderToString( ${n}( {} ) );\nconst file = path.resolve(__dirname, '${e}');\nfs.writeFile(file, html, { flag: "w+" });`}add(e){return Array.isArray(e)?this.lines.push(...e):this.lines.push(e),this}out(){return this.lines.join(";\n")}}function y(e){const{dependencies:n,namespace:t=""}=e,o="\0virtual-entry";return{name:"rollup-plugin-virtualize-dependency",resolveId:e=>"virtual-entry"===e?o:null,load(e){if(e!==o)return null;const r=new h;r.createGlobalObject(t);for(const e of n){const n=h.packageNameToProperty(e);r.import(e,n),r.setGlobalObjectProperty(t,n,n)}return r.out()}}}async function w(e){const{output:n,...t}=e;let o,r=!1;try{o=await s(t),await o.write(n)}catch(e){r=!0,console.error(e)}return o&&await o.close(),!r}function b(e){const{output:n,...t}=e,o=i({...t,output:n,watch:{clearScreen:!1}});return o.on("event",e=>{"BUNDLE_END"===e.code&&e.result&&e.result.close()}),o}function j(s){const{output:i,minify:a,jsx:c,common:l}=s;return{input:"virtual-entry",jsx:c,output:{name:"Islands._Common",file:r.resolve(i,"common.js"),format:"iife"},plugins:[t({preventAssignment:!0,"process.env.NODE_ENV":JSON.stringify("production")}),y({dependencies:l,namespace:"Islands._Common"}),n(),e(),a&&o()]}}const v=m(p);class ${line(e){console.log(e)}async spinner(e,n){const t=d({text:`${e}...`}).start();await n()?t.success(`Succeeded: ${e}`):t.warning(`Failed: ${e}`)}watcher(e,n){const t=d({text:`Rebuilding ${e}...`});let o=0;n.on("event",n=>{if("START"===n.code)t.clear(),t.start(),o=Date.now();else if("END"===n.code){const n=Date.now()-o;t.success(`Rebuilt: ${e} in ${n}ms.`)}else"ERROR"===n.code&&(t.warning(`Failed: ${e}`),console.error(n.error))})}async command(e){try{const{stdout:n,stderr:t}=await v(e);return n&&console.log(n),t&&console.error(t),n}catch(e){throw console.error(e),e}}}function x(e){const{deleteAfterRunning:n=!1}=e;return{name:"rollup-plugin-run-script-after-builder",writeBundle(e,t){const o=e.dir?e.dir:r.dirname(e.file||"");for(const[e,s]of Object.entries(t)){const t=r.resolve(o,e);if("chunk"!==s.type||!s.isEntry)return;if(!t&&!t.endsWith("js"))return;if(!l.existsSync(t))return;(new $).command(`node ${t}`).then(()=>{n&&l.unlinkSync(t)})}}}}function N(r,s){const{name:i,input:l,output:u,minify:p,ssg:m,jsx:d,typescript:f,common:g,define:h}=r,{subName:y,format:w,globals:b={},prefix:j,suffix:v,plugins:$=[]}=s;return{input:l,external:Object.keys(b),jsx:d,output:{name:`Islands.${i}`,globals:b,format:w,entryFileNames:`${i}/${y}`,dir:u,banner:j&&(e=>j(e.name)),footer:v&&(e=>v(e.name))},plugins:[n({extensions:[".cjs",".mjs",".js",".json",".node",".jsx",".ts",".tsx"]}),e(),f&&a({outputToFilesystem:!1,noForceEmit:!0,compilerOptions:{outDir:u,jsx:d}}),f&&c(),t({preventAssignment:!0,values:{...h,"process.env.NODE_ENV":JSON.stringify("production")}}),p&&o(),...$]}}function O(e){return N(e,{subName:"client.js",format:"iife",globals:e.common.reduce((e,n)=>({...e,[n]:`Islands._Common["${h.packageNameToProperty(n)}"]`}),{}),suffix:()=>`\nwindow.Islands['${e.name}']?.render('${e.name}')`})}function k(e){const n=[O(e)];return e.ssg&&n.push(function(e){return N(e,{subName:"server.cjs",format:"cjs",suffix:()=>h.renderReactComponentToFile("ssg.html","module.exports.component"),plugins:[x({deleteAfterRunning:!0})]})}(e)),n}function S(e){return async function(e){let n=!0;for(const t of e)await w(t)||(n=!1);return n}(k(e))}function C(e){return b(O(e))}class T{args;callback;description;constructor(e){let{args:n,callback:t,description:o}=e;n.push({name:"help",alias:"h",type:Boolean,description:"Display this usage guide."}),this.args=n,this.callback=t,this.description=o}run(e){const{help:n,...t}=f(this.args,{argv:e});if(!n)return this.callback(t);console.log(g([{header:"Options",optionList:this.args}]))}}function _(e){const n=u(e,"utf8");return function(e){const n=(e,n)=>"boolean"==typeof e?e:n;return{islands:e.islands,output:e.output||"./dist/",minify:n(e.minify,!0),ssg:n(e.ssg,!0),jsx:e.jsx||"react-jsx",typescript:n(e.typescript,!0),common:e.common||["react","react/jsx-runtime","react-dom/client","@wrdagency/react-islands"],define:e.define||{}}}(JSON.parse(n))}!async function(e){const{command:n,help:t=!1,_unknown:o=[]}=f([{name:"command",type:String,defaultOption:!0},{name:"help",alias:"h",type:Boolean,description:"Display this usage guide."}],{stopAtFirstUnknown:!0});if(n&&n in e)return e[n].run([...o,t&&"--help"].filter(Boolean));if(t){const n=Object.keys(e).map(n=>({name:n,summary:e[n].description})),t=g([{header:"Synopsis",content:"npx react-islands <command> <options>"},{header:"Command List",content:n}]);console.log(t)}else console.error(`Command '${n}' not recognized by React Islands.`)}({build:new T({description:"Build and statically render the islands.",args:[{name:"config",type:String,alias:"c",description:"The config file to use.",defaultValue:"islands.config.json"}],callback:async e=>{const{config:n}=e,t=new $,o=_(n);o.common&&await t.spinner("Creating common dependencies file",async()=>w(j(o)));for(const[e,n]of Object.entries(o.islands))await t.spinner(`Creating island ${e}`,()=>S({name:e,input:n,...o}))}}),watch:new T({description:"Watch & build the islands for development",args:[{name:"config",type:String,alias:"c",description:"The config file to use.",defaultValue:"islands.config.json"}],callback:async e=>{const{config:n}=e,t=new $,o=_(n);if(o.common){const e=b(j(o));t.watcher("Common Dependencies",e)}for(const[e,n]of Object.entries(o.islands)){const r=C({name:e,input:n,...o});t.watcher(e,r)}}})});
2
+ import e from"@rollup/plugin-commonjs";import t from"@rollup/plugin-node-resolve";import n from"@rollup/plugin-replace";import o from"@rollup/plugin-terser";import r from"path";import{watch as i,rollup as s}from"rollup";import c from"@rollup/plugin-typescript";import{typescriptPaths as a}from"rollup-plugin-typescript-paths";import l,{readFileSync as d,existsSync as u,watch as p,statSync as m}from"fs";import{exec as f}from"child_process";import{promisify as h}from"util";import v from"yocto-spinner";import y from"command-line-args";import g from"command-line-usage";import{readFile as w}from"fs/promises";import{createServer as b}from"http";import j from"mime";function $(e,t){var n={};for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&t.indexOf(o)<0&&(n[o]=e[o]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(o=Object.getOwnPropertySymbols(e);r<o.length;r++)t.indexOf(o[r])<0&&Object.prototype.propertyIsEnumerable.call(e,o[r])&&(n[o[r]]=e[o[r]])}return n}function O(e,t,n,o){return new(n||(n=Promise))(function(r,i){function s(e){try{a(o.next(e))}catch(e){i(e)}}function c(e){try{a(o.throw(e))}catch(e){i(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,c)}a((o=o.apply(e,t||[])).next())})}"function"==typeof SuppressedError&&SuppressedError;class x{constructor(){this.lines=[]}import(e,t){return t||(t=x.packageNameToProperty(e)),this.add(`import * as ${t} from "${e}"`)}createGlobalObject(e){return this.add(e.split(".").map(e=>e.trim()).filter(Boolean).map((e,t,n)=>{const o="window."+n.slice(0,t+1).join(".");return`${o} = ${o} || {}`}))}setGlobalObjectProperty(e,t,n){return this.add(`window.${e}['${t}'] = ${n}`)}static packageNameToProperty(e){return e.replace(/^@/,"").replace(/\//g,"_").replace(/[-_](.)/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())}static renderReactComponentToFile(e,t){return`var server = require('react-dom/server');\nvar fs = require('node:fs/promises');\nvar path = require('node:path');\nconst html = server.renderToString( ${t}( {} ) );\nconst file = path.resolve(__dirname, '${e}');\nfs.writeFile(file, html, { flag: "w+" });`}add(e){return Array.isArray(e)?this.lines.push(...e):this.lines.push(e),this}out(){return this.lines.join(";\n")}}function C(e){const{dependencies:t,namespace:n=""}=e,o="\0virtual-entry";return{name:"rollup-plugin-virtualize-dependency",resolveId:e=>"virtual-entry"===e?o:null,load(e){if(e!==o)return null;const r=new x;r.createGlobalObject(n);for(const e of t){const t=x.packageNameToProperty(e);r.import(e,t),r.setGlobalObjectProperty(n,t,t)}return r.out()}}}function S(e){return O(this,void 0,void 0,function*(){const{output:t}=e,n=$(e,["output"]);let o,r=!1;try{o=yield s(n),yield o.write(t)}catch(e){r=!0,console.error(e)}return o&&(yield o.close()),!r})}function R(e){const{output:t}=e,n=$(e,["output"]),o=i(Object.assign(Object.assign({},n),{output:t,watch:{clearScreen:!1}}));return o.on("event",e=>{"BUNDLE_END"===e.code&&e.result&&e.result.close()}),o}function N(i){const{output:s,minify:c,jsx:a,common:l}=i;return{input:"virtual-entry",jsx:a,output:{name:"Islands._Common",file:r.resolve(s,"common.js"),format:"iife"},plugins:[n({preventAssignment:!0,"process.env.NODE_ENV":JSON.stringify("production")}),C({dependencies:l,namespace:"Islands._Common"}),t(),e(),c&&o()]}}function T(e){return R(N(e))}const E=h(f);class k{line(e){console.log(e)}spinner(e,t){return O(this,void 0,void 0,function*(){const n=v({text:`${e}...`}).start();(yield t())?n.success(`Succeeded: ${e}`):n.warning(`Failed: ${e}`)})}watcher(e,t){const n=v({text:`Rebuilding ${e}...`});let o=0;t.on("event",t=>{if("START"===t.code)n.clear(),n.start(),o=Date.now();else if("END"===t.code){const t=Date.now()-o;n.success(`Rebuilt: ${e} in ${t}ms.`)}else"ERROR"===t.code&&(n.warning(`Failed: ${e}`),console.error(t.error))})}command(e){return O(this,void 0,void 0,function*(){try{const{stdout:t,stderr:n}=yield E(e);return t&&console.log(t),n&&console.error(n),t}catch(e){throw console.error(e),e}})}}function I(e){const{deleteAfterRunning:t=!1}=e;return{name:"rollup-plugin-run-script-after-builder",writeBundle(e,n){const o=e.dir?e.dir:r.dirname(e.file||"");for(const[e,i]of Object.entries(n)){const n=r.resolve(o,e);if("chunk"!==i.type||!i.isEntry)return;if(!n&&!n.endsWith("js"))return;if(!l.existsSync(n))return;(new k).command(`node ${n}`).then(()=>{t&&l.unlinkSync(n)})}}}}function D(r,i){const{name:s,input:l,output:d,minify:u,jsx:p,typescript:m,define:f}=r,{external:h=[],subName:v,format:y,globals:g={},prefix:w,suffix:b,plugins:j=[]}=i;return{input:l,external:[...Object.keys(g),...h],jsx:p,output:{name:`Islands.${s}`,globals:g,format:y,entryFileNames:`${s}/${v}`,dir:d,banner:w&&(e=>w(e.name)),footer:b&&(e=>b(e.name))},plugins:[t({extensions:[".cjs",".mjs",".js",".json",".node",".jsx",".ts",".tsx"]}),e(),m&&c({outputToFilesystem:!1,noForceEmit:!0,compilerOptions:{outDir:d,jsx:p}}),m&&a(),n({preventAssignment:!0,values:Object.assign(Object.assign({},f),{"process.env.NODE_ENV":JSON.stringify("production")})}),u&&o(),...j]}}function _(e){return D(e,{subName:"client.js",format:"iife",globals:e.common.reduce((e,t)=>Object.assign(Object.assign({},e),{[t]:`Islands._Common["${x.packageNameToProperty(t)}"]`}),{}),suffix:()=>`\nwindow.Islands['${e.name}']?.render('${e.name}')`})}function H(e){const t=[_(e)];return e.ssg&&t.push(function(e){return D(e,{external:["react","react-dom"],subName:"server.cjs",format:"cjs",suffix:()=>x.renderReactComponentToFile("ssg.html","module.exports.component"),plugins:[I({deleteAfterRunning:!0})]})}(e)),t}function U(e){return function(e){return O(this,void 0,void 0,function*(){let t=!0;for(const n of e)(yield S(n))||(t=!1);return t})}(H(e))}function A(e){return R(_(e))}class F{constructor(e){let{args:t,callback:n,description:o}=e;t.push({name:"help",alias:"h",type:Boolean,description:"Display this usage guide."}),this.args=t,this.callback=n,this.description=o}run(e){const t=y(this.args,{argv:e}),{help:n}=t,o=$(t,["help"]);if(!n)return this.callback(o);console.log(g([{header:"Options",optionList:this.args}]))}}function P(e){const t=d(e,"utf8");return function(e){const t=(e,t)=>"boolean"==typeof e?e:t;return{islands:e.islands,output:e.output||"./dist/",minify:t(e.minify,!0),ssg:t(e.ssg,!0),jsx:e.jsx||"react-jsx",typescript:t(e.typescript,!0),common:e.common||["react","react/jsx-runtime","react-dom/client","@wrdagency/react-islands"],define:e.define||{},serve:{css:e.serve?Array.isArray(e.serve.css)?e.serve.css:e.serve.css?[e.serve.css]:[]:[]}}}(JSON.parse(t))}var B=new F({description:"Build and statically render the islands.",args:[{name:"config",type:String,alias:"c",description:"The config file to use.",defaultValue:"islands.config.json"}],callback:e=>O(void 0,void 0,void 0,function*(){const{config:t}=e,n=new k,o=P(t);o.common&&(yield n.spinner("Creating common dependencies file",()=>O(void 0,void 0,void 0,function*(){return S(N(o))})));for(const[e,t]of Object.entries(o.islands))yield n.spinner(`Creating island ${e}`,()=>U(Object.assign({name:e,input:t},o)))})}),W=new F({description:"Watch & build the islands for development",args:[{name:"config",type:String,alias:"c",description:"The config file to use.",defaultValue:"islands.config.json"}],callback:e=>O(void 0,void 0,void 0,function*(){const{config:t}=e,n=new k,o=P(t);if(o.common){const e=T(o);n.watcher("Common Dependencies",e)}for(const[e,t]of Object.entries(o.islands)){const r=A(Object.assign({name:e,input:t},o));n.watcher(e,r)}})});const G=3e3,L="localhost",V="/events",M="/islands",q="/__react-islands-css";function z(e,t,n){return O(this,void 0,void 0,function*(){const o=r.resolve(t,`.${e}`);if(!function(e,t){const n=r.relative(e,t);return n&&!n.startsWith("..")&&!r.isAbsolute(n)}(t,o))return n.writeHead(403),n.end("Forbidden"),!0;if(!u(o))return!1;if(m(o).isDirectory()){const e=r.join(o,"index.html");return u(e)?(n.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),n.end(yield w(e)),!0):(n.writeHead(404),n.end("Not found"),!0)}var i;return n.writeHead(200,{"Content-Type":(i=o,j.getType(i)||"application/octet-stream")}),n.end(yield w(o)),!0})}function J(e,t,n,o,r,i){return(s,c)=>O(this,void 0,void 0,function*(){const a=new URL(s.url||"/",`http://${s.headers.host||L}`).pathname;if(a===V)return c.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),c.write("retry: 1000\n\n"),i.addClient(c),void s.on("close",()=>{i.removeClient(c)});if("/"===a)return c.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),void c.end((l=n,`<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8" />\n\t<meta name="viewport" content="width=device-width, initial-scale=1" />\n\t<title>React Islands</title>\n</head>\n<body>\n\t<h1>React Islands</h1>\n\t<ul>\n\t\t${l.map(e=>`<li><a href="${M}/${encodeURIComponent(e)}">${e}</a></li>`).join("\n")}\n\t</ul>\n</body>\n</html>`));var l;if(a.startsWith(`${q}/`))return void function(e,t,n){const o=t.get(e);!!o&&(u(o)?(n.writeHead(200,{"Content-Type":"text/css; charset=utf-8"}),w(o).then(e=>n.end(e)).catch(()=>{n.writeHead(500),n.end("Failed to read CSS file")})):(n.writeHead(404),n.end("Not found")))}(a,r,c);if(a.startsWith(`${M}/`)){const t=decodeURIComponent(a.slice(9));return e.islands[t]?(c.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),void c.end(function(e,t){return`<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8" />\n\t<meta name="viewport" content="width=device-width, initial-scale=1" />\n\t<title>${e}</title>\n${function(e){return e.map(e=>`\t<link rel="stylesheet" href="${e}" />`).join("\n")}(t)}\n</head>\n<body>\n\t<div data-island="${e}"></div>\n\t<script src="/common.js"><\/script>\n\t<script src="/${encodeURIComponent(e)}/client.js"><\/script>\n\t<script>\n\t\tconst evtSource = new EventSource('${V}');\n\t\tevtSource.onmessage = () => {\n\t\t\twindow.location.reload();\n\t\t};\n\t<\/script>\n</body>\n</html>`}(t,o))):(c.writeHead(404),void c.end("Island not found"))}(yield z(a,t,c))||(c.writeHead(404),c.end("Not found"))})}!function(e){O(this,void 0,void 0,function*(){const{command:t,help:n=!1,_unknown:o=[]}=y([{name:"command",type:String,defaultOption:!0},{name:"help",alias:"h",type:Boolean,description:"Display this usage guide."}],{stopAtFirstUnknown:!0});if(t&&t in e)return e[t].run([...o,n&&"--help"].filter(Boolean));if(n){const t=Object.keys(e).map(t=>({name:t,summary:e[t].description})),n=g([{header:"Synopsis",content:"npx react-islands <command> <options>"},{header:"Command List",content:t}]);console.log(n)}else console.error(`Command '${t}' not recognized by React Islands.`)})}({build:B,watch:W,serve:new F({description:"Serve the islands on a development server",args:[{name:"config",type:String,alias:"c",description:"The config file to use.",defaultValue:"islands.config.json"}],callback:e=>O(void 0,void 0,void 0,function*(){const{config:t}=e,n=new k,o=P(t),i=process.cwd(),s=r.resolve(i,o.output),c=Object.keys(o.islands),a=function(){const e=new Set;return{notifyReload(){e.forEach(e=>{e.write("data: reload\n\n")})},addClient(t){e.add(t)},removeClient(t){e.delete(t)}}}(),{cssRouteMap:l,cssRoutes:d,cssWatchers:m}=function(e,t,n,o){const i=new Map,s=[],c=new Set;for(const a of t){const t=r.resolve(e,a);if(!u(t)){n.line(`Warning: serve.css file not found: ${a}`);continue}const l=r.relative(e,t).split(r.sep).join("/"),d=`${q}/${encodeURIComponent(l)}`;i.set(d,t),s.push(d);const m=p(t,{persistent:!0},e=>{"change"!==e&&"rename"!==e||o.notifyReload()});c.add(m)}return{cssRouteMap:i,cssRoutes:s,cssWatchers:c}}(i,o.serve.css,n,a),f=o.common?T(o):void 0;f&&(n.watcher("Common Dependencies",f),f.on("event",e=>{"BUNDLE_END"===e.code&&a.notifyReload()}));const h=c.map(e=>{const t=A(Object.assign({name:e,input:o.islands[e]},o));return n.watcher(e,t),t.on("event",e=>{"BUNDLE_END"===e.code&&a.notifyReload()}),t}),v=b(J(o,s,c,d,l,a));v.listen(G,L,()=>{n.line(`Serving islands at http://${L}:3000`);for(const e of c)n.line(`- ${e}: http://${L}:3000${M}/${encodeURIComponent(e)}`)});const y=()=>O(void 0,void 0,void 0,function*(){v.close(),null==f||f.close();for(const e of h)e.close();for(const e of m)e.close()});process.on("SIGINT",y),process.on("SIGTERM",y)})})});
package/dist/index.d.ts CHANGED
@@ -42,6 +42,9 @@ type ConfigOptions = {
42
42
  typescript?: boolean;
43
43
  common?: string[];
44
44
  define?: Record<string, string>;
45
+ serve?: {
46
+ css?: string | string[];
47
+ };
45
48
  };
46
49
 
47
50
  export { Island, isServer, withProps };
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ function RawHTML({ html }) {
26
26
  */
27
27
  function withProps(component, setProps) {
28
28
  return (props) => {
29
- return component({ ...props, ...setProps });
29
+ return component(Object.assign(Object.assign({}, props), setProps));
30
30
  };
31
31
  }
32
32
  /**
@@ -39,14 +39,13 @@ function isServer() {
39
39
  }
40
40
 
41
41
  class Island {
42
- component;
43
- renderOptions;
44
42
  constructor(component, opts = {}) {
43
+ var _a, _b, _c;
45
44
  this.component = component;
46
45
  this.renderOptions = {
47
- shouldHydrate: opts.shouldHydrate ?? true,
48
- multiple: opts.multiple ?? false,
49
- keepChildren: opts.keepChildren ?? false,
46
+ shouldHydrate: (_a = opts.shouldHydrate) !== null && _a !== void 0 ? _a : true,
47
+ multiple: (_b = opts.multiple) !== null && _b !== void 0 ? _b : false,
48
+ keepChildren: (_c = opts.keepChildren) !== null && _c !== void 0 ? _c : false,
50
49
  };
51
50
  }
52
51
  getProps(element) {
@@ -77,8 +76,8 @@ class Island {
77
76
  getRoots(name) {
78
77
  const selector = `[data-island="${name}"]`;
79
78
  const { multiple } = this.renderOptions;
80
- const nodes = [...document.querySelectorAll(selector)];
81
- if (!nodes) {
79
+ const nodes = Array.from(document.querySelectorAll(selector));
80
+ if (!nodes.length) {
82
81
  console.warn(`Could not render React Island because DOM node (${selector}) could not be found.`);
83
82
  return [];
84
83
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@wrdagency/react-islands",
4
- "version": "2.1.5",
4
+ "version": "2.2.0",
5
5
  "description": "",
6
6
  "main": "./dist/index.js",
7
7
  "files": [
@@ -30,6 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@types/command-line-args": "^5.2.3",
32
32
  "@types/command-line-usage": "^5.0.4",
33
+ "@types/node": "^25.9.3",
33
34
  "@types/react": "^19.1.8",
34
35
  "@types/react-dom": "^19.1.6",
35
36
  "typescript": "^5.4.3"
@@ -53,6 +54,7 @@
53
54
  "command-line-usage": "^7.0.3",
54
55
  "rollup": "^4.45.1",
55
56
  "rollup-plugin-dts": "^6.2.1",
57
+ "rollup-plugin-serve": "^3.0.0",
56
58
  "rollup-plugin-typescript-paths": "^1.5.0",
57
59
  "tslib": "^2.8.1",
58
60
  "yocto-spinner": "^1.0.0"
@@ -0,0 +1,370 @@
1
+ import { existsSync, statSync, watch } from "fs";
2
+ import { readFile } from "fs/promises";
3
+ import { createServer, IncomingMessage, ServerResponse } from "http";
4
+ import mime from "mime";
5
+ import path from "path";
6
+ import { watchCommon, watchIsland } from "../rollup";
7
+ import { Command } from "../util/command";
8
+ import { NormalizedConfigOptions, readConfig } from "../util/config";
9
+ import { Output } from "../util/output";
10
+
11
+ const DEFAULT_PORT = 3000;
12
+ const DEFAULT_HOST = "localhost";
13
+ const EVENTS_PATH = "/events";
14
+ const ISLANDS_BASE_PATH = "/islands";
15
+ const SERVE_CSS_BASE_PATH = "/__react-islands-css";
16
+
17
+ function createCssLinks(cssRoutes: string[]): string {
18
+ return cssRoutes
19
+ .map((route) => ` <link rel="stylesheet" href="${route}" />`)
20
+ .join("\n");
21
+ }
22
+
23
+ function createIslandPage(name: string, cssRoutes: string[]): string {
24
+ return `<!doctype html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="utf-8" />
28
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
29
+ <title>${name}</title>
30
+ ${createCssLinks(cssRoutes)}
31
+ </head>
32
+ <body>
33
+ <div data-island="${name}"></div>
34
+ <script src="/common.js"></script>
35
+ <script src="/${encodeURIComponent(name)}/client.js"></script>
36
+ <script>
37
+ const evtSource = new EventSource('${EVENTS_PATH}');
38
+ evtSource.onmessage = () => {
39
+ window.location.reload();
40
+ };
41
+ </script>
42
+ </body>
43
+ </html>`;
44
+ }
45
+
46
+ function createIndexPage(islands: string[]): string {
47
+ const links = islands
48
+ .map(
49
+ (name) =>
50
+ `<li><a href="${ISLANDS_BASE_PATH}/${encodeURIComponent(name)}">${name}</a></li>`,
51
+ )
52
+ .join("\n");
53
+
54
+ return `<!doctype html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="utf-8" />
58
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
59
+ <title>React Islands</title>
60
+ </head>
61
+ <body>
62
+ <h1>React Islands</h1>
63
+ <ul>
64
+ ${links}
65
+ </ul>
66
+ </body>
67
+ </html>`;
68
+ }
69
+
70
+ function getContentType(filePath: string): string {
71
+ return mime.getType(filePath) || "application/octet-stream";
72
+ }
73
+
74
+ function createReloadNotifier() {
75
+ const clients = new Set<ServerResponse>();
76
+
77
+ return {
78
+ notifyReload() {
79
+ clients.forEach((client) => {
80
+ client.write("data: reload\n\n");
81
+ });
82
+ },
83
+
84
+ addClient(res: ServerResponse) {
85
+ clients.add(res);
86
+ },
87
+
88
+ removeClient(res: ServerResponse) {
89
+ clients.delete(res);
90
+ },
91
+ };
92
+ }
93
+
94
+ function watchCssFiles(
95
+ projectRoot: string,
96
+ cssFiles: string[],
97
+ output: Output,
98
+ notifier: ReturnType<typeof createReloadNotifier>,
99
+ ) {
100
+ const cssRouteMap = new Map<string, string>();
101
+ const cssRoutes: string[] = [];
102
+ const cssWatchers = new Set<ReturnType<typeof watch>>();
103
+
104
+ for (const cssFile of cssFiles) {
105
+ const absoluteCssPath = path.resolve(projectRoot, cssFile);
106
+ if (!existsSync(absoluteCssPath)) {
107
+ output.line(`Warning: serve.css file not found: ${cssFile}`);
108
+ continue;
109
+ }
110
+
111
+ const relativePath = path
112
+ .relative(projectRoot, absoluteCssPath)
113
+ .split(path.sep)
114
+ .join("/");
115
+ const cssRoute = `${SERVE_CSS_BASE_PATH}/${encodeURIComponent(relativePath)}`;
116
+
117
+ cssRouteMap.set(cssRoute, absoluteCssPath);
118
+ cssRoutes.push(cssRoute);
119
+
120
+ const watcher = watch(
121
+ absoluteCssPath,
122
+ { persistent: true },
123
+ (eventType) => {
124
+ if (eventType === "change" || eventType === "rename") {
125
+ notifier.notifyReload();
126
+ }
127
+ },
128
+ );
129
+
130
+ cssWatchers.add(watcher);
131
+ }
132
+
133
+ return { cssRouteMap, cssRoutes, cssWatchers };
134
+ }
135
+
136
+ function serveCssAsset(
137
+ pathname: string,
138
+ cssRouteMap: Map<string, string>,
139
+ res: ServerResponse,
140
+ ) {
141
+ const cssFile = cssRouteMap.get(pathname);
142
+ if (!cssFile) {
143
+ return false;
144
+ }
145
+
146
+ if (!existsSync(cssFile)) {
147
+ res.writeHead(404);
148
+ res.end("Not found");
149
+ return true;
150
+ }
151
+
152
+ res.writeHead(200, {
153
+ "Content-Type": "text/css; charset=utf-8",
154
+ });
155
+ readFile(cssFile)
156
+ .then((contents) => res.end(contents))
157
+ .catch(() => {
158
+ res.writeHead(500);
159
+ res.end("Failed to read CSS file");
160
+ });
161
+
162
+ return true;
163
+ }
164
+
165
+ function isSafeAssetPath(distPath: string, assetPath: string) {
166
+ const relative = path.relative(distPath, assetPath);
167
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
168
+ }
169
+
170
+ async function serveStaticAsset(
171
+ pathname: string,
172
+ distPath: string,
173
+ res: ServerResponse,
174
+ ) {
175
+ const assetPath = path.resolve(distPath, `.${pathname}`);
176
+ if (!isSafeAssetPath(distPath, assetPath)) {
177
+ res.writeHead(403);
178
+ res.end("Forbidden");
179
+ return true;
180
+ }
181
+
182
+ if (!existsSync(assetPath)) {
183
+ return false;
184
+ }
185
+
186
+ const fileStat = statSync(assetPath);
187
+ if (fileStat.isDirectory()) {
188
+ const indexFile = path.join(assetPath, "index.html");
189
+ if (!existsSync(indexFile)) {
190
+ res.writeHead(404);
191
+ res.end("Not found");
192
+ return true;
193
+ }
194
+
195
+ res.writeHead(200, {
196
+ "Content-Type": "text/html; charset=utf-8",
197
+ });
198
+ res.end(await readFile(indexFile));
199
+ return true;
200
+ }
201
+
202
+ res.writeHead(200, {
203
+ "Content-Type": getContentType(assetPath),
204
+ });
205
+ res.end(await readFile(assetPath));
206
+ return true;
207
+ }
208
+
209
+ function createRequestHandler(
210
+ config: NormalizedConfigOptions,
211
+ distPath: string,
212
+ islandNames: string[],
213
+ cssRoutes: string[],
214
+ cssRouteMap: Map<string, string>,
215
+ notifier: ReturnType<typeof createReloadNotifier>,
216
+ ) {
217
+ return async (req: IncomingMessage, res: ServerResponse) => {
218
+ const requestUrl = new URL(
219
+ req.url || "/",
220
+ `http://${req.headers.host || DEFAULT_HOST}`,
221
+ );
222
+ const pathname = requestUrl.pathname;
223
+
224
+ if (pathname === EVENTS_PATH) {
225
+ res.writeHead(200, {
226
+ "Content-Type": "text/event-stream",
227
+ "Cache-Control": "no-cache",
228
+ Connection: "keep-alive",
229
+ });
230
+
231
+ res.write("retry: 1000\n\n");
232
+ notifier.addClient(res);
233
+
234
+ req.on("close", () => {
235
+ notifier.removeClient(res);
236
+ });
237
+
238
+ return;
239
+ }
240
+
241
+ if (pathname === "/") {
242
+ res.writeHead(200, {
243
+ "Content-Type": "text/html; charset=utf-8",
244
+ });
245
+ res.end(createIndexPage(islandNames));
246
+ return;
247
+ }
248
+
249
+ if (pathname.startsWith(`${SERVE_CSS_BASE_PATH}/`)) {
250
+ serveCssAsset(pathname, cssRouteMap, res);
251
+ return;
252
+ }
253
+
254
+ if (pathname.startsWith(`${ISLANDS_BASE_PATH}/`)) {
255
+ const islandName = decodeURIComponent(
256
+ pathname.slice(ISLANDS_BASE_PATH.length + 1),
257
+ );
258
+ if (!config.islands[islandName]) {
259
+ res.writeHead(404);
260
+ res.end("Island not found");
261
+ return;
262
+ }
263
+
264
+ res.writeHead(200, {
265
+ "Content-Type": "text/html; charset=utf-8",
266
+ });
267
+ res.end(createIslandPage(islandName, cssRoutes));
268
+ return;
269
+ }
270
+
271
+ const served = await serveStaticAsset(pathname, distPath, res);
272
+ if (served) {
273
+ return;
274
+ }
275
+
276
+ res.writeHead(404);
277
+ res.end("Not found");
278
+ };
279
+ }
280
+
281
+ export default new Command({
282
+ description: "Serve the islands on a development server",
283
+ args: [
284
+ {
285
+ name: "config",
286
+ type: String,
287
+ alias: "c",
288
+ // @ts-ignore
289
+ description: "The config file to use.",
290
+ defaultValue: "islands.config.json",
291
+ },
292
+ ],
293
+ callback: async (args) => {
294
+ const { config: configPath } = args;
295
+
296
+ const output = new Output();
297
+ const config = readConfig(configPath);
298
+ const projectRoot = process.cwd();
299
+ const distPath = path.resolve(projectRoot, config.output);
300
+ const islandNames = Object.keys(config.islands);
301
+ const notifier = createReloadNotifier();
302
+ const { cssRouteMap, cssRoutes, cssWatchers } = watchCssFiles(
303
+ projectRoot,
304
+ config.serve.css,
305
+ output,
306
+ notifier,
307
+ );
308
+
309
+ const commonWatcher = config.common ? watchCommon(config) : undefined;
310
+ if (commonWatcher) {
311
+ output.watcher("Common Dependencies", commonWatcher);
312
+ commonWatcher.on("event", (evt) => {
313
+ if (evt.code === "BUNDLE_END") {
314
+ notifier.notifyReload();
315
+ }
316
+ });
317
+ }
318
+
319
+ const islandWatchers = islandNames.map((name) => {
320
+ const watcher = watchIsland({
321
+ name,
322
+ input: config.islands[name],
323
+ ...config,
324
+ });
325
+
326
+ output.watcher(name, watcher);
327
+ watcher.on("event", (evt) => {
328
+ if (evt.code === "BUNDLE_END") {
329
+ notifier.notifyReload();
330
+ }
331
+ });
332
+
333
+ return watcher;
334
+ });
335
+
336
+ const server = createServer(
337
+ createRequestHandler(
338
+ config,
339
+ distPath,
340
+ islandNames,
341
+ cssRoutes,
342
+ cssRouteMap,
343
+ notifier,
344
+ ),
345
+ );
346
+
347
+ server.listen(DEFAULT_PORT, DEFAULT_HOST, () => {
348
+ output.line(`Serving islands at http://${DEFAULT_HOST}:${DEFAULT_PORT}`);
349
+ for (const name of islandNames) {
350
+ output.line(
351
+ `- ${name}: http://${DEFAULT_HOST}:${DEFAULT_PORT}${ISLANDS_BASE_PATH}/${encodeURIComponent(name)}`,
352
+ );
353
+ }
354
+ });
355
+
356
+ const shutdown = async () => {
357
+ server.close();
358
+ commonWatcher?.close();
359
+ for (const watcher of islandWatchers) {
360
+ watcher.close();
361
+ }
362
+ for (const watcher of cssWatchers) {
363
+ watcher.close();
364
+ }
365
+ };
366
+
367
+ process.on("SIGINT", shutdown);
368
+ process.on("SIGTERM", shutdown);
369
+ },
370
+ });
package/src/bin/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import build from "./commands/build";
3
3
  import watch from "./commands/watch";
4
+ import serve from "./commands/serve";
4
5
  import { commandset } from "./util/command";
5
6
 
6
7
  commandset({
7
8
  build,
8
9
  watch,
10
+ serve,
9
11
  });
@@ -9,6 +9,9 @@ export type ConfigOptions = {
9
9
  typescript?: boolean;
10
10
  common?: string[];
11
11
  define?: Record<string, string>;
12
+ serve?: {
13
+ css?: string | string[];
14
+ };
12
15
  };
13
16
 
14
17
  export type NormalizedConfigOptions = {
@@ -20,6 +23,9 @@ export type NormalizedConfigOptions = {
20
23
  typescript: boolean;
21
24
  common: string[];
22
25
  define: Record<string, string>;
26
+ serve: {
27
+ css: string[];
28
+ };
23
29
  };
24
30
 
25
31
  export type IndividualIslandConfigOptions = Omit<
@@ -38,11 +44,11 @@ export function readConfig(path: string): NormalizedConfigOptions {
38
44
  }
39
45
 
40
46
  export function normalizeBuildOptions(
41
- options: ConfigOptions
47
+ options: ConfigOptions,
42
48
  ): NormalizedConfigOptions {
43
49
  const normalizeBoolean = (
44
50
  value: boolean | undefined | null | void,
45
- fallback: boolean
51
+ fallback: boolean,
46
52
  ): boolean => {
47
53
  return typeof value === "boolean" ? value : fallback;
48
54
  };
@@ -61,5 +67,14 @@ export function normalizeBuildOptions(
61
67
  "@wrdagency/react-islands",
62
68
  ],
63
69
  define: options.define || {},
70
+ serve: {
71
+ css: options.serve
72
+ ? Array.isArray(options.serve.css)
73
+ ? options.serve.css
74
+ : options.serve.css
75
+ ? [options.serve.css]
76
+ : []
77
+ : [],
78
+ },
64
79
  };
65
80
  }
package/src/island.tsx CHANGED
@@ -35,7 +35,7 @@ export class Island {
35
35
 
36
36
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
37
37
  throw new Error(
38
- `Parsed JSON is not a valid dictionary object: '${json}'`
38
+ `Parsed JSON is not a valid dictionary object: '${json}'`,
39
39
  );
40
40
  }
41
41
 
@@ -59,11 +59,11 @@ export class Island {
59
59
  private getRoots(name: string): HTMLElement[] {
60
60
  const selector = `[data-island="${name}"]`;
61
61
  const { multiple } = this.renderOptions;
62
- const nodes = [...document.querySelectorAll(selector)];
62
+ const nodes = Array.from(document.querySelectorAll(selector));
63
63
 
64
- if (!nodes) {
64
+ if (!nodes.length) {
65
65
  console.warn(
66
- `Could not render React Island because DOM node (${selector}) could not be found.`
66
+ `Could not render React Island because DOM node (${selector}) could not be found.`,
67
67
  );
68
68
 
69
69
  return [];
@@ -71,7 +71,7 @@ export class Island {
71
71
 
72
72
  if (nodes.length > 1 && !multiple) {
73
73
  console.warn(
74
- `Multiple elements matched React Island selector (${selector}) but multiple was not enabled. Choosing first element as root.`
74
+ `Multiple elements matched React Island selector (${selector}) but multiple was not enabled. Choosing first element as root.`,
75
75
  );
76
76
 
77
77
  return [nodes[0] as HTMLElement];