diff --git a/examples/basic-without-document/README.md b/examples/basic-without-document/README.md
new file mode 100644
index 000000000..6fef4aa54
--- /dev/null
+++ b/examples/basic-without-document/README.md
@@ -0,0 +1 @@
+# basic-without-document
diff --git a/examples/basic-without-document/build.json b/examples/basic-without-document/build.json
new file mode 100644
index 000000000..5c8a1cb53
--- /dev/null
+++ b/examples/basic-without-document/build.json
@@ -0,0 +1,6 @@
+{
+ "targets": ["web"],
+ "web": {
+ "ssr": true
+ }
+}
diff --git a/examples/basic-without-document/package.json b/examples/basic-without-document/package.json
new file mode 100644
index 000000000..f8957f78b
--- /dev/null
+++ b/examples/basic-without-document/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "with-rax",
+ "description": "rax example",
+ "dependencies": {
+ "rax": "^1.1.0",
+ "rax-document": "^0.1.0",
+ "rax-image": "^2.0.0",
+ "rax-link": "^1.0.1",
+ "rax-text": "^1.0.0",
+ "rax-view": "^1.0.0"
+ },
+ "devDependencies": {
+ "@types/rax": "^1.0.0"
+ },
+ "scripts": {
+ "start": "rax-app start",
+ "build": "rax-app build"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+}
diff --git a/examples/basic-without-document/public/rax.png b/examples/basic-without-document/public/rax.png
new file mode 100644
index 000000000..ffb83fb55
Binary files /dev/null and b/examples/basic-without-document/public/rax.png differ
diff --git a/examples/basic-without-document/src/app.json b/examples/basic-without-document/src/app.json
new file mode 100644
index 000000000..473f653ac
--- /dev/null
+++ b/examples/basic-without-document/src/app.json
@@ -0,0 +1,19 @@
+{
+ "routes": [
+ {
+ "path": "/",
+ "source": "pages/Home/index"
+ },
+ {
+ "path": "/about",
+ "source": "pages/About/index",
+ "window": {
+ "title": "About Page"
+ }
+ }
+ ],
+ "window": {
+ "title": "Rax App"
+ },
+ "metas": [""]
+}
diff --git a/examples/basic-without-document/src/app.ts b/examples/basic-without-document/src/app.ts
new file mode 100644
index 000000000..1023ae2d5
--- /dev/null
+++ b/examples/basic-without-document/src/app.ts
@@ -0,0 +1,4 @@
+import { createElement } from 'rax';
+import { runApp } from 'rax-app';
+
+runApp();
diff --git a/examples/basic-without-document/src/components/Logo/index.css b/examples/basic-without-document/src/components/Logo/index.css
new file mode 100644
index 000000000..2f8d8d114
--- /dev/null
+++ b/examples/basic-without-document/src/components/Logo/index.css
@@ -0,0 +1,5 @@
+.logo {
+ width: 200rpx;
+ height: 180rpx;
+ margin-bottom: 20rpx;
+}
\ No newline at end of file
diff --git a/examples/basic-without-document/src/components/Logo/index.tsx b/examples/basic-without-document/src/components/Logo/index.tsx
new file mode 100644
index 000000000..488be5cb2
--- /dev/null
+++ b/examples/basic-without-document/src/components/Logo/index.tsx
@@ -0,0 +1,21 @@
+import { createElement, PureComponent } from 'rax';
+import Image from 'rax-image';
+
+import './index.css';
+
+class Logo extends PureComponent {
+ render() {
+ const source = {
+ uri: 'https://img.alicdn.com/imgextra/i4/O1CN0145ZaIM1QEObAAbKa1_!!6000000001944-2-tps-1701-1535.png',
+ };
+ console.log('with router =>', this.props);
+ return (
+
+ );
+ }
+}
+
+export default Logo;
diff --git a/examples/basic-without-document/src/pages/About/index.css b/examples/basic-without-document/src/pages/About/index.css
new file mode 100644
index 000000000..a8814796d
--- /dev/null
+++ b/examples/basic-without-document/src/pages/About/index.css
@@ -0,0 +1,16 @@
+.home {
+ align-items: center;
+ margin-top: 200rpx;
+}
+
+.title {
+ font-size: 35rpx;
+ font-weight: bold;
+ margin: 20rpx 0;
+}
+
+.info {
+ font-size: 36rpx;
+ margin: 8rpx 0;
+ color: #555;
+}
diff --git a/examples/basic-without-document/src/pages/About/index.tsx b/examples/basic-without-document/src/pages/About/index.tsx
new file mode 100644
index 000000000..4d21b577a
--- /dev/null
+++ b/examples/basic-without-document/src/pages/About/index.tsx
@@ -0,0 +1,31 @@
+import { createElement, Component } from 'rax';
+import View from 'rax-view';
+import Text from 'rax-text';
+import { getSearchParams } from 'rax-app';
+
+import './index.css';
+
+class About extends Component {
+ componentDidMount() {
+ console.log('about search params', getSearchParams());
+ }
+
+ onShow() {
+ console.log('about show...');
+ }
+
+ onHide() {
+ console.log('about hide...');
+ }
+
+ render() {
+ return (
+
+ About Page
+ (this.props as any).history.push('/')}>Go Home
+
+ );
+ }
+}
+
+export default About;
diff --git a/examples/basic-without-document/src/pages/Home/index.css b/examples/basic-without-document/src/pages/Home/index.css
new file mode 100644
index 000000000..a8814796d
--- /dev/null
+++ b/examples/basic-without-document/src/pages/Home/index.css
@@ -0,0 +1,16 @@
+.home {
+ align-items: center;
+ margin-top: 200rpx;
+}
+
+.title {
+ font-size: 35rpx;
+ font-weight: bold;
+ margin: 20rpx 0;
+}
+
+.info {
+ font-size: 36rpx;
+ margin: 8rpx 0;
+ color: #555;
+}
diff --git a/examples/basic-without-document/src/pages/Home/index.tsx b/examples/basic-without-document/src/pages/Home/index.tsx
new file mode 100644
index 000000000..0e2564774
--- /dev/null
+++ b/examples/basic-without-document/src/pages/Home/index.tsx
@@ -0,0 +1,42 @@
+import { createElement } from 'rax';
+import { usePageShow, usePageHide, getSearchParams } from 'rax-app';
+import View from 'rax-view';
+import Text from 'rax-text';
+import Logo from '@/components/Logo';
+
+import './index.css';
+
+export default function Home(props) {
+ const { history } = props;
+
+ const searchParams = getSearchParams();
+
+ console.log('home search params =>', searchParams);
+ console.log('home page props =>', props);
+
+ usePageShow(() => {
+ console.log('home show...');
+ });
+
+ usePageHide(() => {
+ console.log('home hide...');
+ });
+
+ return (
+
+
+ {props?.data?.title || 'Welcome to Your Rax App'}
+ {props?.data?.info || 'More information about Rax'}
+ history.push('/about', { id: 1 })}>Go About
+
+ );
+}
+
+Home.getInitialProps = async () => {
+ return {
+ data: {
+ title: 'Welcome to Your Rax App with SSR',
+ info: 'More information about Rax',
+ },
+ };
+};
diff --git a/examples/basic-without-document/tsconfig.json b/examples/basic-without-document/tsconfig.json
new file mode 100644
index 000000000..2fad2a09f
--- /dev/null
+++ b/examples/basic-without-document/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compileOnSave": false,
+ "buildOnSave": false,
+ "compilerOptions": {
+ "baseUrl": ".",
+ "outDir": "build",
+ "module": "esnext",
+ "target": "es6",
+ "jsx": "preserve",
+ "jsxFactory": "createElement",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "lib": ["es6", "dom"],
+ "sourceMap": true,
+ "allowJs": true,
+ "rootDir": "./",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": false,
+ "importHelpers": true,
+ "strictNullChecks": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "noUnusedLocals": true,
+ "skipLibCheck": true,
+ "paths": {
+ "@/*": ["./src/*"],
+ "rax-app": [".rax/index.ts"]
+ }
+ },
+ "include": ["src", ".rax"],
+ "exclude": ["node_modules", "build", "public"]
+}
diff --git a/examples/with-rax-store/src/pages/Home/models/counter.ts b/examples/with-rax-store/src/pages/Home/models/counter.ts
index 1ddffb7ce..e1cbc1fb4 100644
--- a/examples/with-rax-store/src/pages/Home/models/counter.ts
+++ b/examples/with-rax-store/src/pages/Home/models/counter.ts
@@ -1,4 +1,4 @@
-import { IRootDispatch } from 'rax-app';
+import { IStoreDispatch } from 'rax-app';
export const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time));
@@ -16,7 +16,7 @@ export default {
},
},
- effects: (dispatch: IRootDispatch) => ({
+ effects: (dispatch: IStoreDispatch) => ({
async decrementAsync() {
await delay(10);
dispatch.counter.decrement();
diff --git a/examples/with-rax-store/tsconfig.json b/examples/with-rax-store/tsconfig.json
index 335d39904..2fad2a09f 100644
--- a/examples/with-rax-store/tsconfig.json
+++ b/examples/with-rax-store/tsconfig.json
@@ -1,20 +1,33 @@
{
+ "compileOnSave": false,
+ "buildOnSave": false,
"compilerOptions": {
- "module": "esNext",
- "target": "es2015",
+ "baseUrl": ".",
"outDir": "build",
- "jsx": "react",
+ "module": "esnext",
+ "target": "es6",
+ "jsx": "preserve",
"jsxFactory": "createElement",
"moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "lib": ["es6", "dom"],
"sourceMap": true,
- "alwaysStrict": true,
- "baseUrl": ".",
+ "allowJs": true,
+ "rootDir": "./",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": false,
+ "importHelpers": true,
+ "strictNullChecks": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "noUnusedLocals": true,
+ "skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
- "rax-app": [".rax/index.ts"],
- "rax": ["node_modules/@types/rax"]
+ "rax-app": [".rax/index.ts"]
}
},
- "include": ["src/*", ".rax"],
- "exclude": ["build"]
+ "include": ["src", ".rax"],
+ "exclude": ["node_modules", "build", "public"]
}
diff --git a/examples/with-rax/src/pages/About/index.css b/examples/with-rax/src/pages/About/index.css
index 85ae2d072..a8814796d 100644
--- a/examples/with-rax/src/pages/About/index.css
+++ b/examples/with-rax/src/pages/About/index.css
@@ -1,10 +1,10 @@
-.about {
+.home {
align-items: center;
margin-top: 200rpx;
}
.title {
- font-size: 45rpx;
+ font-size: 35rpx;
font-weight: bold;
margin: 20rpx 0;
}
diff --git a/packages/error-stack-tracey/index.d.ts b/packages/error-stack-tracey/index.d.ts
index a28678c9d..f8dbf7b34 100644
--- a/packages/error-stack-tracey/index.d.ts
+++ b/packages/error-stack-tracey/index.d.ts
@@ -1,4 +1,4 @@
declare module 'error-stack-tracey' {
- export function parse(error: object, bundleContent: string): object;
+ export function parse(error: object, bundleContent: string): any[];
export function print(message: string, stackFrame: any[]): void;
}
diff --git a/packages/plugin-rax-app/src/base.js b/packages/plugin-rax-app/src/base.js
index 8a2e3c494..f1244dff7 100644
--- a/packages/plugin-rax-app/src/base.js
+++ b/packages/plugin-rax-app/src/base.js
@@ -56,7 +56,7 @@ module.exports = (api, { target, babelConfigOptions, progressOptions = {} }) =>
}
config.plugin('DefinePlugin').tap((args) => [
- Object.assign(...args, {
+ Object.assign({}, ...args, {
'process.env.PUBLIC_URL': JSON.stringify(publicUrl),
}),
]);
diff --git a/packages/plugin-rax-web/package.json b/packages/plugin-rax-web/package.json
index c5289d066..579b3a69b 100644
--- a/packages/plugin-rax-web/package.json
+++ b/packages/plugin-rax-web/package.json
@@ -28,6 +28,7 @@
"@builder/app-helpers": "^2.0.0",
"react-dev-utils": "^10.0.0",
"chalk": "^4.1.0",
- "html-minifier": "^4.0.0"
+ "html-minifier": "^4.0.0",
+ "cheerio": "1.0.0-rc.3"
}
}
diff --git a/packages/plugin-rax-web/src/DocumentPlugin/builtInLoader.ts b/packages/plugin-rax-web/src/DocumentPlugin/builtInLoader.ts
new file mode 100644
index 000000000..aefa34514
--- /dev/null
+++ b/packages/plugin-rax-web/src/DocumentPlugin/builtInLoader.ts
@@ -0,0 +1,40 @@
+import * as qs from 'qs';
+import * as fs from 'fs';
+import { formatPath } from '@builder/app-helpers';
+import { IBuiltInDocumentQuery } from '../types';
+
+/**
+ * loader for wrap document and pages to be server render function, which can render page to html
+ */
+export default function () {
+ const query: IBuiltInDocumentQuery = typeof this.query === 'string' ? qs.parse(this.query.substr(1)) : this.query;
+ const { staticExportPagePath, builtInDocumentTpl } = query;
+
+ const formatedPagePath = staticExportPagePath ? formatPath(staticExportPagePath) : null;
+ const needStaicExport = formatedPagePath && fs.existsSync(formatedPagePath);
+ let source;
+
+ if (needStaicExport) {
+ source = `
+ import { createElement } from 'rax';
+ import renderer from 'rax-server-renderer';
+ import Page from '${formatedPagePath}';
+
+ export function renderInitialHTML(assets) {
+ const contentElement = createElement(Page, {});
+
+ const initialHtml = contentElement ? renderer.renderToString(contentElement, {
+ defaultUnit: 'rpx'
+ }) : '';
+
+ return initialHtml;
+ }
+`;
+ } else {
+ source = `
+ export const renderInitialHTML = () => '';
+ `;
+ }
+ source += `\n export const html = \`${builtInDocumentTpl}\`;`;
+ return source;
+}
diff --git a/packages/plugin-rax-web/src/DocumentPlugin/loader.ts b/packages/plugin-rax-web/src/DocumentPlugin/customLoader.ts
similarity index 63%
rename from packages/plugin-rax-web/src/DocumentPlugin/loader.ts
rename to packages/plugin-rax-web/src/DocumentPlugin/customLoader.ts
index fff9faceb..a3475f214 100644
--- a/packages/plugin-rax-web/src/DocumentPlugin/loader.ts
+++ b/packages/plugin-rax-web/src/DocumentPlugin/customLoader.ts
@@ -1,41 +1,25 @@
import * as qs from 'qs';
-import * as path from 'path';
import * as fs from 'fs';
-
-const isWin = process.platform === 'win32';
-
-/**
- * Transform Windows-style paths, such as 'C:\Windows\system32' to 'C:/Windows/system32'.
- * Because 'C:\Windows\system32' will be escaped to 'C:Windowssystem32'
- * @param {*} p
- */
-const formatPath = (p) => {
- return isWin ? p.split(path.sep).join('/') : p;
-};
+import { ICustomDocumentQuery } from '../types';
/**
* loader for wrap document and pages to be server render function, which can render page to html
*/
export default function () {
- const query = typeof this.query === 'string' ? qs.parse(this.query.substr(1)) : this.query;
- const {
- absoluteDocumentPath,
- absolutePagePath,
- pagePath,
- htmlInfo = {},
- manifests,
- } = query;
+ const query: ICustomDocumentQuery = typeof this.query === 'string' ? qs.parse(this.query.substr(1)) : this.query;
+ const { documentPath, staticExportPagePath, pagePath, htmlInfo = {} } = query;
const { doctype, title } = htmlInfo;
- const formatedPagePath = absolutePagePath ? formatPath(absolutePagePath) : null;
-
- const pageStr = formatedPagePath && fs.existsSync(formatedPagePath) ? `import Page from '${formatedPagePath}';` : 'const Page = null;';
+ const pageStr =
+ staticExportPagePath && fs.existsSync(staticExportPagePath)
+ ? `import Page from '${staticExportPagePath}';`
+ : 'const Page = null;';
const doctypeStr = doctype === null || doctype === '' ? '' : `${doctype || ''}`;
const source = `
import { createElement } from 'rax';
import renderer from 'rax-server-renderer';
- import Document from '${formatPath(absoluteDocumentPath)}';
+ import Document from '${documentPath}';
${pageStr}
function renderToHTML(assets) {
@@ -55,8 +39,7 @@ export default function () {
__initialHtml: initialHtml,
__pagePath: '${pagePath}',
__styles: assets.styles,
- __scripts: assets.scripts,
- __manifests: ${manifests}
+ __scripts: assets.scripts
};
};
diff --git a/packages/plugin-rax-web/src/DocumentPlugin/index.ts b/packages/plugin-rax-web/src/DocumentPlugin/index.ts
index 5e549a396..fb89d214e 100644
--- a/packages/plugin-rax-web/src/DocumentPlugin/index.ts
+++ b/packages/plugin-rax-web/src/DocumentPlugin/index.ts
@@ -1,9 +1,18 @@
import * as qs from 'qs';
import * as path from 'path';
-import * as fs from 'fs';
+import * as fs from 'fs-extra';
import * as webpack from 'webpack';
import * as webpackSources from 'webpack-sources';
import * as errorStackTracey from 'error-stack-tracey';
+import { formatPath } from '@builder/app-helpers';
+import {
+ getBuiltInHtmlTpl,
+ generateHtmlStructure,
+ insertCommonElements,
+ insertLinks,
+ insertScripts,
+} from '../utils/htmlStructure';
+import { IHtmlInfo, IBuiltInDocumentQuery, ICustomDocumentQuery } from '../types';
const { parse, print } = errorStackTracey;
const { RawSource } = webpackSources;
@@ -11,6 +20,7 @@ const PLUGIN_NAME = 'DocumentPlugin';
export default class DocumentPlugin {
options: any;
+ documentPath: string | undefined;
constructor(options) {
/**
* An plugin which generate HTML files
@@ -26,6 +36,10 @@ export default class DocumentPlugin {
* @param {function} [options.configWebpack] custom webpack config for document
*/
this.options = options;
+ const {
+ context: { rootDir },
+ } = options;
+ this.documentPath = getAbsoluteFilePath(rootDir, 'src/document/index');
}
apply(compiler) {
@@ -63,12 +77,8 @@ export default class DocumentPlugin {
}
// Support custom loader
- const loaderForDocument = options.loader || require.resolve('./loader');
-
- // Document path is specified
- const absoluteDocumentPath = getAbsoluteFilePath(rootDir, 'src/document/index');
-
- const manifestString = options.manifests ? JSON.stringify(options.manifests) : null;
+ const loaderForDocument =
+ options.loader || (this.documentPath ? require.resolve('./customLoader') : require.resolve('./builtInLoader'));
delete webpackConfig.entry.index;
// Add ssr loader for each entry
@@ -76,29 +86,41 @@ export default class DocumentPlugin {
const pageInfo = pages[entryName];
const { tempFile, source, pagePath } = pageInfo;
- const absolutePagePath =
+ if (!webpackConfig.entry[tempFile]) {
+ webpackConfig.entry[tempFile] = [];
+ }
+
+ const staticExportPagePath: string =
options.staticExport && source ? getAbsoluteFilePath(rootDir, path.join('src', source)) : '';
const targetPage = source && options.staticConfig.routes.find((route) => route.source === source);
- const htmlInfo = {
+ const htmlInfo: IHtmlInfo = {
...options.htmlInfo,
title: (targetPage && targetPage.window && targetPage.window.title) || options.htmlInfo.title,
};
- const query: any = {
- absoluteDocumentPath,
- absolutePagePath,
- pagePath,
- htmlInfo,
- };
- if (manifestString) {
- query.manifests = manifestString;
+ if (this.documentPath) {
+ const query: ICustomDocumentQuery = {
+ documentPath: this.documentPath,
+ staticExportPagePath,
+ pagePath,
+ htmlInfo,
+ };
+
+ webpackConfig.entry[tempFile].push(`${loaderForDocument}?${qs.stringify(query)}!${this.documentPath}`);
+ } else {
+ const builtInDocumentTpl = getBuiltInHtmlTpl(htmlInfo);
+ const query: IBuiltInDocumentQuery = {
+ staticExportPagePath,
+ builtInDocumentTpl,
+ };
+ // Generate temp entry file
+ const tempEntryPath = path.join(__dirname, 'tempEntry.js');
+ fs.ensureFileSync(tempEntryPath);
+ // Insert elements which define in app.json
+ insertCommonElements(options.staticConfig);
+ webpackConfig.entry[tempFile].push(`${loaderForDocument}?${qs.stringify(query)}!${tempEntryPath}`);
}
-
- if (!webpackConfig.entry[tempFile]) {
- webpackConfig.entry[tempFile] = [];
- }
- webpackConfig.entry[tempFile].push(`${loaderForDocument}?${qs.stringify(query)}!${absoluteDocumentPath}`);
});
let cachedHTML = {};
@@ -153,6 +175,7 @@ export default class DocumentPlugin {
cachedHTML = await generateHtml(compilation, {
pages,
publicPath,
+ existDocument: !!this.documentPath,
});
}
@@ -180,12 +203,23 @@ async function generateHtml(compilation, options) {
const files = compilation.entrypoints.get(entryName).getFiles();
const assets = getAssetsForPage(files, publicPath);
const documentContent = compilation.assets[`${tempFile}.js`].source();
-
let pageSource;
try {
const Document: any = loadDocument(documentContent);
- pageSource = Document.renderToHTML(assets);
+ if (options.existDocument) {
+ const $ = generateHtmlStructure(Document.renderToHTML(assets));
+ pageSource = $.html();
+ } else {
+ const initialHTML = Document.renderInitialHTML();
+ const builtInDocumentTpl = Document.html;
+ insertLinks(assets.styles.map((style) => ``));
+ insertScripts(assets.scripts.map((script) => ``));
+ const $ = generateHtmlStructure(builtInDocumentTpl);
+ const root = $('#root');
+ root.html(initialHTML);
+ pageSource = $.html();
+ }
} catch (error) {
// eslint-disable-next-line no-await-in-loop
const errorStack: any = await parse(error, documentContent);
@@ -268,5 +302,7 @@ function getAbsoluteFilePath(rootDir, filePath) {
return `${path.join(rootDir, filePath)}${ext}`;
});
- return files.find((f) => fs.existsSync(f));
+ const targetFile = files.find((f) => fs.existsSync(f));
+
+ return targetFile ? formatPath(targetFile) : null;
}
diff --git a/packages/plugin-rax-web/src/index.ts b/packages/plugin-rax-web/src/index.ts
index 85c879e8a..4e5c874bf 100644
--- a/packages/plugin-rax-web/src/index.ts
+++ b/packages/plugin-rax-web/src/index.ts
@@ -1,11 +1,12 @@
import * as path from 'path';
import setMPAConfig from '@builder/mpa-config';
+import * as appHelpers from '@builder/app-helpers';
import setDev from './setDev';
import setEntry from './setEntry';
import DocumentPlugin from './DocumentPlugin';
import { GET_RAX_APP_WEBPACK_CONFIG } from './constants';
-import * as appHelpers from '@builder/app-helpers';
import SnapshotPlugin from './SnapshotPlugin';
+import setRegisterMethod from './utils/setRegisterMethod';
const { getMpaEntries } = appHelpers;
export default (api) => {
@@ -33,6 +34,8 @@ export default (api) => {
// Set Entry
setEntry(chainConfig, context);
registerTask(target, chainConfig);
+ // Set global methods
+ setRegisterMethod(api);
if (webConfig.pha) {
// Modify mpa config
diff --git a/packages/plugin-rax-web/src/types.ts b/packages/plugin-rax-web/src/types.ts
new file mode 100644
index 000000000..a001e6664
--- /dev/null
+++ b/packages/plugin-rax-web/src/types.ts
@@ -0,0 +1,16 @@
+export interface IHtmlInfo {
+ title?: string;
+ doctype?: string;
+}
+
+export interface IBuiltInDocumentQuery {
+ staticExportPagePath: string;
+ builtInDocumentTpl: string;
+}
+
+export interface ICustomDocumentQuery {
+ documentPath: string;
+ staticExportPagePath: string;
+ htmlInfo: IHtmlInfo;
+ pagePath: string;
+}
diff --git a/packages/plugin-rax-web/src/utils/htmlStructure.ts b/packages/plugin-rax-web/src/utils/htmlStructure.ts
new file mode 100644
index 000000000..9fc80232d
--- /dev/null
+++ b/packages/plugin-rax-web/src/utils/htmlStructure.ts
@@ -0,0 +1,90 @@
+import * as cheerio from 'cheerio';
+
+let scripts = [];
+let links = [];
+let metas = [];
+
+export function getBuiltInHtmlTpl(htmlInfo) {
+ const { doctype = '', title } = htmlInfo;
+ return `
+ ${doctype}
+
+
+
+
+ ${title}
+
+
+
+
+
+`;
+}
+
+export function insertCommonElements(staticConfig) {
+ const { metas: customMetas, links: customLinks, scripts: customScripts } = staticConfig;
+ if (customMetas) {
+ metas = [...metas, customMetas];
+ }
+ if (customLinks) {
+ links = [...links, customLinks];
+ }
+ if (customScripts) {
+ scripts = [...scripts, customScripts];
+ }
+}
+
+export function generateHtmlStructure(htmlStr) {
+ const $ = cheerio.load(htmlStr);
+ const root = $('#root');
+ const title = $('title');
+ title.before(metas);
+ title.after(links);
+ root.after(scripts);
+ return $;
+}
+
+export function insertScripts(customScripts) {
+ scripts = [...scripts, customScripts];
+}
+
+export function insertLinks(customLinks) {
+ links = [...links, customLinks];
+}
+
+export function insertMetas(customMetas) {
+ metas = [...metas, customMetas];
+}
+
+export function insertScriptsByInfo(customScripts) {
+ insertScripts(
+ customScripts.map((scriptInfo) => {
+ const attrStr = Object.keys(scriptInfo).reduce((curr, next) => `${curr} ${next}=${scriptInfo[next]} `, '');
+ return ``;
+ }),
+ );
+}
+
+export function injectHTML(tagName, value) {
+ switch (tagName) {
+ case 'script':
+ insertScripts(value);
+ break;
+ case 'link':
+ insertLinks(value);
+ break;
+ case 'meta':
+ insertMetas(value);
+ break;
+ default:
+ throw new Error(`Not support inject ${tagName}`);
+ }
+}
+
+export function getInjectedHTML() {
+ return {
+ scripts: [...scripts],
+ links: [...links],
+ metas: [...metas],
+ };
+}
diff --git a/packages/plugin-rax-web/src/utils/setRegisterMethod.ts b/packages/plugin-rax-web/src/utils/setRegisterMethod.ts
new file mode 100644
index 000000000..d2c26a2a1
--- /dev/null
+++ b/packages/plugin-rax-web/src/utils/setRegisterMethod.ts
@@ -0,0 +1,8 @@
+import { injectHTML, insertScriptsByInfo, getInjectedHTML } from './htmlStructure';
+
+export default (api) => {
+ const { registerMethod } = api;
+ registerMethod('injectHTML', injectHTML);
+ registerMethod('insertScriptsByInfo', insertScriptsByInfo);
+ registerMethod('getInjectedHTML', getInjectedHTML);
+};
diff --git a/packages/plugin-ssr/package.json b/packages/plugin-ssr/package.json
index bc083e4d9..a8023d3b2 100644
--- a/packages/plugin-ssr/package.json
+++ b/packages/plugin-ssr/package.json
@@ -30,6 +30,7 @@
"error-stack-tracey": "^0.1.3",
"qs": "^6.9.4",
"@builder/app-helpers": "^2.0.0",
- "react-dev-utils": "^10.0.0"
+ "react-dev-utils": "^10.0.0",
+ "cheerio": "1.0.0-rc.3"
}
}
diff --git a/packages/plugin-ssr/src/constants.ts b/packages/plugin-ssr/src/constants.ts
new file mode 100644
index 000000000..ec59c40e6
--- /dev/null
+++ b/packages/plugin-ssr/src/constants.ts
@@ -0,0 +1,3 @@
+export const GET_RAX_APP_WEBPACK_CONFIG = 'getRaxAppWebpackConfig';
+export const NODE = 'node';
+export const STATIC_CONFIG = 'staticConfig';
diff --git a/packages/plugin-ssr/src/index.ts b/packages/plugin-ssr/src/index.ts
index cc480fa1c..85895fdd8 100644
--- a/packages/plugin-ssr/src/index.ts
+++ b/packages/plugin-ssr/src/index.ts
@@ -1,12 +1,12 @@
-const path = require('path');
-const chalk = require('chalk');
-const getSSRBase = require('./ssr/getBase');
-const setSSRBuild = require('./ssr/setBuild');
-const setSSRDev = require('./ssr/setDev');
-const setWebDev = require('./web/setDev');
+import setWebDev from './web/setDev';
+import * as path from 'path';
+import chalk from 'chalk';
+import getSSRBase from './ssr/getBase';
+import setSSRBuild from './ssr/setBuild';
+import setSSRDev from './ssr/setDev';
// can‘t clone webpack chain object
-module.exports = (api) => {
+export default (api) => {
const { onGetWebpackConfig, registerTask, context, onHook } = api;
const { command, rootDir, userConfig = {} } = context;
const { outputDir } = userConfig;
@@ -19,7 +19,7 @@ module.exports = (api) => {
if (isDev) {
onGetWebpackConfig('web', (config) => {
config.optimization.splitChunks({ cacheGroups: {} });
- setWebDev(config, context);
+ setWebDev(config);
});
}
@@ -36,9 +36,9 @@ module.exports = (api) => {
.libraryTarget('commonjs2');
if (isDev) {
- setSSRDev(config, context);
+ setSSRDev(config, api);
} else {
- setSSRBuild(config, context);
+ setSSRBuild(config);
}
config
diff --git a/packages/plugin-ssr/src/ssr/entryLoader.js b/packages/plugin-ssr/src/ssr/entryLoader.js
deleted file mode 100644
index 6ac11b530..000000000
--- a/packages/plugin-ssr/src/ssr/entryLoader.js
+++ /dev/null
@@ -1,131 +0,0 @@
-const qs = require('qs');
-const path = require('path');
-
-const isWin = process.platform === 'win32';
-
-/**
- * Transform Windows-style paths, such as 'C:\Windows\system32' to 'C:/Windows/system32'.
- * Because 'C:\Windows\system32' will be escaped to 'C:Windowssystem32'
- * @param {*} p
- */
-const formatPath = (p) => {
- return isWin ? p.split(path.sep).join('/') : p;
-};
-
-module.exports = function () {
- const query = typeof this.query === 'string' ? qs.parse(this.query.substr(1)) : this.query;
- const {
- absoluteDocumentPath,
- absoluteAppPath,
- absoluteAppConfigPath,
- absolutePagePath = formatPath(this.resourcePath),
- styles = [],
- scripts = [],
- assetsProcessor,
- } = query;
-
- const renderHtmlFnc = `
- async function renderComponentToHTML(Component, ctx) {
- const pageData = await getInitialProps(Component, ctx);
- const initialData = appConfig.app && appConfig.app.getInitialData ? await appConfig.app.getInitialData() : {};
-
- const data = {
- __SSR_ENABLED__: true,
- initialData,
- pageData,
- };
-
- const contentElement = createElement(Component, pageData);
-
- const initialHtml = renderer.renderToString(contentElement, {
- defaultUnit: 'rpx'
- });
- // use let statement, because styles and scripts may be changed by assetsProcessor
- let styles = ${JSON.stringify(styles)};
- let scripts = ${JSON.stringify(scripts)};
-
- // process public path for different runtime env
- ${assetsProcessor || ''}
-
- // This loader is executed after babel, so need to be tansformed to ES5.
- const DocumentContextProvider = function() {};
- DocumentContextProvider.prototype.getChildContext = function() {
- return {
- __initialHtml: initialHtml,
- __initialData: JSON.stringify(data),
- __styles: styles,
- __scripts: scripts,
- };
- };
- DocumentContextProvider.prototype.render = function() {
- return createElement(Document, initialData);
- };
-
- const DocumentContextProviderElement = createElement(DocumentContextProvider);
-
- const html = '' + renderer.renderToString(DocumentContextProviderElement);
-
- return html;
- }
- `;
-
- const source = `
- import { createElement } from 'rax';
- import renderer from 'rax-server-renderer';
-
- import '${formatPath(absoluteAppPath)}';
- import { getAppConfig } from '${formatPath(absoluteAppConfigPath)}'
- import Page from '${formatPath(absolutePagePath)}';
- import Document from '${formatPath(absoluteDocumentPath)}';
-
- const appConfig = getAppConfig() || {};
-
- ${renderHtmlFnc}
-
- async function render(req, res) {
- const html = await renderToHTML(req, res);
-
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
- res.send(html);
- }
-
- async function renderToHTML(req, res) {
- const html = await renderComponentToHTML(Page, {
- req,
- res
- });
- return html;
- }
-
- // Handler for Midway FaaS and Koa
- async function renderWithContext(ctx) {
- const html = await renderComponentToHTML(Page, ctx);
-
- ctx.set('Content-Type', 'text/html; charset=utf-8');
- ctx.body = html;
- }
-
- export {
- render,
- renderToHTML,
- renderWithContext
- };
-
- export default render;
-
- async function getInitialProps(Component, ctx) {
- if (!Component.getInitialProps) return null;
-
- const props = await Component.getInitialProps(ctx);
-
- if (!props || typeof props !== 'object') {
- const message = '"getInitialProps()" should resolve to an object. But found "' + props + '" instead.';
- throw new Error(message);
- }
-
- return props;
- }
- `;
-
- return source;
-};
diff --git a/packages/plugin-ssr/src/ssr/entryPlugin.js b/packages/plugin-ssr/src/ssr/entryPlugin.js
deleted file mode 100644
index fb6c6199d..000000000
--- a/packages/plugin-ssr/src/ssr/entryPlugin.js
+++ /dev/null
@@ -1,67 +0,0 @@
-const qs = require('qs');
-const path = require('path');
-
-/**
- * An entry plugin which will set loader for entry before compile.
- *
- * Entry Loader for SSR need `publicPath` for assets path.
- * `publicPath` may be changed by other plugin after SSR plugin.
- * So the real `publicPath` can only get after all plugins have registed.
- */
-class EntryPlugin {
- constructor(options) {
- this.options = options;
- }
-
- /**
- * @param {Compiler} compiler the compiler instance
- * @returns {void}
- */
- apply(compiler) {
- const {
- loader,
- entries,
- isMultiPages,
- isInlineStyle,
- absoluteDocumentPath,
- absoluteAppConfigPath,
- absoluteAppPath,
- assetsProcessor,
- } = this.options;
-
- const { publicPath } = compiler.options.output;
-
- const entryConfig = {};
-
- entries.forEach((entry) => {
- const {
- name,
- sourcePath,
- } = entry;
-
- let absolutePagePath;
- const appRegexp = /^app\.(t|j)sx?$/;
- const entryBasename = path.basename(sourcePath);
- const entryDirname = path.dirname(sourcePath);
- if (appRegexp.test(entryBasename)) {
- absolutePagePath = path.join(entryDirname, 'index.tsx');
- }
-
- const query = {
- styles: isMultiPages && !isInlineStyle ? [`${publicPath}${name}.css`] : [],
- scripts: isMultiPages ? [`${publicPath}${name}.js`] : [`${publicPath}index.js`],
- absoluteDocumentPath,
- absoluteAppPath,
- absoluteAppConfigPath,
- absolutePagePath,
- assetsProcessor,
- };
-
- entryConfig[name] = `${loader}?${qs.stringify(query)}!${sourcePath}`;
- });
-
- compiler.options.entry = entryConfig;
- }
-}
-
-module.exports = EntryPlugin;
diff --git a/packages/plugin-ssr/src/ssr/entryPlugin.ts b/packages/plugin-ssr/src/ssr/entryPlugin.ts
new file mode 100644
index 000000000..735d5b02e
--- /dev/null
+++ b/packages/plugin-ssr/src/ssr/entryPlugin.ts
@@ -0,0 +1,81 @@
+import * as qs from 'qs';
+import * as path from 'path';
+import * as fs from 'fs-extra';
+import { formatPath } from '@builder/app-helpers';
+import { IEntryLoaderQuery } from '../types';
+import { STATIC_CONFIG } from '../constants';
+import getBuiltInHtmlTpl from './getBuiltInHTML';
+
+const TEMP_PATH = 'TEMP_PATH';
+
+/**
+ * An entry plugin which will set loader for entry before compile.
+ *
+ * Entry Loader for SSR need `publicPath` for assets path.
+ * `publicPath` may be changed by other plugin after SSR plugin.
+ * So the real `publicPath` can only get after all plugins have registed.
+ */
+export default class EntryPlugin {
+ options: any;
+ constructor(options) {
+ this.options = options;
+ }
+
+ /**
+ * @param {Compiler} compiler the compiler instance
+ * @returns {void}
+ */
+ apply(compiler) {
+ const { entries, api, assetsProcessor } = this.options;
+ const { context, getValue, applyMethod } = api;
+ const { userConfig, rootDir } = context;
+ const { web: webConfig, inlineStyle } = userConfig;
+ const staticConfig = getValue(STATIC_CONFIG);
+ const globalTitle = staticConfig.window && staticConfig.window.title;
+ const tempPath = getValue(TEMP_PATH);
+ const documentPath = getAbsolutePath(path.join(rootDir, 'src/document/index'));
+ const absoluteAppConfigPath = getAbsolutePath(path.join(tempPath, 'appConfig.ts'));
+ const { publicPath } = compiler.options.output;
+ const EntryLoader = documentPath
+ ? require.resolve('./loaders/customDocumentLoader')
+ : require.resolve('./loaders/builtInHTMLLoader');
+
+ const entryConfig = {};
+
+ entries.forEach((entry) => {
+ const { name, entryPath, source } = entry;
+
+ const query: IEntryLoaderQuery = {
+ styles: webConfig.mpa && !inlineStyle ? [`${publicPath}${name}.css`] : [],
+ scripts: webConfig.mpa ? [`${publicPath}${name}.js`] : [`${publicPath}index.js`],
+ absoluteAppConfigPath,
+ entryPath,
+ assetsProcessor,
+ };
+
+ if (documentPath) {
+ query.documentPath = documentPath;
+ } else {
+ const targetRoute = staticConfig.routes.find((route) => route.source === source);
+ const htmlInfo = {
+ doctype: webConfig.doctype,
+ title: targetRoute?.window?.title || globalTitle,
+ };
+ query.builtInHTML = getBuiltInHtmlTpl(htmlInfo);
+ query.styles = query.styles.map((style) => ``);
+ query.scripts = query.scripts.map((script) => ``);
+ query.injectedHTML = applyMethod('getInjectedHTML');
+ }
+
+ entryConfig[name] = `${EntryLoader}?${qs.stringify(query)}!${entryPath}`;
+ });
+
+ compiler.options.entry = entryConfig;
+ }
+}
+
+function getAbsolutePath(targetPath) {
+ const targetExt = ['', '.tsx', '.jsx'].find((ext) => fs.existsSync(`${targetPath}${ext}`));
+ if (targetExt === undefined) return;
+ return formatPath(`${targetPath}${targetExt}`);
+}
diff --git a/packages/plugin-ssr/src/ssr/getBase.js b/packages/plugin-ssr/src/ssr/getBase.js
deleted file mode 100644
index c24139173..000000000
--- a/packages/plugin-ssr/src/ssr/getBase.js
+++ /dev/null
@@ -1,75 +0,0 @@
-const path = require('path');
-const setMPAConfig = require('@builder/mpa-config');
-const fs = require('fs-extra');
-const { getMpaEntries } = require('@builder/app-helpers');
-const getEntryName = require('./getEntryName');
-const EntryPlugin = require('./entryPlugin');
-
-const EntryLoader = require.resolve('./entryLoader');
-
-const GET_RAX_APP_WEBPACK_CONFIG = 'getRaxAppWebpackConfig';
-const TARGET = 'node';
-
-// Can‘t clone webpack chain object, so generate a new chain and reset config
-module.exports = (api) => {
- const { context, getValue } = api;
- const { userConfig, rootDir } = context;
- const { web: webConfig = {}, inlineStyle = false } = userConfig;
-
- const getWebpackBase = getValue(GET_RAX_APP_WEBPACK_CONFIG);
-
- const config = getWebpackBase(api, {
- target: TARGET,
- babelConfigOptions: { styleSheet: true },
- progressOptions: {
- name: 'SSR',
- },
- });
-
- config.name('node');
-
- const appJsonPath = path.resolve(rootDir, 'src/app.json');
-
- let entries = {};
- if (webConfig.mpa) {
- setMPAConfig.default(api, config, { type: TARGET, entries: getMpaEntries(api, { target: 'web', appJsonPath }) });
- const mpaEntries = config.toConfig().entry;
- entries = Object.keys(mpaEntries).map((entryName) => {
- return {
- name: entryName,
- sourcePath: mpaEntries[entryName][0],
- };
- });
- } else {
- // eslint-disable-next-line
- const appJSON = require(appJsonPath);
- entries = appJSON.routes.map((route) => {
- return {
- name: getEntryName(route.path),
- sourcePath: path.join(rootDir, 'src', route.source),
- };
- });
- }
-
- config.plugin('entryPlugin')
- .use(EntryPlugin, [{
- entries,
- loader: EntryLoader,
- isMultiPages: webConfig.mpa || false,
- isInlineStyle: inlineStyle,
- absoluteDocumentPath: moduleResolve(path.join(rootDir, 'src/document/index')),
- absoluteAppPath: moduleResolve(path.join(rootDir, 'src/app')),
- absoluteAppConfigPath: path.join(rootDir, '.rax', 'appConfig.ts'),
- }]);
-
- return config;
-};
-
-
-function moduleResolve(filePath) {
- const ext = ['.ts', '.js', '.tsx', '.jsx'].find((extension) => fs.existsSync(`${filePath}${extension}`));
- if (!ext) {
- throw new Error(`Cannot find target file ${filePath}.`);
- }
- return require.resolve(`${filePath}${ext}`);
-}
diff --git a/packages/plugin-ssr/src/ssr/getBase.ts b/packages/plugin-ssr/src/ssr/getBase.ts
new file mode 100644
index 000000000..5879d1f8b
--- /dev/null
+++ b/packages/plugin-ssr/src/ssr/getBase.ts
@@ -0,0 +1,56 @@
+import * as path from 'path';
+import setMPAConfig from '@builder/mpa-config';
+import { formatPath, getMpaEntries } from '@builder/app-helpers';
+import getEntryName from './getEntryName';
+import EntryPlugin from './entryPlugin';
+import { GET_RAX_APP_WEBPACK_CONFIG, NODE, STATIC_CONFIG } from '../constants';
+
+// Can‘t clone webpack chain object, so generate a new chain and reset config
+export default (api) => {
+ const { context, getValue } = api;
+ const { userConfig, rootDir } = context;
+ const { web: webConfig = {} } = userConfig;
+
+ const getWebpackBase = getValue(GET_RAX_APP_WEBPACK_CONFIG);
+
+ const config = getWebpackBase(api, {
+ target: NODE,
+ babelConfigOptions: { styleSheet: true },
+ progressOptions: {
+ name: 'SSR',
+ },
+ });
+
+ config.name('node');
+ const staticConfig = getValue(STATIC_CONFIG);
+
+ let entries = {};
+ if (webConfig.mpa) {
+ const mpaEntries = getMpaEntries(api, { target: 'web', appJsonContent: staticConfig });
+ setMPAConfig(api, config, { type: NODE, entries: mpaEntries });
+ entries = mpaEntries.map(({ entryName, entryPath, source }) => {
+ return {
+ name: entryName,
+ entryPath: formatPath(path.join(rootDir, 'src', entryPath)),
+ source,
+ };
+ });
+ } else {
+ entries = staticConfig.routes.map((route) => {
+ return {
+ name: getEntryName(route.path),
+ entryPath: formatPath(path.join(rootDir, 'src', route.source)),
+ source: route.source,
+ };
+ });
+ }
+
+ config.plugin('entryPlugin').use(EntryPlugin, [
+ {
+ entries,
+ api,
+ },
+ ]);
+
+ return config;
+};
diff --git a/packages/plugin-ssr/src/ssr/getBuiltInHTML.ts b/packages/plugin-ssr/src/ssr/getBuiltInHTML.ts
new file mode 100644
index 000000000..087181b10
--- /dev/null
+++ b/packages/plugin-ssr/src/ssr/getBuiltInHTML.ts
@@ -0,0 +1,16 @@
+export default function getBuiltInHtmlTpl(htmlInfo) {
+ const { doctype = '', title } = htmlInfo;
+ return `
+ ${doctype}
+
+
+
+
+ ${title}
+
+
+
+
+
+`;
+}
diff --git a/packages/plugin-ssr/src/ssr/getEntryName.js b/packages/plugin-ssr/src/ssr/getEntryName.ts
similarity index 88%
rename from packages/plugin-ssr/src/ssr/getEntryName.js
rename to packages/plugin-ssr/src/ssr/getEntryName.ts
index 309cefc33..c13853f4c 100644
--- a/packages/plugin-ssr/src/ssr/getEntryName.js
+++ b/packages/plugin-ssr/src/ssr/getEntryName.ts
@@ -2,7 +2,7 @@
* Generate entryname by route.path
* Example: '/about/' -> 'about/index'
*/
-module.exports = (path) => {
+export default (path) => {
let entryName = 'index';
if (path && path !== '/') {
diff --git a/packages/plugin-ssr/src/ssr/getMpaRoutes.js b/packages/plugin-ssr/src/ssr/getMpaRoutes.js
deleted file mode 100644
index 8a92eee16..000000000
--- a/packages/plugin-ssr/src/ssr/getMpaRoutes.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const path = require('path');
-const getEntryName = require('./getEntryName');
-
-const appRegexp = /^app\.(t|j)sx?$/;
-
-function getMpaRoutes(config) {
- const routes = [];
- const distDir = config.output.get('path');
- const filename = config.output.get('filename');
- const mpaEntries = config.toConfig().entry;
-
- Object.keys(mpaEntries).forEach((entryName) => {
- const entryBasename = path.basename(mpaEntries[entryName][0]);
- const entryDirname = path.dirname(mpaEntries[entryName][0]);
- if (appRegexp.test(entryBasename)) {
- // eslint-disable-next-line
- const staticConfig = require(`${entryDirname}/app.json`);
- staticConfig.routes.forEach((route) => {
- routes.push({
- path: `/${entryName}.html`,
- source: route.source,
- entryName: getEntryName(route.path),
- componentPath: path.join(distDir, filename.replace('[name]', entryName)),
- });
- });
- } else {
- const entryPath = mpaEntries[entryName][0];
- const pageName = path.dirname(entryPath).split('/')[0];
- routes.push({
- path: `/${entryName}.html`,
- source: `/pages/${pageName}/index`,
- entryName,
- componentPath: path.join(distDir, filename.replace('[name]', entryName)),
- });
- }
- });
-
- return routes;
-}
-
-module.exports = getMpaRoutes;
diff --git a/packages/plugin-ssr/src/ssr/loaders/EntryLoader.ts b/packages/plugin-ssr/src/ssr/loaders/EntryLoader.ts
new file mode 100644
index 000000000..6ccbb87f4
--- /dev/null
+++ b/packages/plugin-ssr/src/ssr/loaders/EntryLoader.ts
@@ -0,0 +1,97 @@
+import { formatPath } from '@builder/app-helpers';
+
+export default class {
+ source = '';
+ absoluteAppConfigPath: string;
+ entryPath: string;
+ constructor({ absoluteAppConfigPath, entryPath }) {
+ this.absoluteAppConfigPath = absoluteAppConfigPath;
+ this.entryPath = entryPath;
+ }
+
+ addInitImport() {
+ this.source += `
+ import { createElement } from 'rax';
+ import renderer from 'rax-server-renderer';
+
+ import { getAppConfig } from '${this.absoluteAppConfigPath}'
+ import Page from '${this.entryPath}';
+ `;
+ return this;
+ }
+
+ addVariableDeclaration() {
+ this.source += `
+ const appConfig = getAppConfig() || {};
+ `;
+ return this;
+ }
+
+ addRender() {
+ this.source += `
+ async function render(req, res) {
+ const html = await renderToHTML(req, res);
+
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.send(html);
+ }`;
+ return this;
+ }
+ addRenderToHTML() {
+ this.source += `
+ async function renderToHTML(req, res) {
+ const html = await renderComponentToHTML(Page, {
+ req,
+ res
+ });
+ return html;
+ }`;
+ return this;
+ }
+
+ addRenderWithContext() {
+ this.source += `
+ // Handler for Midway FaaS and Koa
+ async function renderWithContext(ctx) {
+ const html = await renderComponentToHTML(Page, ctx);
+
+ ctx.set('Content-Type', 'text/html; charset=utf-8');
+ ctx.body = html;
+ }`;
+ return this;
+ }
+
+ addExport() {
+ this.source += `
+ export {
+ render,
+ renderToHTML,
+ renderWithContext
+ };
+
+ export default render;
+ `;
+ return this;
+ }
+
+ addGetInitialProps() {
+ this.source += `
+ async function getInitialProps(Component, ctx) {
+ if (!Component.getInitialProps) return null;
+
+ const props = await Component.getInitialProps(ctx);
+
+ if (!props || typeof props !== 'object') {
+ const message = '"getInitialProps()" should resolve to an object. But found "' + props + '" instead.';
+ throw new Error(message);
+ }
+
+ return props;
+ }`;
+ return this;
+ }
+
+ getSource() {
+ return this.source;
+ }
+}
diff --git a/packages/plugin-ssr/src/ssr/loaders/builtInHTMLLoader.ts b/packages/plugin-ssr/src/ssr/loaders/builtInHTMLLoader.ts
new file mode 100644
index 000000000..04b0bdd26
--- /dev/null
+++ b/packages/plugin-ssr/src/ssr/loaders/builtInHTMLLoader.ts
@@ -0,0 +1,107 @@
+import * as qs from 'qs';
+import { formatPath } from '@builder/app-helpers';
+import EntryLoader from './EntryLoader';
+import { IInjectedHTML } from '../../types';
+
+class BuiltInHTMLLoader extends EntryLoader {
+ injectedHTML: IInjectedHTML;
+ styles: string[];
+ scripts: string[];
+ assetsProcessor: string;
+ builtInHTML: string;
+ constructor(options) {
+ super(options);
+ this.injectedHTML = options.injectedHTML;
+ this.styles = options.styles || [];
+ this.scripts = options.scripts;
+ this.assetsProcessor = options.assetsProcessor;
+ this.builtInHTML = options.builtInHTML;
+ }
+ addInitImport() {
+ super.addInitImport();
+ this.source += `
+ import * as cheerio from 'cheerio';
+ `;
+ return this;
+ }
+ addVariableDeclaration() {
+ super.addVariableDeclaration();
+ this.source += `
+ let builtInMetas = ${JSON.stringify(this.injectedHTML.metas)} || [];
+ let builtInLinks = ${JSON.stringify(this.injectedHTML.links)} || [];
+ let builtInScripts = ${JSON.stringify(this.injectedHTML.scripts)} || [];
+ `;
+ return this;
+ }
+ addGenerateHtml() {
+ this.source += `
+ function generateHtml(htmlStr, initialHtml) {
+ const $ = cheerio.load(htmlStr);
+ const root = $('#root');
+ const title = $('title');
+ title.before(builtInMetas);
+ title.after(builtInLinks);
+ root.after(builtInScripts);
+ root.html(initialHtml)
+ return $.html();
+ }
+ `;
+ return this;
+ }
+ addRenderComponentToHTML() {
+ this.source += `
+ async function renderComponentToHTML(Component, ctx) {
+ const pageData = await getInitialProps(Component, ctx);
+ const initialData = appConfig.app && appConfig.app.getInitialData ? await appConfig.app.getInitialData() : {};
+
+ const data = {
+ __SSR_ENABLED__: true,
+ initialData,
+ pageData,
+ };
+
+ builtInScripts.push('')
+
+ const contentElement = createElement(Component, pageData);
+
+ const initialHtml = renderer.renderToString(contentElement, {
+ defaultUnit: 'rpx'
+ });
+ // use let statement, because styles and scripts may be changed by assetsProcessor
+ let styles = ${JSON.stringify(this.styles)};
+ let scripts = ${JSON.stringify(this.scripts)};
+
+ // process public path for different runtime env
+ ${this.assetsProcessor || ''}
+
+ builtInLinks = [...builtInLinks, ...styles];
+ builtInScripts = [...builtInScripts, ...scripts];
+
+ return generateHtml(\`${this.builtInHTML}\`, initialHtml);
+ }
+ `;
+ return this;
+ }
+}
+
+export default function () {
+ const query = typeof this.query === 'string' ? qs.parse(this.query.substr(1)) : this.query;
+
+ if (!query.entryPath) {
+ query.entryPath = formatPath(this.resourcePath);
+ }
+
+ const builtInHTMLLoader = new BuiltInHTMLLoader(query);
+
+ return builtInHTMLLoader
+ .addInitImport()
+ .addVariableDeclaration()
+ .addGenerateHtml()
+ .addGetInitialProps()
+ .addRenderComponentToHTML()
+ .addRenderToHTML()
+ .addRender()
+ .addRenderWithContext()
+ .addExport()
+ .getSource();
+}
diff --git a/packages/plugin-ssr/src/ssr/loaders/customDocumentLoader.ts b/packages/plugin-ssr/src/ssr/loaders/customDocumentLoader.ts
new file mode 100644
index 000000000..bcfdcc4a5
--- /dev/null
+++ b/packages/plugin-ssr/src/ssr/loaders/customDocumentLoader.ts
@@ -0,0 +1,91 @@
+import * as qs from 'qs';
+import { formatPath } from '@builder/app-helpers';
+import EntryLoader from './EntryLoader';
+
+class CustomDocumentLoader extends EntryLoader {
+ documentPath: string;
+ styles: string[];
+ scripts: string[];
+ assetsProcessor: string;
+ constructor(options) {
+ super(options);
+ this.documentPath = options.documentPath;
+ this.styles = options.styles || [];
+ this.scripts = options.scripts;
+ this.assetsProcessor = options.assetsProcessor;
+ }
+ addInitImport() {
+ super.addInitImport();
+ this.source += `
+ import Document from '${this.documentPath}';
+ `;
+ return this;
+ }
+ addRenderComponentToHTML() {
+ this.source += `
+ async function renderComponentToHTML(Component, ctx) {
+ const pageData = await getInitialProps(Component, ctx);
+ const initialData = appConfig.app && appConfig.app.getInitialData ? await appConfig.app.getInitialData() : {};
+
+ const data = {
+ __SSR_ENABLED__: true,
+ initialData,
+ pageData,
+ };
+
+ const contentElement = createElement(Component, pageData);
+
+ const initialHtml = renderer.renderToString(contentElement, {
+ defaultUnit: 'rpx'
+ });
+ // use let statement, because styles and scripts may be changed by assetsProcessor
+ let styles = ${JSON.stringify(this.styles)};
+ let scripts = ${JSON.stringify(this.scripts)};
+
+ // process public path for different runtime env
+ ${this.assetsProcessor || ''}
+
+ // This loader is executed after babel, so need to be tansformed to ES5.
+ const DocumentContextProvider = function() {};
+ DocumentContextProvider.prototype.getChildContext = function() {
+ return {
+ __initialHtml: initialHtml,
+ __initialData: JSON.stringify(data),
+ __styles: styles,
+ __scripts: scripts,
+ };
+ };
+ DocumentContextProvider.prototype.render = function() {
+ return createElement(Document, initialData);
+ };
+
+ const DocumentContextProviderElement = createElement(DocumentContextProvider);
+
+ const html = '' + renderer.renderToString(DocumentContextProviderElement);
+
+ return html;
+ }
+ `;
+ return this;
+ }
+}
+
+export default function () {
+ const query = typeof this.query === 'string' ? qs.parse(this.query.substr(1)) : this.query;
+ if (!query.entryPath) {
+ query.entryPath = formatPath(this.resourcePath);
+ }
+
+ const customDocumentLoader = new CustomDocumentLoader(query);
+
+ return customDocumentLoader
+ .addInitImport()
+ .addVariableDeclaration()
+ .addGetInitialProps()
+ .addRenderComponentToHTML()
+ .addRenderToHTML()
+ .addRender()
+ .addRenderWithContext()
+ .addExport()
+ .getSource();
+}
diff --git a/packages/plugin-ssr/src/ssr/setBuild.js b/packages/plugin-ssr/src/ssr/setBuild.ts
similarity index 57%
rename from packages/plugin-ssr/src/ssr/setBuild.js
rename to packages/plugin-ssr/src/ssr/setBuild.ts
index 7caea3ec0..9e9f00899 100644
--- a/packages/plugin-ssr/src/ssr/setBuild.js
+++ b/packages/plugin-ssr/src/ssr/setBuild.ts
@@ -1,3 +1,3 @@
-module.exports = (config) => {
+export default (config) => {
config.optimization.minimize(false);
};
diff --git a/packages/plugin-ssr/src/ssr/setDev.js b/packages/plugin-ssr/src/ssr/setDev.ts
similarity index 54%
rename from packages/plugin-ssr/src/ssr/setDev.js
rename to packages/plugin-ssr/src/ssr/setDev.ts
index 118e7d701..4ee39d9d1 100644
--- a/packages/plugin-ssr/src/ssr/setDev.js
+++ b/packages/plugin-ssr/src/ssr/setDev.ts
@@ -1,39 +1,43 @@
-const path = require('path');
-const Module = require('module');
-const fs = require('fs-extra');
-const { parse, print } = require('error-stack-tracey');
-const getEntryName = require('./getEntryName');
-const getMpaRoutes = require('./getMpaRoutes');
+import * as path from 'path';
+import * as Module from 'module';
+import * as fs from 'fs';
+import * as errorStackTracey from 'error-stack-tracey';
+import { getMpaEntries } from '@builder/app-helpers';
+import getEntryName from './getEntryName';
+
+const { parse, print } = errorStackTracey;
function exec(code, filename, filePath) {
- const module = new Module(filename, this);
- module.paths = Module._nodeModulePaths(filePath);
+ const module: any = new Module(filename, this);
+ module.paths = (Module as any)._nodeModulePaths(filePath);
module.filename = filename;
module._compile(code, filename);
return module.exports;
}
-module.exports = (config, context) => {
+export default (config, api) => {
+ const { context, getValue } = api;
const { rootDir, userConfig } = context;
- const { web: webConfig = {} } = userConfig;
+ const { web: webConfig = {}, outputDir } = userConfig;
+ const distDir = path.join(rootDir, outputDir, 'node');
config.mode('development');
-
- let routes = [];
+ const staticConfig = getValue('staticConfig');
+ const { routes } = staticConfig;
if (webConfig.mpa) {
- routes = getMpaRoutes(config);
+ const entries = getMpaEntries(api, { target: 'web', appJsonContent: staticConfig });
+ routes.forEach((route) => {
+ const { entryName } = entries.find(({ source }) => source === route.source);
+ route.path = `/${entryName}.html`;
+ route.entryName = entryName;
+ route.componentPath = path.join(distDir, `${entryName}.js`);
+ });
} else {
- const absoluteAppJSONPath = path.join(rootDir, 'src/app.json');
- const distDir = config.output.get('path');
- const filename = config.output.get('filename');
- // eslint-disable-next-line
- routes = require(absoluteAppJSONPath).routes;
-
routes.forEach((route) => {
const entryName = getEntryName(route.path);
route.entryName = entryName;
- route.componentPath = path.join(distDir, filename.replace('[name]', entryName));
+ route.componentPath = path.join(distDir, `${entryName}.js`);
});
}
@@ -41,16 +45,20 @@ module.exports = (config, context) => {
config.devtool('eval-cheap-source-map');
config.devServer.hot(false);
+ const originalBeforeDevFunc = config.devServer.get('before');
// There can only be one `before` config, this config will overwrite `before` config in web plugin.
config.devServer.set('before', (app, devServer) => {
+ if (originalBeforeDevFunc) {
+ originalBeforeDevFunc(app, devServer);
+ }
// outputFileSystem in devServer is MemoryFileSystem by defalut, but it can also be custom with other file systems.
const outputFs = devServer.compiler.compilers[0].outputFileSystem;
routes.forEach((route) => {
app.get(route.path, async (req, res) => {
const bundleContent = outputFs.readFileSync(route.componentPath, 'utf8');
- process.once('unhandledRejection', async (error) => {
+ process.once('unhandledRejection', async (error: Error) => {
const errorStack = await parse(error, bundleContent);
print(error.message, errorStack);
});
diff --git a/packages/plugin-ssr/src/types.ts b/packages/plugin-ssr/src/types.ts
new file mode 100644
index 000000000..8b492b2e4
--- /dev/null
+++ b/packages/plugin-ssr/src/types.ts
@@ -0,0 +1,17 @@
+export interface IInjectedHTML {
+ scripts: string[];
+ links: string[];
+ metas: string[];
+}
+
+export interface IEntryLoaderQuery {
+ styles: string[];
+ scripts: string[];
+ absoluteAppConfigPath: string;
+ entryPath: string;
+ assetsProcessor?: string;
+ documentPath?: string;
+ builtInHTML?: string;
+ injectedHTML?: IInjectedHTML;
+}
+
diff --git a/packages/plugin-ssr/src/web/setDev.js b/packages/plugin-ssr/src/web/setDev.ts
similarity index 92%
rename from packages/plugin-ssr/src/web/setDev.js
rename to packages/plugin-ssr/src/web/setDev.ts
index 1cab0ef29..911c69c0b 100644
--- a/packages/plugin-ssr/src/web/setDev.js
+++ b/packages/plugin-ssr/src/web/setDev.ts
@@ -1,4 +1,4 @@
-module.exports = (config) => {
+export default (config) => {
const allEntries = config.entryPoints.entries();
// eslint-disable-next-line
for (const entryName in allEntries) {
diff --git a/packages/rax-app-renderer/src/renderer.tsx b/packages/rax-app-renderer/src/renderer.tsx
index 629c96b3f..ddbe52e2b 100644
--- a/packages/rax-app-renderer/src/renderer.tsx
+++ b/packages/rax-app-renderer/src/renderer.tsx
@@ -62,6 +62,10 @@ function App(props) {
}
async function raxAppRenderer(options) {
+ if (!options.appConfig) {
+ options.appConfig = {};
+ }
+
const { appConfig, setAppConfig } = options || {};
setAppConfig(appConfig);