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 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);