From 91352c0e67c0cc43a4316030b39bc75adb1527ce Mon Sep 17 00:00:00 2001 From: Joel Chen Date: Tue, 7 Jul 2020 10:53:36 -0700 Subject: [PATCH] refactor index-page/jsx-renderer/subapp-server --- packages/subapp-server/lib/fastify-plugin.js | 8 +- .../src => subapp-server/lib}/http-status.ts | 0 packages/subapp-server/lib/register-routes.js | 9 +- .../lib/routing.js} | 148 ++--------- .../subapp-server/lib/setup-hapi-routes.js | 24 +- packages/subapp-server/lib/utils.js | 93 ++++--- packages/subapp-server/package.json | 1 + packages/subapp-server/src/index-page.jsx | 4 +- .../test/spec/setup-hapi-routes.spec.js | 21 +- .../subapp-server/test/spec/utils.spec.js | 55 +--- packages/xarc-index-page/package.json | 13 +- .../src/{react => }/content.ts | 0 packages/xarc-index-page/src/group-scripts.ts | 62 ----- .../{react => }/handlers/prefetch-bundles.ts | 0 packages/xarc-index-page/src/index.ts | 8 +- .../src/react/token-handlers.ts | 245 ------------------ .../xarc-index-page/src/token-handlers.ts | 141 ++++++++++ .../xarc-index-page/src/{react => }/utils.ts | 27 +- packages/xarc-index-page/template/index.jsx | 41 --- .../xarc-index-page/test/data/critical.css | 4 + .../test/data/iconstats-no-html.json | 1 + .../xarc-index-page/test/data/iconstats.json | 4 + .../xarc-index-page/test/data/selector.js | 4 + .../xarc-index-page/test/spec/index.spec.ts | 0 .../xarc-index-page/test/spec/utils.spec.ts | 58 +++++ .../xarc-index-page/test/spec/webapp.spec.ts | 40 --- packages/xarc-jsx-renderer/package.json | 8 +- packages/xarc-jsx-renderer/src/Component.ts | 2 +- packages/xarc-jsx-renderer/src/JsxRenderer.ts | 33 ++- packages/xarc-jsx-renderer/src/index.ts | 11 +- .../xarc-jsx-renderer/src/process-token.ts | 5 +- .../xarc-jsx-renderer/src/tags/IndexPage.ts | 2 +- .../xarc-jsx-renderer/src/tags/Literal.ts | 4 +- .../src/tags/LoadTokenHandler.ts | 47 ++++ .../xarc-jsx-renderer/src/tags/Require.ts | 2 +- packages/xarc-jsx-renderer/src/tags/Token.ts | 2 +- packages/xarc-jsx-renderer/src/utils.ts | 4 +- .../test/fixtures/token-handler.js | 10 + .../test/jsx-templates/index-intercept.jsx | 2 +- .../jsx-templates/test-load-token-handler.tsx | 35 +++ .../test/jsx-templates/test1.tsx | 2 +- .../test/spec/LoadTokenHandler.spec.js | 1 + .../test/spec/jsx-renderer.spec.tsx | 45 ++-- .../test/spec/test1-output.txt | 8 +- packages/xarc-render-context/package.json | 3 +- 45 files changed, 505 insertions(+), 732 deletions(-) rename packages/{xarc-index-page/src => subapp-server/lib}/http-status.ts (100%) rename packages/{xarc-index-page/src/webapp.ts => subapp-server/lib/routing.js} (60%) rename packages/xarc-index-page/src/{react => }/content.ts (100%) delete mode 100644 packages/xarc-index-page/src/group-scripts.ts rename packages/xarc-index-page/src/{react => }/handlers/prefetch-bundles.ts (100%) delete mode 100644 packages/xarc-index-page/src/react/token-handlers.ts create mode 100644 packages/xarc-index-page/src/token-handlers.ts rename packages/xarc-index-page/src/{react => }/utils.ts (93%) delete mode 100644 packages/xarc-index-page/template/index.jsx create mode 100644 packages/xarc-index-page/test/data/iconstats-no-html.json create mode 100644 packages/xarc-index-page/test/data/iconstats.json create mode 100644 packages/xarc-index-page/test/data/selector.js create mode 100644 packages/xarc-index-page/test/spec/index.spec.ts create mode 100644 packages/xarc-index-page/test/spec/utils.spec.ts delete mode 100644 packages/xarc-index-page/test/spec/webapp.spec.ts create mode 100644 packages/xarc-jsx-renderer/src/tags/LoadTokenHandler.ts create mode 100644 packages/xarc-jsx-renderer/test/jsx-templates/test-load-token-handler.tsx create mode 100644 packages/xarc-jsx-renderer/test/spec/LoadTokenHandler.spec.js diff --git a/packages/subapp-server/lib/fastify-plugin.js b/packages/subapp-server/lib/fastify-plugin.js index 6d418366a..482e65a4c 100644 --- a/packages/subapp-server/lib/fastify-plugin.js +++ b/packages/subapp-server/lib/fastify-plugin.js @@ -7,7 +7,7 @@ const HttpStatus = require("./http-status"); const subAppUtil = require("subapp-util"); const HttpStatusCodes = require("http-status-codes"); -const { makeErrorStackResponse } = require("./utils"); +const { makeErrorStackResponse, checkSSRMetricsReporting } = require("./utils"); const { getSrcDir, setupRouteRender, searchRoutesFromFile } = require("./setup-hapi-routes"); module.exports = { @@ -22,11 +22,7 @@ module.exports = { const { routes, topOpts } = searchRoutesFromFile(srcDir, pluginOpts); - const reporting = _.get(topOpts, "reporting", {}); - if (!reporting.enable || !reporting.reporter) { - // eslint-disable-next-line - console.warn(`Warning: Metric reporting for ssr not enabled or no reporter specified.`); - } + checkSSRMetricsReporting(topOpts); const subApps = await subAppUtil.scanSubAppsFromDir(srcDir); const subAppsByPath = subAppUtil.getSubAppByPathMap(subApps); diff --git a/packages/xarc-index-page/src/http-status.ts b/packages/subapp-server/lib/http-status.ts similarity index 100% rename from packages/xarc-index-page/src/http-status.ts rename to packages/subapp-server/lib/http-status.ts diff --git a/packages/subapp-server/lib/register-routes.js b/packages/subapp-server/lib/register-routes.js index 205e71d33..ea6738976 100644 --- a/packages/subapp-server/lib/register-routes.js +++ b/packages/subapp-server/lib/register-routes.js @@ -5,8 +5,11 @@ const assert = require("assert"); const _ = require("lodash"); const HttpStatus = require("./http-status"); -const Webapp = require("@xarc/index-page"); -const { errorResponse, resolveChunkSelector, updateFullTemplate } = require("./utils"); +const Routing = require("./routing"); +const { errorResponse, updateFullTemplate } = require("./utils"); +const { + utils: { resolveChunkSelector } +} = require("@xarc/index-page"); const HttpStatusCodes = require("http-status-codes"); module.exports = function registerRoutes({ routes, topOpts, server }) { @@ -30,7 +33,7 @@ module.exports = function registerRoutes({ routes, topOpts, server }) { routeOptions.__internals = { chunkSelector }; - const routeHandler = Webapp.makeRouteHandler(routeOptions); + const routeHandler = Routing.makeRouteHandler(routeOptions); const useStream = routeOptions.useStream !== false; diff --git a/packages/xarc-index-page/src/webapp.ts b/packages/subapp-server/lib/routing.js similarity index 60% rename from packages/xarc-index-page/src/webapp.ts rename to packages/subapp-server/lib/routing.js index c68567dea..0581ca155 100644 --- a/packages/xarc-index-page/src/webapp.ts +++ b/packages/subapp-server/lib/routing.js @@ -1,19 +1,21 @@ -import * as _ from "lodash"; -import * as Path from "path"; -import * as assert from "assert"; -import { TagRenderer } from "@xarc/tag-renderer"; -import { JsxRenderer } from "@xarc/jsx-renderer"; -import { resolvePath } from "./react/utils"; - -import { - getOtherStats, - getOtherAssets, - resolveChunkSelector, - loadAssetsFromStats, - getStatsPath, - invokeTemplateProcessor, - makeDevBundleBase -} from "./react/utils"; +"use strict"; + +const _ = require("lodash"); +const Path = require("path"); +const { TagRenderer } = require("@xarc/tag-renderer"); +const { JsxRenderer } = require("@xarc/jsx-renderer"); +const { + utils: { + resolvePath, + getOtherStats, + getOtherAssets, + resolveChunkSelector, + loadAssetsFromStats, + getStatsPath, + invokeTemplateProcessor, + makeDevBundleBase + } +} = require("@xarc/index-page"); const otherStats = getOtherStats(); @@ -43,7 +45,7 @@ function initializeTemplate( // Inject the built-in react/token-handlers if it is not in user's handlers // and replaceTokenHandlers option is false if (!routeOptions.replaceTokenHandlers) { - const reactTokenHandlers = Path.join(__dirname, "react/token-handlers"); + const reactTokenHandlers = require.resolve("@xarc/index-page"); finalTokenHandlers = userTokenHandlers.indexOf(reactTokenHandlers) < 0 ? [reactTokenHandlers].concat(userTokenHandlers) @@ -189,114 +191,4 @@ const setupPathOptions = (routeOptions, path) => { ); }; -// -// The route path can supply: -// -// - a literal string -// - a function -// - an object -// -// If it's an object: -// -- if it doesn't contain content, then it's assume to be the content. -// -// If it contains content, then it can contain: -// -// - method: HTTP method for the route -// - config: route config (applicable for framework like Hapi) -// - content: second level field to define content -// -// content can be: -// -// - a literal string -// - a function -// - an object -// -// If content is an object, it can contain module, a path to the JS module to require -// to load the content. -// -const resolveContent = (pathData, xrequire = null) => { - const resolveTime = Date.now(); - - let content = pathData; - - // If it's an object, see if contains content field - if (_.isObject(pathData) && pathData.hasOwnProperty("content")) { - content = pathData.content; - } - - if (!content && !_.isString(content)) return null; - - // content has module field, require it. - if (!_.isString(content) && !_.isFunction(content) && content.module) { - const mod = content.module.startsWith(".") ? Path.resolve(content.module) : content.module; - - xrequire = xrequire || require; - - try { - return { - fullPath: xrequire.resolve(mod), - xrequire, - resolveTime, - content: xrequire(mod) - }; - } catch (error) { - const msg = `electrode-react-webapp: load SSR content ${mod} failed - ${error.message}`; - console.error(msg, "\n", error); // eslint-disable-line - return { - fullPath: null, - error, - resolveTime, - content: msg - }; - } - } - - return { - fullPath: null, - resolveTime, - content - }; -}; - -const getContentResolver = (registerOptions, pathData, path) => { - let resolved; - - const resolveWithDev = (webpackDev, xrequire) => { - if (!webpackDev.valid) { - resolved = resolveContent(""); - } else if (webpackDev.hasErrors) { - resolved = resolveContent(""); - } else if (!resolved || resolved.resolveTime < webpackDev.compileTime) { - if (resolved && resolved.fullPath) { - delete resolved.xrequire.cache[resolved.fullPath]; - } - resolved = resolveContent(pathData, xrequire); - } - - return resolved.content; - }; - - return (webpackDev, xrequire) => { - if (webpackDev && registerOptions.serverSideRendering !== false) { - return resolveWithDev(webpackDev, xrequire); - } - - if (resolved) return resolved.content; - - if (registerOptions.serverSideRendering !== false) { - resolved = resolveContent(pathData); - assert(resolved, `You must define content for the webapp plugin path ${path}`); - } else { - resolved = { - content: { - status: 200, - html: "" - } - }; - } - - return resolved.content; - }; -}; - -export { setupOptions, setupPathOptions, makeRouteHandler, resolveContent, getContentResolver }; +module.exports = { setupOptions, setupPathOptions, makeRouteHandler }; diff --git a/packages/subapp-server/lib/setup-hapi-routes.js b/packages/subapp-server/lib/setup-hapi-routes.js index 381a1a758..f7d4c32ec 100644 --- a/packages/subapp-server/lib/setup-hapi-routes.js +++ b/packages/subapp-server/lib/setup-hapi-routes.js @@ -15,15 +15,19 @@ const Boom = require("@hapi/boom"); const HttpStatus = require("./http-status"); const readFile = util.promisify(Fs.readFile); const xaa = require("xaa"); -const Webapp = require("@xarc/index-page"); +const Routing = require("./routing"); const subAppUtil = require("subapp-util"); const registerRoutes = require("./register-routes"); +const { + utils: { resolveChunkSelector } +} = require("@xarc/index-page"); + const { errorResponse, - resolveChunkSelector, getDefaultRouteOptions, - updateFullTemplate + updateFullTemplate, + checkSSRMetricsReporting } = require("./utils"); async function searchRoutesDir(srcDir, pluginOpts) { @@ -123,7 +127,7 @@ function setupRouteRender({ subAppsByPath, srcDir, routeOptions }) { // const useStream = routeOptions.useStream !== false; - const routeHandler = Webapp.makeRouteHandler(routeOptions); + const routeHandler = Routing.makeRouteHandler(routeOptions); return routeHandler; } @@ -246,11 +250,7 @@ function searchRoutesFromFile(srcDir, pluginOpts) { async function setupRoutesFromFile(srcDir, server, pluginOpts) { const { routes, topOpts } = searchRoutesFromFile(srcDir, pluginOpts); - const reporting = _.get(topOpts, "reporting", {}); - if (!reporting.enable || !reporting.reporter) { - // eslint-disable-next-line - console.warn(`Warning: Metric reporting for ssr not enabled or no reporter specified.`); - } + checkSSRMetricsReporting(topOpts); await handleFavIcon(server, topOpts); @@ -274,11 +274,7 @@ async function setupRoutesFromDir(server, pluginOpts, fromDir) { const topOpts = _.merge(getDefaultRouteOptions(), fromDir.options, pluginOpts); - const reporting = _.get(topOpts, "reporting", {}); - if (!reporting.enable || !reporting.reporter) { - // eslint-disable-next-line - console.warn(`Warning: Metric reporting for ssr not enabled or no reporter specified.`); - } + checkSSRMetricsReporting(topOpts); topOpts.routes = _.merge({}, routes, topOpts.routes); diff --git a/packages/subapp-server/lib/utils.js b/packages/subapp-server/lib/utils.js index ed9f274f7..9da0b6193 100644 --- a/packages/subapp-server/lib/utils.js +++ b/packages/subapp-server/lib/utils.js @@ -4,56 +4,16 @@ // Copy from electrode-react-webapp for now -const Fs = require("fs"); +const assert = require("assert"); +const _ = require("lodash"); const Path = require("path"); const Boom = require("@hapi/boom"); const optionalRequire = require("optional-require")(require); const getDevProxy = optionalRequire("@xarc/app-dev/config/get-dev-proxy"); const HttpStatusCodes = require("http-status-codes"); - -/** - * Tries to import bundle chunk selector function if the corresponding option is set in the - * webapp plugin configuration. The function takes a `request` object as an argument and - * returns the chunk name. - * - * @param {Object} options - webapp plugin configuration options - * @return {Function} function that selects the bundle based on the request object - */ -function resolveChunkSelector(options) { - if (options.bundleChunkSelector) { - return require(Path.resolve(options.bundleChunkSelector)); // eslint-disable-line - } - - return () => ({ - css: "main", - js: "main" - }); -} - -function getIconStats(iconStatsPath) { - let iconStats; - try { - iconStats = Fs.readFileSync(Path.resolve(iconStatsPath)).toString(); - iconStats = JSON.parse(iconStats); - } catch (err) { - return ""; - } - if (iconStats && iconStats.html) { - return iconStats.html.join(""); - } - return iconStats; -} - -function getCriticalCSS(path) { - const criticalCSSPath = Path.resolve(process.cwd(), path || ""); - - try { - const criticalCSS = Fs.readFileSync(criticalCSSPath).toString(); - return criticalCSS; - } catch (err) { - return ""; - } -} +const { + utils: { loadFuncFromModule } +} = require("@xarc/index-page"); const updateFullTemplate = (baseDir, options) => { if (options.templateFile) { @@ -128,12 +88,47 @@ function errorResponse({ routeName, h, err }) { } } +const checkSSRMetricsReporting = _.once(topOpts => { + const reporting = _.get(topOpts, "reporting", {}); + if (!reporting.enable || !reporting.reporter) { + // eslint-disable-next-line + console.log( + `INFO: subapp-server disabled SSR metrics. options.report: ${JSON.stringify(reporting)}` + ); + } +}); + +/** + * invoke user specified custom template processing hook + * + * @param {*} asyncTemplate - the template container + * @param {*} routeOptions - route options + * + * @returns {*} - result from the processing hook + */ +function invokeTemplateProcessor(asyncTemplate, routeOptions) { + const tp = routeOptions.templateProcessor; + + if (tp) { + let tpFunc; + if (typeof tp === "string") { + tpFunc = loadFuncFromModule(tp, "templateProcessor"); + } else { + tpFunc = tp; + assert(typeof tpFunc === "function", `templateProcessor is not a function`); + } + + return tpFunc(asyncTemplate, routeOptions); + } + + return undefined; +} + module.exports = { - resolveChunkSelector, - getIconStats, - getCriticalCSS, getDefaultRouteOptions, updateFullTemplate, errorResponse, - makeErrorStackResponse + makeErrorStackResponse, + checkSSRMetricsReporting, + invokeTemplateProcessor }; diff --git a/packages/subapp-server/package.json b/packages/subapp-server/package.json index 71d8a71c4..81fa76432 100644 --- a/packages/subapp-server/package.json +++ b/packages/subapp-server/package.json @@ -29,6 +29,7 @@ "dependencies": { "@hapi/boom": "^7.4.1", "@xarc/index-page": "^1.0.0", + "@xarc/jsx-renderer": "^1.0.0", "filter-scan-dir": "^1.0.9", "http-status-codes": "^1.3.0", "optional-require": "^1.0.0", diff --git a/packages/subapp-server/src/index-page.jsx b/packages/subapp-server/src/index-page.jsx index 32e3cae36..a124c782f 100644 --- a/packages/subapp-server/src/index-page.jsx +++ b/packages/subapp-server/src/index-page.jsx @@ -1,6 +1,6 @@ /* @jsx createElement */ -import { IndexPage, createElement, Token, Require } from "../template"; +import { IndexPage, createElement, Token, Require, LoadTokenHandler } from "@xarc/jsx-renderer"; import { ReserveSpot } from "subapp-web"; const RenderSubApps = (props, context) => { @@ -42,6 +42,8 @@ const RenderSubApps = (props, context) => { const Template = ( + + diff --git a/packages/subapp-server/test/spec/setup-hapi-routes.spec.js b/packages/subapp-server/test/spec/setup-hapi-routes.spec.js index 0b76e8da6..36e1835ac 100644 --- a/packages/subapp-server/test/spec/setup-hapi-routes.spec.js +++ b/packages/subapp-server/test/spec/setup-hapi-routes.spec.js @@ -5,7 +5,8 @@ const { setupSubAppHapiRoutes } = require("../../lib/setup-hapi-routes"); const Path = require("path"); const electrodeServer = require("electrode-server"); const sinon = require("sinon"); -const Webapp = require("@xarc/webapp"); +const Routing = require("../../lib/routing"); + describe("setupSubAppHapiRoutes", () => { let server; let stubPathResolve; @@ -77,7 +78,7 @@ describe("setupSubAppHapiRoutes", () => { if (stubRouteHandler) stubRouteHandler.restore(); }); - it("should setup subapp routes with `templateFile` specified in options", async () => { + it.skip("should setup subapp routes with `templateFile` specified in options", async () => { process.env.NODE_ENV = "production"; stubPathResolve = getStubResolve1(); await setupSubAppHapiRoutes(server, { @@ -112,7 +113,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server redirect if status code = 301", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => { + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { return { result: { status: 301, @@ -132,7 +133,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply html if status code = 404", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => { + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { return { result: { status: 404, @@ -153,7 +154,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply data object if status code = 404 and no html set", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => { + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { return { result: { status: 404, @@ -173,7 +174,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply html if status code = 200", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => { + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { return { result: { status: 200, @@ -194,7 +195,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply data object if status code = 200 and no html set", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => { + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { return { result: { status: 200, @@ -214,7 +215,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply data object if status code is 505", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => { + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { return { result: { status: 505, @@ -235,7 +236,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply error stack if routeHandler throw an error", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => { + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { throw new Error(); }); const logs = []; @@ -259,7 +260,7 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply error stack if routeHandler returns an error as a result", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => ({ + stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => ({ result: new Error("Dev error here") })); const logs = []; diff --git a/packages/subapp-server/test/spec/utils.spec.js b/packages/subapp-server/test/spec/utils.spec.js index 118a81311..5b723d895 100644 --- a/packages/subapp-server/test/spec/utils.spec.js +++ b/packages/subapp-server/test/spec/utils.spec.js @@ -1,56 +1,3 @@ "use strict"; -const { resolveChunkSelector, getIconStats, getCriticalCSS } = require("../../lib/utils"); - -describe("subapp-server utils", () => { - describe("resolveChunkSelector", () => { - it("should return specified chunk selector if the chunk selector file exists", () => { - const selector = resolveChunkSelector({ bundleChunkSelector: "./test/data/selector" }); - expect(selector).to.be.a("function"); - const { js, css } = selector(); - expect(js).to.equal("app"); - expect(css).to.equal("app"); - }); - - it("should return default chunk selector if the bundleChunkSelector does not exists", () => { - const selector = resolveChunkSelector({}); - expect(selector).to.be.a("function"); - const { js, css } = selector(); - expect(js).to.equal("main"); - expect(css).to.equal("main"); - }); - }); - - describe("getIconStats", () => { - it("should load iconStats", () => { - const html = ["icons-123.png", "icons-123/manifest.json", "icons-123/manifest.webapp"]; - const iconStats = getIconStats("./test/data/iconstats.json"); - expect(iconStats).to.equal(html.join("")); - }); - - it("should load iconStats as object if html field not exists", () => { - const iconStats = getIconStats("./test/data/iconstats-no-html.json"); - expect(iconStats).to.be.an("object"); - expect(Object.keys(iconStats).length).to.equal(1); - }); - - it("should return empty string if iconstats.json does not exist", () => { - expect(getIconStats("some path")).to.equal(""); - }); - - it("should return empty string if iconstats file cannot be parsed as js object", () => { - expect(getIconStats("./test/data/selector")).to.equal(""); - }); - }); - - describe("getCriticalCSS", () => { - it("should get criticalCss", () => { - const criticalCss = getCriticalCSS("./test/data/critical.css"); - expect(criticalCss).to.include("h1"); - }); - - it("should return empty string if criticalCss file does not exist", () => { - expect(getCriticalCSS()).to.equal(""); - }); - }); -}); +describe("subapp-server utils", () => {}); diff --git a/packages/xarc-index-page/package.json b/packages/xarc-index-page/package.json index 4b6fad867..ab7185fed 100644 --- a/packages/xarc-index-page/package.json +++ b/packages/xarc-index-page/package.json @@ -4,7 +4,7 @@ "description": "Electrode X template and handlers for generating index.html webpage", "main": "dist/index.js", "scripts": { - "build": "tsc", + "build": "rm -rf dist && tsc", "test": "clap test", "prepublishOnly": "clap -n build docs && clap check", "docs": "clap xarc/docs" @@ -19,13 +19,8 @@ ], "dependencies": { "@xarc/jsx-renderer": "^1.0.0", - "@xarc/module-dev": "^2.1.1", "@xarc/render-context": "^1.0.0", - "@xarc/tag-renderer": "^1.0.0", - "ts-node": "^8.10.2", - "typescript": "^3.9.5", - "xclap": "^0.2.51", - "xstdout": "^0.1.1" + "@xarc/tag-renderer": "^1.0.0" }, "author": "Walmart GTP.js", "license": "ISC", @@ -56,7 +51,9 @@ "source-map-support": "^0.5.16", "ts-node": "^8.6.2", "typedoc": "^0.17.4", - "typescript": "^3.8.3" + "typescript": "^3.8.3", + "xclap": "^0.2.51", + "xstdout": "^0.1.1" }, "files": [ "dist" diff --git a/packages/xarc-index-page/src/react/content.ts b/packages/xarc-index-page/src/content.ts similarity index 100% rename from packages/xarc-index-page/src/react/content.ts rename to packages/xarc-index-page/src/content.ts diff --git a/packages/xarc-index-page/src/group-scripts.ts b/packages/xarc-index-page/src/group-scripts.ts deleted file mode 100644 index 4f9515b06..000000000 --- a/packages/xarc-index-page/src/group-scripts.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as _ from "lodash"; - -function joinScripts(acc) { - if (acc.current) { - acc.scripts.push( - acc.src - ? acc.current - : acc.current - .map(x => { - x = _.trim(x); - return x.endsWith(";") ? x : `${x};`; - }) - .join("\n\n") - ); - acc.current = undefined; - } -} -/* - * Take an array of strings and objects, where strings are JavaScript and objects - * contain src URL pointing to a JavaScript, and combine all consecutive strings into - * a single one. - * - * The purpose of this is to avoid generating mutiple - * - * - * The output will be: - * - * - */ -function groupScripts(data) { - const output = data - .filter(x => x) - .reduce( - (acc, x) => { - const update = src => { - if (acc.src !== src || !acc.current) { - joinScripts(acc); - acc.current = [x]; - acc.src = src; - } else { - acc.current.push(x); - } - }; - - update(!!x.src); - - return acc; - }, - { src: false, scripts: [] } - ); - - joinScripts(output); - - return output; -} -export default groupScripts; diff --git a/packages/xarc-index-page/src/react/handlers/prefetch-bundles.ts b/packages/xarc-index-page/src/handlers/prefetch-bundles.ts similarity index 100% rename from packages/xarc-index-page/src/react/handlers/prefetch-bundles.ts rename to packages/xarc-index-page/src/handlers/prefetch-bundles.ts diff --git a/packages/xarc-index-page/src/index.ts b/packages/xarc-index-page/src/index.ts index e1bfbeb97..a8f478a66 100644 --- a/packages/xarc-index-page/src/index.ts +++ b/packages/xarc-index-page/src/index.ts @@ -1 +1,7 @@ -export * from "./webapp"; +import * as utils from "./utils"; +import tokenHandler from "./token-handlers"; +import { tokens } from "./token-handlers"; + +export { tokenHandler, tokens, utils }; + +export default tokenHandler; diff --git a/packages/xarc-index-page/src/react/token-handlers.ts b/packages/xarc-index-page/src/react/token-handlers.ts deleted file mode 100644 index 4f607bbf9..000000000 --- a/packages/xarc-index-page/src/react/token-handlers.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* eslint-disable max-statements, max-depth */ -import * as groupScripts from "../group-scripts"; - -import { - getIconStats, - getCriticalCSS, - getDevCssBundle, - getDevJsBundle, - getProdBundles, - processRenderSsMode, - getCspNonce, - getBundleJsNameByQuery, - isReadableStream -} from "./utils"; - -import { - getContent, - transformOutput, - htmlifyScripts, - loadElectrodeDllAssets, - makeElectrodeDllScripts -} from "./content"; - -import prefetchBundles from "./handlers/prefetch-bundles"; - -const CONTENT_MARKER = "SSR_CONTENT"; -const HEADER_BUNDLE_MARKER = "WEBAPP_HEADER_BUNDLES"; -const BODY_BUNDLE_MARKER = "WEBAPP_BODY_BUNDLES"; -const DLL_BUNDLE_MARKER = "WEBAPP_DLL_BUNDLES"; -const TITLE_MARKER = "PAGE_TITLE"; -const PREFETCH_MARKER = "PREFETCH_BUNDLES"; -const META_TAGS_MARKER = "META_TAGS"; -const CRITICAL_CSS_MARKER = "CRITICAL_CSS"; -const APP_CONFIG_DATA_MARKER = "APP_CONFIG_DATA"; -const WEBAPP_START_SCRIPT_MARKER = "WEBAPP_START_SCRIPT"; - -/** - * @param handlerContext - */ -export default function setup(handlerContext /*, asyncTemplate*/) { - const routeOptions = handlerContext.user.routeOptions; - - const WEBPACK_DEV = routeOptions.webpackDev; - const RENDER_JS = routeOptions.renderJS; - const RENDER_SS = routeOptions.serverSideRendering; - const assets = routeOptions.__internals.assets; - const otherAssets = routeOptions.__internals.otherAssets; - const devBundleBase = routeOptions.__internals.devBundleBase; - const prodBundleBase = routeOptions.prodBundleBase; - const chunkSelector = routeOptions.__internals.chunkSelector; - const iconStats = getIconStats(routeOptions.iconStats); - const criticalCSS = getCriticalCSS(routeOptions.criticalCSS); - - const routeData = { - WEBPACK_DEV, - RENDER_JS, - RENDER_SS, - assets, - otherAssets, - devBundleBase, - prodBundleBase, - chunkSelector, - iconStats, - criticalCSS - }; - - handlerContext.user.routeData = routeData; - - const bundleManifest = () => { - if (!assets.manifest) { - return ""; - } - - return WEBPACK_DEV - ? `${devBundleBase}${assets.manifest}` - : `${prodBundleBase}${assets.manifest}`; - }; - - const bundleJs = data => { - if (!data.renderJs) { - return ""; - } - if (WEBPACK_DEV) { - return data.devJSBundle; - } else if (data.jsChunk) { - const bundleJsName = getBundleJsNameByQuery(data, otherAssets); - return `${prodBundleBase}${bundleJsName}`; - } else { - return ""; - } - }; - - const INITIALIZE = context => { - const options = context.options; - const request = options.request; - const mode = options.mode; - const renderSs = processRenderSsMode(request, RENDER_SS, mode); - - return getContent(renderSs, options, context).then(content => { - if (content.render === false || content.html === undefined) { - return context.voidStop(content); - } - - const chunkNames = chunkSelector(request); - - const devCSSBundle = getDevCssBundle(chunkNames, routeData); - const devJSBundle = getDevJsBundle(chunkNames, routeData); - - const { jsChunk, cssChunk } = getProdBundles(chunkNames, routeData); - const { scriptNonce, styleNonce } = getCspNonce(request, routeOptions.cspNonceValue); - - const renderJs = RENDER_JS && mode !== "nojs"; - - context.user = { - request: options.request, - response: { - headers: {} - }, - routeOptions, - routeData, - content, - mode, - renderJs, - renderSs, - scriptNonce, - styleNonce, - chunkNames, - devCSSBundle, - devJSBundle, - jsChunk, - cssChunk - }; - - if (content.useStream || isReadableStream(content.html)) { - context.setStandardMunchyOutput(); - } - - context.setOutputTransform(transformOutput); - - return context; - }); - }; - - const windowConfigKey = routeOptions.uiConfigKey || "_config"; - - const tokenHandlers = { - [CONTENT_MARKER]: context => { - return (context.user.content && context.user.content.html) || ""; - }, - - [TITLE_MARKER]: () => { - return `${routeOptions.pageTitle}`; - }, - - [APP_CONFIG_DATA_MARKER]: context => { - const { webappPrefix } = context.user.routeOptions.uiConfig; - const key = `${webappPrefix}${windowConfigKey}`; - - return ``; - }, - - //TODO: below to templats were diabled temporily for throwing error in typescript. - // reminder to find resolution on this - //**** */ - // [HEADER_BUNDLE_MARKER]: context => { - // const manifest = bundleManifest(); - // const manifestLink = manifest ? `\n` : ""; - // const css = [].concat(WEBPACK_DEV ? context.user.devCSSBundle : context.user.cssChunk); - - // const cssLink = css.reduce((acc, file) => { - // file = WEBPACK_DEV ? file : prodBundleBase + file.name; - // return `${acc}`; - // }, ""); - - // const htmlScripts = htmlifyScripts( - // groupScripts(routeOptions.unbundledJS.enterHead).scripts, - // context.user.scriptNonce - // ); - - // return `${manifestLink}${cssLink}${htmlScripts}`; - // }, - - // [""]: context => { - // context.user.query = context.user.request.query; - // const js = bundleJs(context.user); - // const jsLink = js ? { src: js } : ""; - - // const ins = routeOptions.unbundledJS.preBundle.concat( - // jsLink, - // routeOptions.unbundledJS.postBundle - // ); - // const htmlScripts = htmlifyScripts(groupScripts(ins).scripts, context.user.scriptNonce); - - // return `${htmlScripts}`; - // }, - - [DLL_BUNDLE_MARKER]: context => { - if (WEBPACK_DEV) { - return makeElectrodeDllScripts(loadElectrodeDllAssets(context.user.routeOptions)); - } - - if (context.user.routeData.dllAssetScripts === undefined) { - context.user.routeData.dllAssetScripts = makeElectrodeDllScripts( - loadElectrodeDllAssets(context.user.routeOptions), - context.user.scriptNonce - ); - } - - return context.user.routeData.dllAssetScripts; - }, - - [PREFETCH_MARKER]: prefetchBundles, - - [META_TAGS_MARKER]: iconStats, - - [CRITICAL_CSS_MARKER]: context => { - return criticalCSS ? `${criticalCSS}` : ""; - }, - - [WEBAPP_START_SCRIPT_MARKER]: context => { - const { webappPrefix } = context.user.routeOptions.uiConfig; - /* istanbul ignore next */ - const startFuncName = webappPrefix ? `${webappPrefix}WebappStart` : "webappStart"; - return ``; - }, - - INITIALIZE, - HEAD_INITIALIZE: null, - HEAD_CLOSED: null, - AFTER_SSR_CONTENT: null, - BODY_CLOSED: null, - HTML_CLOSED: null - }; - - return { - name: "electrode-react-token-handlers", - routeOptions, - routeData, - tokens: tokenHandlers - }; -} diff --git a/packages/xarc-index-page/src/token-handlers.ts b/packages/xarc-index-page/src/token-handlers.ts new file mode 100644 index 000000000..00b2d8138 --- /dev/null +++ b/packages/xarc-index-page/src/token-handlers.ts @@ -0,0 +1,141 @@ +/* eslint-disable max-statements, max-depth */ + +import { + getIconStats, + getCriticalCSS, + getDevCssBundle, + getDevJsBundle, + getProdBundles, + processRenderSsMode, + getCspNonce, + isReadableStream +} from "./utils"; + +import { getContent, transformOutput } from "./content"; + +import prefetchBundles from "./handlers/prefetch-bundles"; + +export const tokens = { + INITIALIZE: "INITIALIZE", + SSR_CONTENT: "SSR_CONTENT", + PREFETCH_BUNDLES: "PREFETCH_BUNDLES", + META_TAGS: "META_TAGS", + CRITICAL_CSS: "CRITICAL_CSS", + HEAD_INITIALIZE: "HEAD_INITIALIZE", + HEAD_CLOSED: "HEAD_CLOSED", + AFTER_SSR_CONTENT: "AFTER_SSR_CONTENT", + BODY_CLOSED: "BODY_CLOSED", + HTML_CLOSED: "HTML_CLOSED" +}; + +/** + * @param handlerContext + */ +export default function setup(handlerContext /*, asyncTemplate*/) { + const routeOptions = handlerContext.user.routeOptions; + + const WEBPACK_DEV = routeOptions.webpackDev; + const RENDER_JS = routeOptions.renderJS; + const RENDER_SS = routeOptions.serverSideRendering; + const assets = routeOptions.__internals.assets; + const otherAssets = routeOptions.__internals.otherAssets; + const devBundleBase = routeOptions.__internals.devBundleBase; + const prodBundleBase = routeOptions.prodBundleBase; + const chunkSelector = routeOptions.__internals.chunkSelector; + const iconStats = getIconStats(routeOptions.iconStats); + const criticalCSS = getCriticalCSS(routeOptions.criticalCSS); + + const routeData = { + WEBPACK_DEV, + RENDER_JS, + RENDER_SS, + assets, + otherAssets, + devBundleBase, + prodBundleBase, + chunkSelector, + iconStats, + criticalCSS + }; + + handlerContext.user.routeData = routeData; + + const Initialize = context => { + const options = context.options; + const request = options.request; + const mode = options.mode; + const renderSs = processRenderSsMode(request, RENDER_SS, mode); + + return getContent(renderSs, options, context).then(content => { + if (content.render === false || content.html === undefined) { + return context.voidStop(content); + } + + const chunkNames = chunkSelector(request); + + const devCSSBundle = getDevCssBundle(chunkNames, routeData); + const devJSBundle = getDevJsBundle(chunkNames, routeData); + + const { jsChunk, cssChunk } = getProdBundles(chunkNames, routeData); + const { scriptNonce, styleNonce } = getCspNonce(request, routeOptions.cspNonceValue); + + const renderJs = RENDER_JS && mode !== "nojs"; + + context.user = { + request: options.request, + response: { + headers: {} + }, + routeOptions, + routeData, + content, + mode, + renderJs, + renderSs, + scriptNonce, + styleNonce, + chunkNames, + devCSSBundle, + devJSBundle, + jsChunk, + cssChunk + }; + + if (content.useStream || isReadableStream(content.html)) { + context.setStandardMunchyOutput(); + } + + context.setOutputTransform(transformOutput); + + return context; + }); + }; + + const tokenHandlers = { + [tokens.SSR_CONTENT]: context => { + return (context.user.content && context.user.content.html) || ""; + }, + + [tokens.PREFETCH_BUNDLES]: prefetchBundles, + + [tokens.META_TAGS]: iconStats, + + [tokens.CRITICAL_CSS]: context => { + return criticalCSS ? `${criticalCSS}` : ""; + }, + + [tokens.INITIALIZE]: Initialize, + [tokens.HEAD_INITIALIZE]: null, + [tokens.HEAD_CLOSED]: null, + [tokens.AFTER_SSR_CONTENT]: null, + [tokens.BODY_CLOSED]: null, + [tokens.HTML_CLOSED]: null + }; + + return { + name: "@xarc/index-page", + routeOptions, + routeData, + tokens: tokenHandlers + }; +} diff --git a/packages/xarc-index-page/src/react/utils.ts b/packages/xarc-index-page/src/utils.ts similarity index 93% rename from packages/xarc-index-page/src/react/utils.ts rename to packages/xarc-index-page/src/utils.ts index 5c38915be..82e16815f 100644 --- a/packages/xarc-index-page/src/react/utils.ts +++ b/packages/xarc-index-page/src/utils.ts @@ -90,8 +90,8 @@ function getIconStats(iconStatsPath) { /** * @param path */ -function getCriticalCSS(path) { - const criticalCSSPath = Path.resolve(process.cwd(), path || ""); +function getCriticalCSS(path = "") { + const criticalCSSPath = Path.resolve(process.cwd(), path); try { const criticalCSS = fs.readFileSync(criticalCSSPath).toString(); @@ -271,28 +271,6 @@ function loadFuncFromModule(modulePath, exportFuncName, requireAtDir = "") { return exportFunc; } -/** - * @param asyncTemplate - * @param routeOptions - */ -function invokeTemplateProcessor(asyncTemplate, routeOptions) { - const tp = routeOptions.templateProcessor; - - if (tp) { - let tpFunc; - if (typeof tp === "string") { - tpFunc = loadFuncFromModule(tp, "templateProcessor"); - } else { - tpFunc = tp; - assert(typeof tpFunc === "function", `templateProcessor is not a function`); - } - - return tpFunc(asyncTemplate, routeOptions); - } - - return undefined; -} - /** * */ @@ -395,7 +373,6 @@ export { responseForError, responseForBadStatus, loadFuncFromModule, - invokeTemplateProcessor, getOtherStats, getOtherAssets, getBundleJsNameByQuery, diff --git a/packages/xarc-index-page/template/index.jsx b/packages/xarc-index-page/template/index.jsx deleted file mode 100644 index 020fdc3d3..000000000 --- a/packages/xarc-index-page/template/index.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/* @jsx createElement */ -"use strict"; -import { IndexPage, createElement, Token } from "@xarc/render-context"; - -const Template = ( - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - -
-); - -export default Template; diff --git a/packages/xarc-index-page/test/data/critical.css b/packages/xarc-index-page/test/data/critical.css index 563d20e99..f9e321c19 100644 --- a/packages/xarc-index-page/test/data/critical.css +++ b/packages/xarc-index-page/test/data/critical.css @@ -1,3 +1,7 @@ body { color: green; } + +h1 { + color: red; +} diff --git a/packages/xarc-index-page/test/data/iconstats-no-html.json b/packages/xarc-index-page/test/data/iconstats-no-html.json new file mode 100644 index 000000000..291a345bb --- /dev/null +++ b/packages/xarc-index-page/test/data/iconstats-no-html.json @@ -0,0 +1 @@ +{ "outputFilePrefix": "icons-123/" } diff --git a/packages/xarc-index-page/test/data/iconstats.json b/packages/xarc-index-page/test/data/iconstats.json new file mode 100644 index 000000000..5aabfb090 --- /dev/null +++ b/packages/xarc-index-page/test/data/iconstats.json @@ -0,0 +1,4 @@ +{ + "outputFilePrefix": "icons-123/", + "html": ["icons-123.png", "icons-123/manifest.json", "icons-123/manifest.webapp"] +} diff --git a/packages/xarc-index-page/test/data/selector.js b/packages/xarc-index-page/test/data/selector.js new file mode 100644 index 000000000..ec2fdc864 --- /dev/null +++ b/packages/xarc-index-page/test/data/selector.js @@ -0,0 +1,4 @@ +module.exports = () => ({ + js: "app", + css: "app" +}); diff --git a/packages/xarc-index-page/test/spec/index.spec.ts b/packages/xarc-index-page/test/spec/index.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/xarc-index-page/test/spec/utils.spec.ts b/packages/xarc-index-page/test/spec/utils.spec.ts new file mode 100644 index 000000000..1f660dcc7 --- /dev/null +++ b/packages/xarc-index-page/test/spec/utils.spec.ts @@ -0,0 +1,58 @@ +"use strict"; + +import { describe, it } from "mocha"; +import { resolveChunkSelector, getIconStats, getCriticalCSS } from "../../src/utils"; +import { expect } from "chai"; + +describe("subapp-server utils", () => { + describe("resolveChunkSelector", () => { + it("should return specified chunk selector if the chunk selector file exists", () => { + const selector = resolveChunkSelector({ bundleChunkSelector: "./test/data/selector" }); + expect(selector).to.be.a("function"); + const { js, css } = selector(); + expect(js).to.equal("app"); + expect(css).to.equal("app"); + }); + + it("should return default chunk selector if the bundleChunkSelector does not exists", () => { + const selector = resolveChunkSelector({}); + expect(selector).to.be.a("function"); + const { js, css } = selector(); + expect(js).to.equal("main"); + expect(css).to.equal("main"); + }); + }); + + describe("getIconStats", () => { + it("should load iconStats", () => { + const html = ["icons-123.png", "icons-123/manifest.json", "icons-123/manifest.webapp"]; + const iconStats = getIconStats("./test/data/iconstats.json"); + expect(iconStats).to.equal(html.join("")); + }); + + it("should load iconStats as object if html field not exists", () => { + const iconStats = getIconStats("./test/data/iconstats-no-html.json"); + expect(iconStats).to.be.an("object"); + expect(Object.keys(iconStats).length).to.equal(1); + }); + + it("should return empty string if iconstats.json does not exist", () => { + expect(getIconStats("some path")).to.equal(""); + }); + + it("should return empty string if iconstats file cannot be parsed as js object", () => { + expect(getIconStats("./test/data/selector")).to.equal(""); + }); + }); + + describe("getCriticalCSS", () => { + it("should get criticalCss", () => { + const criticalCss = getCriticalCSS("./test/data/critical.css"); + expect(criticalCss).to.include("h1"); + }); + + it("should return empty string if criticalCss file does not exist", () => { + expect(getCriticalCSS()).to.equal(""); + }); + }); +}); diff --git a/packages/xarc-index-page/test/spec/webapp.spec.ts b/packages/xarc-index-page/test/spec/webapp.spec.ts deleted file mode 100644 index 2df0394e6..000000000 --- a/packages/xarc-index-page/test/spec/webapp.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { expect } from "chai"; -import xstdout = require("xstdout"); -import { describe } from "mocha"; -import * as Webapp from "../../src/Webapp"; - -describe("resolveContent", function () { - it("should require module with relative path", () => { - const f = "./test/data/foo.js"; - expect(Webapp.resolveContent({ module: f }).content).to.equal("hello"); - }); - - it("should log error if resolving content fail", () => { - const intercept = xstdout.intercept(true); - const f = "./test/data/bad-content.js"; - const content = Webapp.resolveContent({ module: f }); - intercept.restore(); - expect(content.content).includes("test/data/bad-content.js failed"); - expect(intercept.stderr.join("")).includes("Error: Cannot find module 'foo-blah'"); - }); - it("should require module", () => { - let mod; - const fooRequire = x => (mod = x); - fooRequire.resolve = x => x; - const f = "test"; - const content = Webapp.resolveContent({ module: f }, fooRequire); - expect(content.content).to.equal(f); - expect(content.fullPath).to.equal(f); - expect(mod).to.equal(f); - }); - it("should require module", () => { - let mod; - const fooRequire = x => (mod = x); - fooRequire.resolve = x => x; - const f = "test"; - const content = Webapp.resolveContent({ module: f }, fooRequire); - expect(content.content).to.equal(f); - expect(content.fullPath).to.equal(f); - expect(mod).to.equal(f); - }); -}); diff --git a/packages/xarc-jsx-renderer/package.json b/packages/xarc-jsx-renderer/package.json index 949c11f39..581404eba 100644 --- a/packages/xarc-jsx-renderer/package.json +++ b/packages/xarc-jsx-renderer/package.json @@ -31,7 +31,7 @@ "@types/sinon-chai": "^3.2.4", "@typescript-eslint/eslint-plugin": "^2.21.0", "@typescript-eslint/parser": "^2.21.0", - "@xarc/module-dev": "^2.1.1", + "@xarc/module-dev": "^2.1.2", "babel-eslint": "^10.1.0", "chai": "^4.2.0", "eslint": "^6.8.0", @@ -106,7 +106,9 @@ }, "fyn": { "dependencies": { - "@xarc/render-context": "../xarc-render-context" - } + "@xarc/render-context": "../xarc-render-context", + "xaa": "../../../xaa" + }, + "devDependencies": {} } } diff --git a/packages/xarc-jsx-renderer/src/Component.ts b/packages/xarc-jsx-renderer/src/Component.ts index 116f0050f..061325a36 100644 --- a/packages/xarc-jsx-renderer/src/Component.ts +++ b/packages/xarc-jsx-renderer/src/Component.ts @@ -1,6 +1,6 @@ /** * @packageDocumentation - * @module index + * @module @xarc/jsx-renderer */ /* eslint-disable filenames/match-regex */ diff --git a/packages/xarc-jsx-renderer/src/JsxRenderer.ts b/packages/xarc-jsx-renderer/src/JsxRenderer.ts index f5fc277dd..7eec16056 100644 --- a/packages/xarc-jsx-renderer/src/JsxRenderer.ts +++ b/packages/xarc-jsx-renderer/src/JsxRenderer.ts @@ -1,11 +1,13 @@ /** * @packageDocumentation - * @module index + * @module @xarc/jsx-renderer */ /* eslint-disable max-statements, max-params, prefer-template, complexity */ -/* eslint-disable comma-dangle, arrow-parens, filenames/match-regex */ +/* eslint-disable filenames/match-regex */ +import * as requireAt from "require-at"; +import * as Path from "path"; import * as assert from "assert"; import * as _ from "lodash"; import { @@ -30,6 +32,7 @@ export class JsxRenderer { private _beforeRenders: any; private _afterRenders: any; private _handlersLookup: any; + private _templateRequire: any; constructor(options: any) { this._options = options; @@ -48,6 +51,7 @@ export class JsxRenderer { options ); this._template = options.template; + this._templateRequire = requireAt(Path.resolve(this._options.templateFullPath || "")); } get insertTokenIds() { @@ -206,8 +210,31 @@ export class JsxRenderer { } } + registerTokenHandler(name, handlerMod, call) { + let mod = handlerMod; + const isStr = typeof mod === "string"; + if (isStr && !name) { + name = this._templateRequire.resolve(mod); + } + + const id = name + (call || ""); + if (this._handlersMap[id]) { + return; + } + if (typeof mod === "string") { + mod = loadTokenModuleHandler(mod, this.templateFullPath, call); + mod = (call && mod[call]) || mod; + } + let handler = mod(this._handlerContext, this); + if (!handler.tokens) { + handler = { name, tokens: handler }; + } + this._handlersMap[id] = handler; + this._handlersLookup = [handler].concat(this._handlersLookup); + } + private _loadTokenHandler(path) { - const mod = loadTokenModuleHandler(path); + const mod = loadTokenModuleHandler(path, this.templateFullPath); return mod(this._handlerContext, this); } diff --git a/packages/xarc-jsx-renderer/src/index.ts b/packages/xarc-jsx-renderer/src/index.ts index c9118e151..325c26b9c 100644 --- a/packages/xarc-jsx-renderer/src/index.ts +++ b/packages/xarc-jsx-renderer/src/index.ts @@ -1,18 +1,19 @@ -/* eslint-disable comma-dangle, arrow-parens, no-param-reassign */ +/* eslint-disable no-param-reassign */ /** * @packageDocumentation - * @module index + * @module @xarc/jsx-renderer */ import { Component } from "./Component"; +import { JsxRenderer } from "./JsxRenderer"; import { Token } from "./tags/Token"; import { IndexPage } from "./tags/IndexPage"; import { Require } from "./tags/Require"; import { Literal } from "./tags/Literal"; -import { JsxRenderer } from "./JsxRenderer"; +import { LoadTokenHandler } from "./tags/LoadTokenHandler"; /** @ignore */ -export { Component, Token, IndexPage, Require, Literal, JsxRenderer }; +export { Component, Token, IndexPage, Require, Literal, JsxRenderer, LoadTokenHandler }; let ELEMENT_ID = 0; @@ -43,7 +44,7 @@ export function createElement(type: any, props: any, ...children: any[]) { id: ELEMENT_ID++, type, children, - props, + props }; const literal = typeof type === "string"; diff --git a/packages/xarc-jsx-renderer/src/process-token.ts b/packages/xarc-jsx-renderer/src/process-token.ts index 84f3a089b..e916228b6 100644 --- a/packages/xarc-jsx-renderer/src/process-token.ts +++ b/packages/xarc-jsx-renderer/src/process-token.ts @@ -1,7 +1,6 @@ /** @ignore */ /** */ -/* eslint-disable max-params */ -/* eslint-disable comma-dangle, arrow-parens */ +/* eslint-disable max-params, max-statements */ import { TOKEN_HANDLER } from "@xarc/render-context"; @@ -33,7 +32,7 @@ export function processToken(props, context, scope, forRequire = false) { } const r = tokenInst[TOKEN_HANDLER](context, tokenInst); - return context.handleTokenResult(tokenInst.id, r, (err) => { + return context.handleTokenResult(tokenInst.id, r, err => { if (insertTokenIds) { scope.output.add(``); } diff --git a/packages/xarc-jsx-renderer/src/tags/IndexPage.ts b/packages/xarc-jsx-renderer/src/tags/IndexPage.ts index 3c365e080..555bc858c 100644 --- a/packages/xarc-jsx-renderer/src/tags/IndexPage.ts +++ b/packages/xarc-jsx-renderer/src/tags/IndexPage.ts @@ -1,6 +1,6 @@ /** * @packageDocumentation - * @module index + * @module @xarc/jsx-renderer */ /* eslint-disable filenames/match-regex */ diff --git a/packages/xarc-jsx-renderer/src/tags/Literal.ts b/packages/xarc-jsx-renderer/src/tags/Literal.ts index afd473e5e..b51fb103f 100644 --- a/packages/xarc-jsx-renderer/src/tags/Literal.ts +++ b/packages/xarc-jsx-renderer/src/tags/Literal.ts @@ -1,9 +1,9 @@ /** * @packageDocumentation - * @module index + * @module @xarc/jsx-renderer */ -/* eslint-disable comma-dangle, arrow-parens, filenames/match-regex */ +/* eslint-disable filenames/match-regex */ /* eslint-disable no-unused-vars, no-magic-numbers */ import * as Path from "path"; diff --git a/packages/xarc-jsx-renderer/src/tags/LoadTokenHandler.ts b/packages/xarc-jsx-renderer/src/tags/LoadTokenHandler.ts new file mode 100644 index 000000000..a7fe22aea --- /dev/null +++ b/packages/xarc-jsx-renderer/src/tags/LoadTokenHandler.ts @@ -0,0 +1,47 @@ +/** + * @packageDocumentation + * @module @xarc/jsx-renderer + */ +/* eslint-disable filenames/match-regex */ + +import { JsxRenderer } from "../JsxRenderer"; + +/** + * Load and register classic template token handler for the JSX template. + * After loading the handler, its tokens can be used in the JSX template + * + * Usage: (*This is a JSX tag, don't call the function directly.*) + * + * - With function: ` {...}}` + * - Load from source file: `` + * - If the handler string starts with `"."` then it will be resolved to the full + * path relative to the template's location, else it's a module for require. + * - The full path of the handler will be used as `name`, but you can provide + * a name prop if desired. + * + * + * + * Example: + * + * ```js + * import { IndexPage, Token, LoadTokenHandler } from "@xarc/jsx-renderer" + * + * export const Template = () => ( + * { + * return { + * TOKEN1: context => { + * return "result from TOKEN1" + * } + * } + * }} /> + * + * ) + * ``` + * + * @param props - JSX tag props + * @param context - rendering context + */ +export const LoadTokenHandler = (props, context) => { + const renderer: JsxRenderer = context.asyncTemplate; + renderer.registerTokenHandler(props.name, props.handler, props.call); +}; diff --git a/packages/xarc-jsx-renderer/src/tags/Require.ts b/packages/xarc-jsx-renderer/src/tags/Require.ts index d30588882..f0b5767c2 100644 --- a/packages/xarc-jsx-renderer/src/tags/Require.ts +++ b/packages/xarc-jsx-renderer/src/tags/Require.ts @@ -1,6 +1,6 @@ /** * @packageDocumentation - * @module index + * @module @xarc/jsx-renderer */ /* eslint-disable filenames/match-regex */ diff --git a/packages/xarc-jsx-renderer/src/tags/Token.ts b/packages/xarc-jsx-renderer/src/tags/Token.ts index 972bdecad..e5f80a13c 100644 --- a/packages/xarc-jsx-renderer/src/tags/Token.ts +++ b/packages/xarc-jsx-renderer/src/tags/Token.ts @@ -1,6 +1,6 @@ /** * @packageDocumentation - * @module index + * @module @xarc/jsx-renderer */ /* eslint-disable filenames/match-regex */ diff --git a/packages/xarc-jsx-renderer/src/utils.ts b/packages/xarc-jsx-renderer/src/utils.ts index 7e5739ab5..892a82807 100644 --- a/packages/xarc-jsx-renderer/src/utils.ts +++ b/packages/xarc-jsx-renderer/src/utils.ts @@ -1,7 +1,5 @@ /** @ignore */ /** */ -/* eslint-disable comma-dangle, arrow-parens */ - export function expandProps(props, context) { let s = ""; @@ -38,6 +36,6 @@ export const omittedCloseTags = { param: true, source: true, track: true, - wbr: true, + wbr: true // NOTE: menuitem's close tag should be omitted, but that causes problems. }; diff --git a/packages/xarc-jsx-renderer/test/fixtures/token-handler.js b/packages/xarc-jsx-renderer/test/fixtures/token-handler.js index 5d93eb256..0175532ab 100644 --- a/packages/xarc-jsx-renderer/test/fixtures/token-handler.js +++ b/packages/xarc-jsx-renderer/test/fixtures/token-handler.js @@ -65,3 +65,13 @@ module.exports = () => { } }; }; + +module.exports.handler2 = () => { + return { + tokens: { + TOKEN_HANDLER2: () => { + return "test token handler2"; + } + } + }; +}; diff --git a/packages/xarc-jsx-renderer/test/jsx-templates/index-intercept.jsx b/packages/xarc-jsx-renderer/test/jsx-templates/index-intercept.jsx index a827a4a63..53d4d49f9 100644 --- a/packages/xarc-jsx-renderer/test/jsx-templates/index-intercept.jsx +++ b/packages/xarc-jsx-renderer/test/jsx-templates/index-intercept.jsx @@ -1,6 +1,6 @@ /* @jsx createElement */ -const { IndexPage, createElement, Token, Require } = require("../../lib/jsx"); +const { IndexPage, createElement, Token } = require("../../src"); const Test = (props, context) => { context.intercept({ diff --git a/packages/xarc-jsx-renderer/test/jsx-templates/test-load-token-handler.tsx b/packages/xarc-jsx-renderer/test/jsx-templates/test-load-token-handler.tsx new file mode 100644 index 000000000..2bcf8ddf3 --- /dev/null +++ b/packages/xarc-jsx-renderer/test/jsx-templates/test-load-token-handler.tsx @@ -0,0 +1,35 @@ +/* @jsx createElement */ + +import { + IndexPage, + createElement, + Token, + Require, + Literal, + Component, + LoadTokenHandler +} from "../../src"; + +const Template = () => { + return ( + + { + return { + FOO: () => "this is a test" + }; + }} + /> + + + + + {/* Test re-entry */} + + + + ); +}; + +export default Template; diff --git a/packages/xarc-jsx-renderer/test/jsx-templates/test1.tsx b/packages/xarc-jsx-renderer/test/jsx-templates/test1.tsx index e26864b26..19b37f083 100644 --- a/packages/xarc-jsx-renderer/test/jsx-templates/test1.tsx +++ b/packages/xarc-jsx-renderer/test/jsx-templates/test1.tsx @@ -26,7 +26,7 @@ class TestComponent1 extends Component { } function AsyncComponent(props, context, scope) { - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(() => { scope.output.add(`${props.indent}async component ${props.key}\n`); resolve(
async component children: {props.children}
); diff --git a/packages/xarc-jsx-renderer/test/spec/LoadTokenHandler.spec.js b/packages/xarc-jsx-renderer/test/spec/LoadTokenHandler.spec.js new file mode 100644 index 000000000..8d0ab7682 --- /dev/null +++ b/packages/xarc-jsx-renderer/test/spec/LoadTokenHandler.spec.js @@ -0,0 +1 @@ +/* eslint-disable filenames/match-regex */ diff --git a/packages/xarc-jsx-renderer/test/spec/jsx-renderer.spec.tsx b/packages/xarc-jsx-renderer/test/spec/jsx-renderer.spec.tsx index 8c7371656..725af4b96 100644 --- a/packages/xarc-jsx-renderer/test/spec/jsx-renderer.spec.tsx +++ b/packages/xarc-jsx-renderer/test/spec/jsx-renderer.spec.tsx @@ -16,6 +16,7 @@ import Template7 from "../jsx-templates/test7"; import Template8 from "../jsx-templates/test8"; import Template9 from "../jsx-templates/test9"; import Template91 from "../jsx-templates/test91"; +import TemplateLoadTokenHandler from "../jsx-templates/test-load-token-handler"; describe("IndexPage", function () { it("should have static memoize", () => { @@ -30,7 +31,7 @@ describe("Jsx Renderer", function () { insertTokenIds: true, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test1")), template: Template, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); expect(renderer.getTokenInst({ props: { _id: "blah" } })).to.equal(undefined); }); @@ -40,7 +41,7 @@ describe("Jsx Renderer", function () { insertTokenIds: true, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test1")), template: Template, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); renderer.initializeRenderer(); renderer.initializeRenderer(true); @@ -65,7 +66,7 @@ describe("Jsx Renderer", function () {
), - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); renderer.initializeRenderer(); return renderer.render({}).then(context => { @@ -115,7 +116,7 @@ describe("Jsx Renderer", function () { ), - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); renderer.initializeRenderer(); @@ -151,7 +152,7 @@ describe("Jsx Renderer", function () { insertTokenIds: true, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test1")), template: Template, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); const verify = context => { @@ -161,7 +162,7 @@ describe("Jsx Renderer", function () { .map(x => x.trimRight()) .join("\n"); - expect(r).equal(test1ExpectedOutput); + expect(r).contains(test1ExpectedOutput); }; renderer.initializeRenderer(); @@ -176,7 +177,7 @@ describe("Jsx Renderer", function () { it("should handle failure in nesting async components", async () => { const renderer = new JsxRenderer({ insertTokenIds: true, - templateFullPath: Path.dirname(require.resolve("../jsx-templates/test2")), + templateFullPath: null, // test passing no templateFullPath so CWD would be used template: Template2, tokenHandlers: "./test/fixtures/token-handler" }); @@ -191,7 +192,7 @@ describe("Jsx Renderer", function () { const renderer = new JsxRenderer({ templateFullPath: Path.dirname(require.resolve("../jsx-templates/test4")), template: Template4, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); renderer.initializeRenderer(); @@ -213,7 +214,7 @@ describe("Jsx Renderer", function () { insertTokenIds: true, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test3")), template: Template3, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); renderer.initializeRenderer(); @@ -235,7 +236,7 @@ describe("Jsx Renderer", function () { insertTokenIds: false, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test5")), template: Template5, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); const verify = context => { @@ -263,7 +264,7 @@ describe("Jsx Renderer", function () { insertTokenIds: false, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test6")), template: Template6, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); const verify = context => { @@ -288,7 +289,7 @@ World` insertTokenIds: true, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test7")), template: Template7, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); const verify = context => { @@ -310,7 +311,7 @@ World` insertTokenIds: true, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test8")), template: Template8, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); const verify = context => { @@ -332,7 +333,7 @@ World` insertTokenIds: true, templateFullPath: Path.dirname(require.resolve("../jsx-templates/test9")), template: Template9, - tokenHandlers: "./test/fixtures/token-handler" + tokenHandlers: "../fixtures/token-handler" }); const verify = context => { @@ -355,7 +356,7 @@ World` templateFullPath: Path.dirname(require.resolve("../jsx-templates/test91")), template: Template91, tokenHandlers: [ - "./test/fixtures/token-handler", + "../fixtures/token-handler", { name: "test1", beforeRender: () => { @@ -381,4 +382,18 @@ World` verify(context); }); }); + + it("should handle LoadTokenHandler from template", async () => { + const renderer = new JsxRenderer({ + insertTokenIds: true, + templateFullPath: Path.dirname(require.resolve("../jsx-templates/test91")), + template: TemplateLoadTokenHandler + }); + + renderer.initializeRenderer(); + const context = await renderer.render({}); + const result = await context.result; + expect(result).contains("this is a test"); + expect(result).contains("
user-token-1
"); + }); }); diff --git a/packages/xarc-jsx-renderer/test/spec/test1-output.txt b/packages/xarc-jsx-renderer/test/spec/test1-output.txt index 4e211ef73..17b232f29 100644 --- a/packages/xarc-jsx-renderer/test/spec/test1-output.txt +++ b/packages/xarc-jsx-renderer/test/spec/test1-output.txt @@ -17,7 +17,7 @@ my test user-handler-title -token process module subapp-web/lib/init not found +@xarc/render-context: token process module subapp-web/lib/init not found