@wrdagency/react-islands 2.1.6 → 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 +28 -64
- package/bin/index.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -8
- package/package.json +3 -1
- package/src/bin/commands/serve.ts +370 -0
- package/src/bin/index.ts +2 -0
- package/src/bin/util/config.ts +17 -2
- package/src/island.tsx +5 -5
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
|
|
14
|
-
import { createIsland } from "@wrdagency/react-islands";
|
|
13
|
+
import { Island } from "@wrdagency/react-islands";
|
|
15
14
|
|
|
16
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
41
|
+
Run the package CLI commands from your project root:
|
|
55
42
|
|
|
56
43
|
```
|
|
57
|
-
npx
|
|
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
|
-
|
|
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 r from"@rollup/plugin-terser";import o 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 r="window."+t.slice(0,n+1).join(".");return`${r} = ${r} || {}`}))}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,r="\0virtual-entry";return{name:"rollup-plugin-virtualize-dependency",resolveId:e=>"virtual-entry"===e?r:null,load(e){if(e!==r)return null;const o=new h;o.createGlobalObject(t);for(const e of n){const n=h.packageNameToProperty(e);o.import(e,n),o.setGlobalObjectProperty(t,n,n)}return o.out()}}}async function w(e){const{output:n,...t}=e;let r,o=!1;try{r=await s(t),await r.write(n)}catch(e){o=!0,console.error(e)}return r&&await r.close(),!o}function b(e){const{output:n,...t}=e,r=i({...t,output:n,watch:{clearScreen:!1}});return r.on("event",e=>{"BUNDLE_END"===e.code&&e.result&&e.result.close()}),r}function j(s){const{output:i,minify:a,jsx:c,common:l}=s;return{input:"virtual-entry",jsx:c,output:{name:"Islands._Common",file:o.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&&r()]}}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 r=0;n.on("event",n=>{if("START"===n.code)t.clear(),t.start(),r=Date.now();else if("END"===n.code){const n=Date.now()-r;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 r=e.dir?e.dir:o.dirname(e.file||"");for(const[e,s]of Object.entries(t)){const t=o.resolve(r,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(o,s){const{name:i,input:l,output:u,minify:p,jsx:m,typescript:d,define:f}=o,{external:g=[],subName:h,format:y,globals:w={},prefix:b,suffix:j,plugins:v=[]}=s;return{input:l,external:[...Object.keys(w),...g],jsx:m,output:{name:`Islands.${i}`,globals:w,format:y,entryFileNames:`${i}/${h}`,dir:u,banner:b&&(e=>b(e.name)),footer:j&&(e=>j(e.name))},plugins:[n({extensions:[".cjs",".mjs",".js",".json",".node",".jsx",".ts",".tsx"]}),e(),d&&a({outputToFilesystem:!1,noForceEmit:!0,compilerOptions:{outDir:u,jsx:m}}),d&&c(),t({preventAssignment:!0,values:{...f,"process.env.NODE_ENV":JSON.stringify("production")}}),p&&r(),...v]}}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,{external:["react","react-dom"],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:r}=e;n.push({name:"help",alias:"h",type:Boolean,description:"Display this usage guide."}),this.args=n,this.callback=t,this.description=r}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:r=[]}=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([...r,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 $,r=_(n);r.common&&await t.spinner("Creating common dependencies file",async()=>w(j(r)));for(const[e,n]of Object.entries(r.islands))await t.spinner(`Creating island ${e}`,()=>S({name:e,input:n,...r}))}}),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 $,r=_(n);if(r.common){const e=b(j(r));t.watcher("Common Dependencies",e)}for(const[e,n]of Object.entries(r.islands)){const o=C({name:e,input:n,...r});t.watcher(e,o)}}})});
|
|
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
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({
|
|
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
|
|
48
|
-
multiple: opts.multiple
|
|
49
|
-
keepChildren: opts.keepChildren
|
|
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 =
|
|
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.
|
|
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
package/src/bin/util/config.ts
CHANGED
|
@@ -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 =
|
|
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];
|