${renderedRoot}`,
- );
- this.push(rendered);
+export default async (stream, req, flecks) => {
+ const {
+ appMountId,
+ base,
+ icon,
+ meta,
+ title,
+ } = flecks.get('@flecks/web/server');
+ // Extract assets.
+ const css = [];
+ let inline = '';
+ let isInScript = 0;
+ let isSkipping = false;
+ const js = [];
+ const parserStream = new WritableStream({
+ onclosetag(tagName) {
+ if ('script' === tagName) {
+ isInScript -= 1;
+ isSkipping = false;
}
- catch (error) {
- debug('React SSR failed: %O', error);
- this.push(string);
+ },
+ onopentag(tagName, attribs) {
+ if ('script' === tagName) {
+ isInScript += 1;
+ if ('ignore' === attribs?.['data-flecks']) {
+ isSkipping = true;
+ }
+ else if (attribs.src) {
+ js.push(attribs.src);
+ }
}
- }
- else {
- this.push(string);
- }
- done();
- }
-
-}
-
-export default (stream, req, flecks) => stream.pipe(new Ssr(flecks, req));
+ if ('style' === tagName && attribs['data-href']) {
+ css.push(attribs['data-href']);
+ }
+ if ('link' === tagName && 'stylesheet' === attribs.rel && attribs.href) {
+ css.push(attribs.href);
+ }
+ },
+ ontext(text) {
+ if (isInScript > 0 && !isSkipping) {
+ inline += text;
+ }
+ },
+ });
+ await new Promise((resolve, reject) => {
+ const piped = stream.pipe(parserStream);
+ piped.on('error', reject);
+ piped.on('finish', resolve);
+ });
+ // Render document.
+ const DocumentElement = React.createElement(
+ Document,
+ {
+ appMountId: flecks.interpolate(appMountId),
+ base: flecks.interpolate(base),
+ config: React.createElement(
+ 'script',
+ {dangerouslySetInnerHTML: {__html: await configSource(flecks, req)}},
+ ),
+ css,
+ icon,
+ meta,
+ root: React.createElement(await root(flecks, req)),
+ title: flecks.interpolate(title),
+ },
+ );
+ return new Promise((resolve) => {
+ const rendered = renderToPipeableStream(
+ DocumentElement,
+ {
+ bootstrapScripts: js,
+ bootstrapScriptContent: inline,
+ onError() {
+ resolve(stream);
+ },
+ onShellError() {
+ resolve(stream);
+ },
+ onShellReady() {
+ resolve(rendered);
+ },
+ },
+ );
+ });
+};
diff --git a/packages/web/src/server/build/template.ejs b/packages/web/src/server/build/template.ejs
index ba65a92..38aa13c 100644
--- a/packages/web/src/server/build/template.ejs
+++ b/packages/web/src/server/build/template.ejs
@@ -1,9 +1,6 @@
-
-
-
<%= htmlWebpackPlugin.options.title %>
<%= htmlWebpackPlugin.tags.headTags %>
diff --git a/packages/web/src/server/build/web.webpack.config.js b/packages/web/src/server/build/web.webpack.config.js
index d3fde49..be8b48f 100644
--- a/packages/web/src/server/build/web.webpack.config.js
+++ b/packages/web/src/server/build/web.webpack.config.js
@@ -19,13 +19,16 @@ const {
} = process.env;
module.exports = async (env, argv, flecks) => {
- const {id} = flecks.get('@flecks/core');
const {
appMountId,
+ base,
devHost,
devPort,
devStats,
+ meta,
+ icon,
port,
+ title,
} = flecks.get('@flecks/web/server');
const isProduction = 'production' === argv.mode;
const plugins = [
@@ -73,13 +76,28 @@ module.exports = async (env, argv, flecks) => {
// @todo source maps working?
entry[name] = [entryPoint];
plugins.push(new HtmlWebpackPlugin({
- appMountId,
+ appMountId: flecks.interpolate(appMountId),
+ base: flecks.interpolate(base),
chunks: [name],
filename: `${name}.html`,
inject: false,
lang: 'en',
+ meta,
template: flecks.buildConfig('template.ejs', name),
templateParameters: (compilation, assets, assetTags, options) => {
+ function createTag(tagName, attributes, content) {
+ const tag = HtmlWebpackPlugin.createHtmlTagObject(
+ tagName,
+ attributes,
+ content,
+ {plugin: '@flecks/web/server'},
+ );
+ tag.toString = () => htmlTagObjectToString(tag, false);
+ return tag;
+ }
+ if (icon) {
+ assetTags.headTags.push('link', {rel: 'icon', href: icon});
+ }
if ('index' === name) {
const styleChunks = Array.from(compilation.chunks)
.filter((chunk) => chunk.idNameHints.has('flecksCompiled'));
@@ -93,29 +111,24 @@ module.exports = async (env, argv, flecks) => {
if (asset) {
assetTags.headTags = assetTags.headTags
.filter(({attributes}) => attributes?.href !== styleChunkFiles[j]);
- let tag;
- if (isProduction) {
- tag = HtmlWebpackPlugin.createHtmlTagObject(
- 'style',
- {'data-href': `/${styleChunkFiles[j]}`},
- asset.source(),
- {plugin: '@flecks/web/server'},
- );
- }
- else {
- tag = HtmlWebpackPlugin.createHtmlTagObject(
- 'link',
- {
- href: `/${styleChunkFiles[j]}`,
- rel: 'stylesheet',
- type: 'text/css',
- },
- undefined,
- {plugin: '@flecks/web/server'},
- );
- }
- tag.toString = () => htmlTagObjectToString(tag, false);
- assetTags.headTags.unshift(tag);
+ assetTags.headTags.unshift(
+ createTag(
+ ...isProduction
+ ? [
+ 'style',
+ {'data-href': `/${styleChunkFiles[j]}`},
+ asset.source(),
+ ]
+ : [
+ 'link',
+ {
+ href: `/${styleChunkFiles[j]}`,
+ rel: 'stylesheet',
+ type: 'text/css',
+ },
+ ],
+ ),
+ );
}
}
}
@@ -130,7 +143,7 @@ module.exports = async (env, argv, flecks) => {
},
};
},
- title: id,
+ title: flecks.interpolate(title),
...htmlTemplateConfig,
}));
});
diff --git a/packages/web/src/server/config.js b/packages/web/src/server/config.js
index 615880f..8be8373 100644
--- a/packages/web/src/server/config.js
+++ b/packages/web/src/server/config.js
@@ -50,6 +50,7 @@ class InlineConfig extends Transform {
[
'',
`
',
diff --git a/packages/web/src/server/http.js b/packages/web/src/server/http.js
index b745c8b..70cf611 100644
--- a/packages/web/src/server/http.js
+++ b/packages/web/src/server/http.js
@@ -15,8 +15,8 @@ const {
const debug = D('@flecks/web/server/http');
-const deliverHtmlStream = (stream, flecks, req, res) => {
- flecks.invokeComposed('@flecks/web/server.stream.html', stream, req).pipe(res);
+const deliverHtmlStream = async (stream, flecks, req, res) => {
+ (await flecks.invokeComposedAsync('@flecks/web/server.stream.html', stream, req)).pipe(res);
};
export const createHttpServer = async (flecks) => {
diff --git a/packages/web/src/server/index.js b/packages/web/src/server/index.js
index 26051c0..ee73c5a 100644
--- a/packages/web/src/server/index.js
+++ b/packages/web/src/server/index.js
@@ -14,7 +14,7 @@ const {
const debug = D('@flecks/web/server');
-export {augmentBuild};
+export {augmentBuild, configSource};
export const hooks = {
'@flecks/core.build': augmentBuild,
@@ -122,6 +122,10 @@ export const hooks = {
* The ID of the root element on the page.
*/
appMountId: 'root',
+ /**
+ * Base tag path.
+ */
+ base: '/',
/**
* (webpack-dev-server) Disable the host check.
*
@@ -161,10 +165,21 @@ export const hooks = {
* Host to bind.
*/
host: '0.0.0.0',
+ /**
+ * Path to icon.
+ */
+ icon: '',
/**
* Port to bind.
*/
port: 32340,
+ /**
+ * Meta tags.
+ */
+ meta: {
+ charset: 'utf-8',
+ viewport: 'width=device-width, user-scalable=no',
+ },
/**
* Public path to server.
*/
@@ -176,6 +191,10 @@ export const hooks = {
colors: true,
errorDetails: true,
},
+ /**
+ * HTML title.
+ */
+ title: '[@flecks/core.id]',
/**
* Proxies to trust.
*