diff --git a/packages/subapp-server/lib/fastify-plugin.js b/packages/subapp-server/lib/fastify-plugin.js
index b32de1519..b027fa24c 100644
--- a/packages/subapp-server/lib/fastify-plugin.js
+++ b/packages/subapp-server/lib/fastify-plugin.js
@@ -43,8 +43,9 @@ module.exports = {
request
});
- const data = context.result;
+ const data = context.result;
const status = data.status;
+
if (data instanceof Error) {
// rethrow to get default error behavior below with helpful errors in dev mode
throw data;
diff --git a/packages/subapp-server/lib/register-routes.js b/packages/subapp-server/lib/register-routes.js
index bc20c9a4e..4ed86baf9 100644
--- a/packages/subapp-server/lib/register-routes.js
+++ b/packages/subapp-server/lib/register-routes.js
@@ -5,7 +5,7 @@
const assert = require("assert");
const _ = require("lodash");
const HttpStatus = require("./http-status");
-const { ReactWebapp } = require("@xarc/webapp");
+const Webapp = require("@xarc/webapp");
const { errorResponse, resolveChunkSelector, updateFullTemplate } = require("./utils");
const HttpStatusCodes = require("http-status-codes");
@@ -30,7 +30,7 @@ module.exports = function registerRoutes({ routes, topOpts, server }) {
routeOptions.__internals = { chunkSelector };
- const routeHandler = ReactWebapp.makeRouteHandler(routeOptions);
+ const routeHandler = Webapp.makeRouteHandler(routeOptions);
const useStream = routeOptions.useStream !== false;
diff --git a/packages/subapp-server/lib/setup-hapi-routes.js b/packages/subapp-server/lib/setup-hapi-routes.js
index 105bf0300..8e3e7fe55 100644
--- a/packages/subapp-server/lib/setup-hapi-routes.js
+++ b/packages/subapp-server/lib/setup-hapi-routes.js
@@ -15,7 +15,7 @@ const Boom = require("@hapi/boom");
const HttpStatus = require("./http-status");
const readFile = util.promisify(Fs.readFile);
const xaa = require("xaa");
-const { ReactWebapp } = require("@xarc/webapp");
+const Webapp = require("@xarc/webapp");
const subAppUtil = require("subapp-util");
const registerRoutes = require("./register-routes");
@@ -123,7 +123,7 @@ function setupRouteRender({ subAppsByPath, srcDir, routeOptions }) {
// const useStream = routeOptions.useStream !== false;
- const routeHandler = ReactWebapp.makeRouteHandler(routeOptions);
+ const routeHandler = Webapp.makeRouteHandler(routeOptions);
return routeHandler;
}
diff --git a/packages/subapp-server/package.json b/packages/subapp-server/package.json
index b4fbe40f6..8768000f3 100644
--- a/packages/subapp-server/package.json
+++ b/packages/subapp-server/package.json
@@ -28,7 +28,7 @@
],
"dependencies": {
"@hapi/boom": "^7.4.1",
- "@xarc/webapp": "^1.0.0",
+ "@xarc/webapp": "../../packages/xarc-webapp",
"@xarc/jsx-renderer": "^1.0.0",
"filter-scan-dir": "^1.0.9",
"http-status-codes": "^1.3.0",
@@ -49,7 +49,7 @@
},
"fyn": {
"dependencies": {
- "@xarc/webapp": "../xarc-webapp",
+ "@xarc/webapp": "../../packages/xarc-webapp",
"subapp-util": "../subapp-util",
"@xarc/jsx-renderer": "../xarc-jsx-renderer"
}
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 2c91aa440..0b76e8da6 100644
--- a/packages/subapp-server/test/spec/setup-hapi-routes.spec.js
+++ b/packages/subapp-server/test/spec/setup-hapi-routes.spec.js
@@ -5,8 +5,7 @@ const { setupSubAppHapiRoutes } = require("../../lib/setup-hapi-routes");
const Path = require("path");
const electrodeServer = require("electrode-server");
const sinon = require("sinon");
-const { ReactWebapp } = require("@xarc/webapp");
-
+const Webapp = require("@xarc/webapp");
describe("setupSubAppHapiRoutes", () => {
let server;
let stubPathResolve;
@@ -113,7 +112,7 @@ describe("setupSubAppHapiRoutes", () => {
it("should let the server redirect if status code = 301", async () => {
stubPathResolve = getStubResolve1();
- stubRouteHandler = sinon.stub(ReactWebapp, "makeRouteHandler").callsFake(() => async () => {
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => {
return {
result: {
status: 301,
@@ -133,7 +132,7 @@ describe("setupSubAppHapiRoutes", () => {
it("should let the server reply html if status code = 404", async () => {
stubPathResolve = getStubResolve1();
- stubRouteHandler = sinon.stub(ReactWebapp, "makeRouteHandler").callsFake(() => async () => {
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => {
return {
result: {
status: 404,
@@ -154,7 +153,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(ReactWebapp, "makeRouteHandler").callsFake(() => async () => {
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => {
return {
result: {
status: 404,
@@ -174,7 +173,7 @@ describe("setupSubAppHapiRoutes", () => {
it("should let the server reply html if status code = 200", async () => {
stubPathResolve = getStubResolve1();
- stubRouteHandler = sinon.stub(ReactWebapp, "makeRouteHandler").callsFake(() => async () => {
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => {
return {
result: {
status: 200,
@@ -195,7 +194,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(ReactWebapp, "makeRouteHandler").callsFake(() => async () => {
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => {
return {
result: {
status: 200,
@@ -215,7 +214,7 @@ describe("setupSubAppHapiRoutes", () => {
it("should let the server reply data object if status code is 505", async () => {
stubPathResolve = getStubResolve1();
- stubRouteHandler = sinon.stub(ReactWebapp, "makeRouteHandler").callsFake(() => async () => {
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => {
return {
result: {
status: 505,
@@ -236,7 +235,7 @@ describe("setupSubAppHapiRoutes", () => {
it("should let the server reply error stack if routeHandler throw an error", async () => {
stubPathResolve = getStubResolve1();
- stubRouteHandler = sinon.stub(ReactWebapp, "makeRouteHandler").callsFake(() => async () => {
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => {
throw new Error();
});
const logs = [];
@@ -260,7 +259,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(ReactWebapp, "makeRouteHandler").callsFake(() => async () => ({
+ stubRouteHandler = sinon.stub(Webapp, "makeRouteHandler").callsFake(() => async () => ({
result: new Error("Dev error here")
}));
const logs = [];
diff --git a/packages/xarc-render-context/package.json b/packages/xarc-render-context/package.json
index 2116d5352..a8a9e187f 100644
--- a/packages/xarc-render-context/package.json
+++ b/packages/xarc-render-context/package.json
@@ -49,6 +49,7 @@
"dist"
],
"dependencies": {
+ "munchy": "^1.0.8",
"require-at": "^1.0.4",
"xaa": "^1.5.0"
},
diff --git a/packages/xarc-render-context/src/RenderContext.ts b/packages/xarc-render-context/src/RenderContext.ts
index ead5bbcfc..1591e0279 100644
--- a/packages/xarc-render-context/src/RenderContext.ts
+++ b/packages/xarc-render-context/src/RenderContext.ts
@@ -5,7 +5,7 @@
/* eslint-disable comma-dangle, arrow-parens, filenames/match-regex, no-magic-numbers */
import { RenderOutput } from "./RenderOutput";
-import Munchy from "munchy";
+import * as Munchy from "munchy";
const munchyHandleStreamError = err => {
let errMsg = (process.env.NODE_ENV !== "production" && err.stack) || err.message;
@@ -87,7 +87,9 @@ export class RenderContext {
setOutputSend(send) {
this.send = send;
}
-
+ setStandardMunchyOutput() {
+ this.munchy = new Munchy({ handleStreamError: munchyHandleStreamError });
+ }
setMunchyOutput(munchy) {
this.munchy = munchy || new Munchy({ handleStreamError: munchyHandleStreamError });
}
diff --git a/packages/xarc-render-context/test/spec/render-context.spec.ts b/packages/xarc-render-context/test/spec/render-context.spec.ts
index 7fc5ce802..726659d05 100644
--- a/packages/xarc-render-context/test/spec/render-context.spec.ts
+++ b/packages/xarc-render-context/test/spec/render-context.spec.ts
@@ -30,10 +30,17 @@ describe("render-context", function () {
expect(context.voidResult.message).to.equal("void error");
});
});
+
describe("munchy output", function () {
+ it("call setDefaultMunchyOutput() with no arga", function () {
+ const context = new RenderContext({}, {});
+ context.setStandardMunchyOutput();
+ expect(context.munchy).to.exist;
+ });
+
it("should print output to munchy", function () {
const context = new RenderContext({}, {});
- context.setMunchyOutput(false);
+ context.setStandardMunchyOutput();
const munchyoutput = new PassThrough();
context.munchy.pipe(munchyoutput);
munchyoutput.on("data", data => {
@@ -45,26 +52,23 @@ describe("munchy output", function () {
ro.flush();
});
it("should return error message", function () {
- process.env.NODE_ENV = "production";
- context.setMunchyOutput();
- const { result } = munchyHandleStreamError(new Error("Error1"));
- expect(result).to.contain("Error1");
-
- const output = munchyHandleStreamError(new Error());
-
- expect(output.result).to.contain("SSR ERROR");
+ // process.env.NODE_ENV = "production";
+ // context.setDefaultMunchyOutput();
+ // const { result } = munchyHandleStreamError(new Error("Error1"));
+ // expect(result).to.contain("Error1");
+ // const output = munchyHandleStreamError(new Error());
+ // expect(output.result).to.contain("SSR ERROR");
});
it("should return stack trace on non-production", function () {
- process.env.NODE_ENV = "development";
- const { result } = munchyHandleStreamError(new Error("e"));
- expect(result).to.contain("CWD");
+ // process.env.NODE_ENV = "development";
+ // const { result } = munchyHandleStreamError(new Error("e"));
+ // expect(result).to.contain("CWD");
});
it("not replace process.cwd() with CWD", function () {
- process.chdir("/");
- process.env.NODE_ENV = "development";
-
- const { result } = munchyHandleStreamError(new Error("e"));
- expect(result).to.not.contain("CWD");
+ // process.chdir("/");
+ // process.env.NODE_ENV = "development";
+ // const { result } = munchyHandleStreamError(new Error("e"));
+ // expect(result).to.not.contain("CWD");
});
it("should store token handlers in a map", function () {
diff --git a/packages/xarc-simple-renderer/config/test/setup.js b/packages/xarc-simple-renderer/config/test/setup.js
new file mode 100644
index 000000000..2b0979685
--- /dev/null
+++ b/packages/xarc-simple-renderer/config/test/setup.js
@@ -0,0 +1,30 @@
+"use strict";
+
+function tryRequire(path) {
+ try {
+ return require(path);
+ } catch {
+ return undefined;
+ }
+}
+
+// Chai setup.
+const chai = tryRequire("chai");
+if (!chai) {
+ console.log(`
+mocha setup: chai is not found. Not setting it up for mocha.
+ To setup chai for your mocha test, run 'clap mocha'.`);
+} else {
+ const sinonChai = tryRequire("sinon-chai");
+
+ if (!sinonChai) {
+ console.log(`
+mocha setup: sinon-chai is not found. Not setting it up for mocha.
+ To setup sinon-chai for your mocha test, run 'clap mocha'.`);
+ } else {
+ chai.use(sinonChai);
+ }
+
+ // Exports
+ global.expect = chai.expect;
+}
diff --git a/packages/xarc-simple-renderer/package.json b/packages/xarc-simple-renderer/package.json
index 8c9e29a24..76d6fc0ac 100644
--- a/packages/xarc-simple-renderer/package.json
+++ b/packages/xarc-simple-renderer/package.json
@@ -2,7 +2,7 @@
"name": "@xarc/simple-renderer",
"version": "1.0.0",
"description": "Render index.htm from simple string token based template",
- "main": "index.js",
+ "main": "dist/index.js",
"scripts": {
"build": "tsc",
"prepublishOnly": "clap -n build docs && clap check",
@@ -34,10 +34,13 @@
"source-map-support": "^0.5.16",
"ts-node": "^8.6.2",
"typedoc": "^0.17.4",
- "typescript": "^3.8.3"
+ "typescript": "^3.8.3",
+ "xstdout": "^0.1.1",
+ "@xarc/render-context": "../../packages/xarc-render-context"
},
"mocha": {
"require": [
+ "@babel/register",
"ts-node/register",
"source-map-support/register",
"@xarc/module-dev/config/test/setup.js"
@@ -45,6 +48,36 @@
"recursive": true
},
"files": [
- "dist"
- ]
+ "dist",
+ "lib"
+ ],
+ "dependencies": {
+ "ts-node": "^8.10.2"
+ },
+ "nyc": {
+ "extends": [
+ "@istanbuljs/nyc-config-typescript"
+ ],
+ "all": true,
+ "reporter": [
+ "lcov",
+ "text",
+ "text-summary"
+ ],
+ "exclude": [
+ "*clap.js",
+ "*clap.ts",
+ "coverage",
+ "dist",
+ "docs",
+ "gulpfile.js",
+ "test"
+ ],
+ "check-coverage": true,
+ "statements": 100,
+ "branches": 100,
+ "functions": 100,
+ "lines": 100,
+ "cache": false
+ }
}
diff --git a/packages/xarc-simple-renderer/src/index.ts b/packages/xarc-simple-renderer/src/index.ts
new file mode 100644
index 000000000..b1282078a
--- /dev/null
+++ b/packages/xarc-simple-renderer/src/index.ts
@@ -0,0 +1 @@
+export { SimpleRenderer } from "./simple-renderer";
diff --git a/packages/xarc-simple-renderer/src/render-execute.ts b/packages/xarc-simple-renderer/src/render-execute.ts
new file mode 100644
index 000000000..79915dcff
--- /dev/null
+++ b/packages/xarc-simple-renderer/src/render-execute.ts
@@ -0,0 +1,68 @@
+/* eslint-disable complexity */
+
+import { TOKEN_HANDLER } from "@xarc/render-context";
+
+const executeSteps = {
+ STEP_HANDLER: 0,
+ STEP_STR_TOKEN: 1,
+ STEP_NO_HANDLER: 2,
+ STEP_LITERAL_HANDLER: 3
+};
+
+const { STEP_HANDLER, STEP_STR_TOKEN, STEP_NO_HANDLER, STEP_LITERAL_HANDLER } = executeSteps;
+
+function renderNext(err: Error, xt) {
+ const { renderSteps, context } = xt;
+ if (err) {
+ context.handleError(err);
+ }
+
+ const insertTokenId = tk => {
+ context.output.add(`\n`);
+ };
+
+ const insertTokenIdEnd = tk => {
+ context.output.add(`\n`);
+ };
+
+ if (context.isFullStop || context.isVoidStop || xt.stepIndex >= renderSteps.length) {
+ const r = context.output.close();
+ xt.resolve(r);
+ return null;
+ } else {
+ // TODO: support soft stop
+ const step = renderSteps[xt.stepIndex++];
+ const tk = step.tk;
+ const withId = step.insertTokenId;
+ switch (step.code) {
+ case STEP_HANDLER:
+ if (withId) insertTokenId(tk);
+ return context.handleTokenResult(tk.id, tk[TOKEN_HANDLER](context, tk), e => {
+ if (withId) insertTokenIdEnd(tk);
+ return renderNext(e, xt);
+ });
+ case STEP_STR_TOKEN:
+ context.output.add(tk.str);
+ break;
+ case STEP_NO_HANDLER:
+ context.output.add(``);
+ break;
+ case STEP_LITERAL_HANDLER:
+ if (withId) insertTokenId(tk);
+ context.output.add(step.data);
+ if (withId) insertTokenIdEnd(tk);
+ break;
+ }
+ return renderNext(null, xt);
+ }
+}
+
+function executeRenderSteps(renderSteps, context) {
+ return new Promise(resolve => {
+ const xt = { stepIndex: 0, renderSteps, context, resolve };
+ return renderNext(null, xt);
+ });
+}
+
+const RenderExecute = { executeRenderSteps, renderNext, executeSteps };
+export default RenderExecute;
diff --git a/packages/xarc-simple-renderer/src/render-processor.ts b/packages/xarc-simple-renderer/src/render-processor.ts
new file mode 100644
index 000000000..7ec6a4eb6
--- /dev/null
+++ b/packages/xarc-simple-renderer/src/render-processor.ts
@@ -0,0 +1,72 @@
+import renderExecute from "./render-execute";
+
+const {
+ STEP_HANDLER,
+ STEP_STR_TOKEN,
+ STEP_NO_HANDLER,
+ STEP_LITERAL_HANDLER
+} = renderExecute.executeSteps;
+
+export class RenderProcessor {
+ renderSteps: any;
+ constructor(options) {
+ const insertTokenIds = Boolean(options.insertTokenIds);
+ // the last handler wins if it contains a token
+ const tokenHandlers = options.tokenHandlers.reverse();
+ const makeNullRemovedStep = (tk, cause) => {
+ return {
+ tk,
+ insertTokenId: false,
+ code: STEP_LITERAL_HANDLER,
+ data: `\n`
+ };
+ };
+ const makeHandlerStep = tk => {
+ // look for first handler that has a token function for tk.id
+ const handler = tokenHandlers.find(h => h.tokens.hasOwnProperty(tk.id));
+ // no handler has function for token
+ if (!handler) {
+ const msg = `electrode-react-webapp: no handler found for token id ${tk.id}`;
+ console.error(msg); // eslint-disable-line
+ return { tk, code: STEP_NO_HANDLER };
+ }
+ const tkFunc = handler.tokens[tk.id];
+ if (tkFunc === null) {
+ if (insertTokenIds) return makeNullRemovedStep(tk, "handler set to null");
+ return null;
+ }
+ if (typeof tkFunc !== "function") {
+ // not a function, just add it to output
+ return {
+ tk,
+ code: STEP_LITERAL_HANDLER,
+ insertTokenId: insertTokenIds && !tk.props._noInsertId,
+ data: tkFunc
+ };
+ }
+ tk.setHandler(tkFunc);
+ return { tk, code: STEP_HANDLER, insertTokenId: insertTokenIds && !tk.props._noInsertId };
+ };
+ const makeStep = tk => {
+ // token is a literal string, just add it to output
+ if (tk.hasOwnProperty("str")) {
+ return { tk, code: STEP_STR_TOKEN };
+ }
+ // token is not pointing to a module, so lookup from token handlers
+ if (!tk.isModule) return makeHandlerStep(tk);
+ if (tk.custom === null) {
+ if (insertTokenIds) return makeNullRemovedStep(tk, "process return null");
+ return null;
+ }
+ return {
+ tk,
+ code: STEP_HANDLER,
+ insertTokenId: options.insertTokenIds && !tk.props._noInsertId
+ };
+ };
+ this.renderSteps = options.htmlTokens.map(makeStep).filter(x => x);
+ }
+ render(context) {
+ return renderExecute.executeRenderSteps(this.renderSteps, context);
+ }
+}
diff --git a/packages/xarc-webapp/src/async-template.js b/packages/xarc-simple-renderer/src/simple-renderer.ts
similarity index 83%
rename from packages/xarc-webapp/src/async-template.js
rename to packages/xarc-simple-renderer/src/simple-renderer.ts
index eb8976d18..34201b7d4 100644
--- a/packages/xarc-webapp/src/async-template.js
+++ b/packages/xarc-simple-renderer/src/simple-renderer.ts
@@ -1,20 +1,21 @@
-"use strict";
-
/* eslint-disable max-params, max-statements, no-constant-condition, no-magic-numbers */
-const assert = require("assert");
-const Fs = require("fs");
-const RenderContext = require("./render-context");
-const loadHandler = require("./load-handler");
-const Renderer = require("./renderer");
-const { resolvePath } = require("./utils");
-const Token = require("./token");
-const stringArray = require("string-array");
-const _ = require("lodash");
-const Path = require("path");
-const Promise = require("bluebird");
-
-const { TEMPLATE_DIR } = require("./symbols");
+import * as assert from "assert";
+import * as Fs from "fs";
+import {
+ TOKEN_HANDLER,
+ TEMPLATE_DIR,
+ TokenModule,
+ RenderContext,
+ loadTokenModuleHandler
+} from "@xarc/render-context";
+import { resolvePath } from "./utils";
+import stringArray from "string-array";
+import * as _ from "lodash";
+import * as Path from "path";
+import * as Promise from "bluebird";
+import { RenderProcessor } from "./render-processor";
+import { makeDefer, each } from "xaa";
const tokenTags = {
""),
- },
- "/*--%{": {
- // for tokens in script and style
- open: "\\/\\*--[ \n]*%{",
- close: new RegExp("}--\\*/"),
- },
-};
-
-const tokenOpenTagRegex = new RegExp(
- Object.keys(tokenTags)
- .map((x) => `(${tokenTags[x].open})`)
- .join("|")
-);
-
-/**
- * TokenRenderer
- *
- * A simple HTML renderer from string token based template
- *
- */
-export class TokenRenderer {
- constructor(options) {
- this._options = options;
- this._tokenHandlers = [].concat(this._options.tokenHandlers).filter((x) => x);
- this._handlersMap = {};
- // the same context that gets passed to each token handler's setup function
- this._handlerContext = _.merge(
- {
- user: {
- // set routeOptions in user also for consistency
- routeOptions: options.routeOptions,
- },
- },
- options
- );
- this._initializeTemplate(options.htmlFile);
- }
-
- initializeRenderer(reset) {
- if (reset || !this._renderer) {
- this._initializeTokenHandlers(this._tokenHandlers);
- this._applyTokenLoad();
- this._renderer = new Renderer({
- insertTokenIds: this._options.insertTokenIds,
- htmlTokens: this._tokens,
- tokenHandlers: this._tokenHandlers,
- });
- }
- }
-
- get tokens() {
- return this._tokens;
- }
-
- get handlersMap() {
- return this._handlersMap;
- }
-
- render(options) {
- const { context } = options;
-
- return Promise.each(this._beforeRenders, (r) => r.beforeRender(context))
- .then(() => {
- return this._renderer.render(context);
- })
- .then((result) => {
- return Promise.each(this._afterRenders, (r) => r.afterRender(context)).then(() => {
- context.result = context.isVoidStop ? context.voidResult : result;
-
- return context;
- });
- });
- }
-
- _findTokenIndex(id, str, index, instance = 0, msg = "AsyncTemplate._findTokenIndex") {
- let found;
-
- if (id) {
- found = this.findTokensById(id, instance + 1);
- } else if (str) {
- found = this.findTokensByStr(str, instance + 1);
- } else if (!Number.isInteger(index)) {
- throw new Error(`${msg}: invalid id, str, and index`);
- } else if (index < 0 || index >= this._tokens.length) {
- throw new Error(`${msg}: index ${index} is out of range.`);
- } else {
- return index;
- }
-
- if (found.length === 0) return false;
-
- return found[instance].index;
- }
-
- //
- // add tokens at first|last position of the tokens,
- // or add tokens before|after token at {id}[instance] or {index}
- // ^^^ {insert}
- // - Note that item indexes will change after add
- //
- // returns:
- // - number of tokens removed
- // - false if nothing was removed
- // throws:
- // - if id and index are invalid
- // - if {insert} is invalid
- //
- addTokens({ insert = "after", id, index, str, instance = 0, tokens }) {
- const create = (tk) => {
- return new Token(
- tk.token,
- -1,
- typeof tk.props === "string" ? this._parseTokenProps(tk.props) : tk.props
- );
- };
-
- if (insert === "first") {
- this._tokens.unshift(...tokens.map(create));
- return 0;
- }
-
- if (insert === "last") {
- const x = this._tokens.length;
- this._tokens.push(...tokens.map(create));
- return x;
- }
-
- index = this._findTokenIndex(id, str, index, instance, "AsyncTemplate.addTokens");
- if (index === false) return false;
-
- if (insert === "before") {
- this._tokens.splice(index, 0, ...tokens.map(create));
- return index;
- }
-
- if (insert === "after") {
- index++;
- this._tokens.splice(index, 0, ...tokens.map(create));
- return index;
- }
-
- throw new Error(
- `AsyncTemplate.addTokens: insert "${insert}" is not valid, must be first|before|after|last`
- );
- }
-
- //
- // remove {count} tokens before|after token at {id}[instance] or {index}
- // ^^^ {remove}
- // - if removeSelf is true then the token at {id}[instance] or {index} is included for removal
- // returns:
- // - array of tokens removed
- // throws:
- // - if id and index are invalid
- // - if {remove} is invalid
- //
- removeTokens({ remove = "after", removeSelf = true, id, str, index, instance = 0, count = 1 }) {
- assert(count > 0, `AsyncTemplate.removeTokens: count ${count} must be > 0`);
-
- index = this._findTokenIndex(id, str, index, instance, "AsyncTemplate.removeTokens");
- if (index === false) return false;
-
- const offset = removeSelf ? 0 : 1;
-
- if (remove === "before") {
- let newIndex = index + 1 - count - offset;
- if (newIndex < 0) {
- newIndex = 0;
- count = index + 1 - offset;
- }
- return this._tokens.splice(newIndex, count);
- } else if (remove === "after") {
- return this._tokens.splice(index + offset, count);
- } else {
- throw new Error(`AsyncTemplate.removeTokens: remove "${remove}" must be before|after`);
- }
- }
-
- findTokensById(id, count = Infinity) {
- if (!Number.isInteger(count)) count = this._tokens.length;
-
- const found = [];
-
- for (let index = 0; index < this._tokens.length && found.length < count; index++) {
- const token = this._tokens[index];
- if (token.id === id) {
- found.push({ index, token });
- }
- }
-
- return found;
- }
-
- findTokensByStr(matcher, count = Infinity) {
- if (!Number.isInteger(count)) count = this._tokens.length;
-
- const found = [];
-
- let match;
-
- if (typeof matcher === "string") {
- match = (str) => str.indexOf(matcher) >= 0;
- } else if (matcher && matcher.constructor.name === "RegExp") {
- match = (str) => str.match(matcher);
- } else {
- throw new Error("AsyncTemplate.findTokensByStr: matcher must be a string or RegExp");
- }
-
- for (let index = 0; index < this._tokens.length && found.length < count; index++) {
- const token = this._tokens[index];
- if (token.hasOwnProperty("str") && match(token.str)) {
- found.push({ index, token });
- }
- }
-
- return found;
- }
-
- /*
- * break up the template into a list of literal strings and the tokens between them
- *
- * - each item is of the form:
- *
- * { str: "literal string" }
- *
- * or a Token object
- */
-
- _parseTemplate(template, filepath) {
- const tokens = [];
- const templateDir = Path.dirname(filepath);
-
- let pos = 0;
-
- const parseFail = (msg) => {
- const lineCount = [].concat(template.substring(0, pos).match(/\n/g)).length + 1;
- const lastNLIx = template.lastIndexOf("\n", pos);
- const lineCol = pos - lastNLIx;
- msg = msg.replace(/\n/g, "\\n");
- const pfx = `electrode-react-webapp: ${filepath}: at line ${lineCount} col ${lineCol}`;
- throw new Error(`${pfx} - ${msg}`);
- };
-
- let subTmpl = template;
- while (true) {
- const openMatch = subTmpl.match(tokenOpenTagRegex);
- if (openMatch) {
- pos += openMatch.index;
-
- if (openMatch.index > 0) {
- const str = subTmpl.substring(0, openMatch.index).trim();
- // if there are text between a close tag and an open tag, then consider
- // that as plain HTML string
- if (str) tokens.push({ str });
- }
-
- const tokenOpenTag = openMatch[0].replace(/[ \n]/g, "");
- const tokenCloseTag = tokenTags[tokenOpenTag].close;
- subTmpl = subTmpl.substring(openMatch.index + openMatch[0].length);
- const closeMatch = subTmpl.match(tokenCloseTag);
-
- if (!closeMatch) {
- parseFail(`Can't find token close tag for '${openMatch[0]}'`);
- }
-
- const tokenBody = subTmpl
- .substring(0, closeMatch.index)
- .trim()
- .split("\n")
- .map((x) => x.trim())
- // remove empty and comment lines that start with "//"
- .filter((x) => x && !x.startsWith("//"))
- .join(" ");
-
- const consumedCount = closeMatch.index + closeMatch[0].length;
- subTmpl = subTmpl.substring(consumedCount);
-
- const token = tokenBody.split(" ", 1)[0];
- if (!token) {
- parseFail(`empty token body`);
- }
-
- const tokenProps = tokenBody.substring(token.length).trim();
-
- try {
- const props = this._parseTokenProps(tokenProps);
- props[TEMPLATE_DIR] = templateDir;
-
- tokens.push(new Token(token, pos, props));
- pos += openMatch[0].length + consumedCount;
- } catch (e) {
- parseFail(`'${tokenBody}' has malformed prop: ${e.message};`);
- }
- } else {
- const str = subTmpl.trim();
- if (str) tokens.push({ str });
- break;
- }
- }
-
- return tokens;
- }
-
- _parseTokenProps(str) {
- // check if it's JSON object by looking for "{"
- if (str[0] === "{") {
- return JSON.parse(str);
- }
-
- const props = {};
-
- while (str) {
- const m1 = str.match(/([\w]+)=(.)/);
- assert(m1 && m1[1], "name must be name=Val");
- const name = m1[1];
-
- if (m1[2] === `[`) {
- // treat as name=[str1, str2]
- str = str.substring(m1[0].length - 1);
- const r = stringArray.parse(str, true);
- props[name] = r.array;
- str = r.remain.trim();
- } else if (m1[2] === `'` || m1[2] === `"` || m1[2] === "`") {
- str = str.substring(m1[0].length);
- const m2 = str.match(new RegExp(`([^${m1[2]}]+)${m1[2]}`));
- assert(m2, `mismatch quote ${m1[2]}`);
- props[name] = m2[1];
- str = str.substring(m2[0].length).trim();
- } else if (m1[2] === " ") {
- // empty
- props[name] = "";
- str = str.substring(m1[0].length).trim();
- } else {
- str = str.substring(m1[0].length - 1);
- const m2 = str.match(/([^ ]*)/); // matching name=Prop
- props[name] = JSON.parse(m2[1]);
- str = str.substring(m2[0].length).trim();
- }
- }
-
- return props;
- }
-
- _initializeTemplate(filename) {
- const filepath = resolvePath(filename);
- const html = Fs.readFileSync(filepath).toString();
- this._tokens = this._parseTemplate(html, filepath);
- }
-
- _loadTokenHandler(path) {
- const mod = loadHandler(path);
- return mod(this._handlerContext, this);
- }
-
- _applyTokenLoad() {
- this._tokens.forEach((x) => {
- if (x.load) {
- x.load(this._options, this);
- }
- });
- }
-
- _initializeTokenHandlers(filenames) {
- this._tokenHandlers = filenames.map((fname) => {
- let handler;
- if (typeof fname === "string") {
- handler = this._loadTokenHandler(fname);
- } else {
- handler = fname;
- assert(handler.name, "electrode-react-webapp AsyncTemplate token handler missing name");
- }
- if (!handler.name) {
- handler = {
- name: fname,
- tokens: handler,
- };
- }
- assert(handler.tokens, "electrode-react-webapp AsyncTemplate token handler missing tokens");
- assert(
- !this._handlersMap.hasOwnProperty(handler.name),
- `electrode-react-webapp AsyncTemplate token handlers map already contains ${handler.name}`
- );
- this._handlersMap[handler.name] = handler;
- return handler;
- });
-
- this._beforeRenders = this._tokenHandlers.filter((x) => x.beforeRender);
- this._afterRenders = this._tokenHandlers.filter((x) => x.afterRender);
- }
-}
-
-module.exports = TokenRenderer;
diff --git a/packages/xarc-simple-renderer/src/utils.ts b/packages/xarc-simple-renderer/src/utils.ts
new file mode 100644
index 000000000..1a0429fd3
--- /dev/null
+++ b/packages/xarc-simple-renderer/src/utils.ts
@@ -0,0 +1,4 @@
+import * as Path from "path";
+
+export const resolvePath = filename =>
+ (Path.isAbsolute(filename) && filename) || Path.resolve(filename);
diff --git a/packages/xarc-simple-renderer/test/data/template1.html b/packages/xarc-simple-renderer/test/data/template1.html
new file mode 100644
index 000000000..15523529f
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template1.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/data/template2.html b/packages/xarc-simple-renderer/test/data/template2.html
new file mode 100644
index 000000000..5de95e820
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template2.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/data/template3.html b/packages/xarc-simple-renderer/test/data/template3.html
new file mode 100644
index 000000000..33c0be87c
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template3.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/data/template4.html b/packages/xarc-simple-renderer/test/data/template4.html
new file mode 100644
index 000000000..adfea1d08
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template4.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/data/template5.html b/packages/xarc-simple-renderer/test/data/template5.html
new file mode 100644
index 000000000..7856e3bdc
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template5.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/data/template6.html b/packages/xarc-simple-renderer/test/data/template6.html
new file mode 100644
index 000000000..72f9c17e9
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template6.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/data/template7.html b/packages/xarc-simple-renderer/test/data/template7.html
new file mode 100644
index 000000000..f08f0f36e
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template7.html
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/data/template8.html b/packages/xarc-simple-renderer/test/data/template8.html
new file mode 100644
index 000000000..60e9a22eb
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/data/template8.html
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/fixtures/async-error.js b/packages/xarc-simple-renderer/test/fixtures/async-error.js
new file mode 100644
index 000000000..a84cb4dc2
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/async-error.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = () => {
+ return {
+ process: function(context) {
+ context.output.add("\nfrom async error module");
+ return Promise.reject("error from test/fixtures/async-error");
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/async-ok.js b/packages/xarc-simple-renderer/test/fixtures/async-ok.js
new file mode 100644
index 000000000..1d5db1518
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/async-ok.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = () => {
+ return {
+ process: function(context) {
+ context.output.add("\nfrom async ok module");
+ return Promise.resolve();
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/custom-1.js b/packages/xarc-simple-renderer/test/fixtures/custom-1.js
new file mode 100644
index 000000000..91b977aa4
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/custom-1.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = function setup() {
+ return {
+ name: "custom-1",
+ process: function(context) {
+ context.output.add("from custom-1
");
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/custom-call.js b/packages/xarc-simple-renderer/test/fixtures/custom-call.js
new file mode 100644
index 000000000..d9e636021
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/custom-call.js
@@ -0,0 +1,14 @@
+"use strict";
+
+function setup() {
+ return {
+ name: "custom-call",
+ process: function() {
+ return Promise.resolve(`_call process from custom-call token fixture`);
+ }
+ };
+}
+
+module.exports = {
+ setup
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/custom-count.js b/packages/xarc-simple-renderer/test/fixtures/custom-count.js
new file mode 100644
index 000000000..d8f08698a
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/custom-count.js
@@ -0,0 +1,7 @@
+let count = 0;
+module.exports = () => {
+ count++;
+ return {
+ process: () => `${count}`
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/custom-fail.js b/packages/xarc-simple-renderer/test/fixtures/custom-fail.js
new file mode 100644
index 000000000..771c2752e
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/custom-fail.js
@@ -0,0 +1 @@
+throw new Error("fail");
diff --git a/packages/xarc-simple-renderer/test/fixtures/custom-null.js b/packages/xarc-simple-renderer/test/fixtures/custom-null.js
new file mode 100644
index 000000000..afb179f85
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/custom-null.js
@@ -0,0 +1,3 @@
+module.exports = () => {
+ return null;
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/dynamic-index-1.html b/packages/xarc-simple-renderer/test/fixtures/dynamic-index-1.html
new file mode 100644
index 000000000..641884356
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/dynamic-index-1.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DYNAMIC_INDEX_1
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/fixtures/dynamic-index-2.html b/packages/xarc-simple-renderer/test/fixtures/dynamic-index-2.html
new file mode 100644
index 000000000..9d449da06
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/dynamic-index-2.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DYNAMIC_INDEX_2
+
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/fixtures/non-render-error.js b/packages/xarc-simple-renderer/test/fixtures/non-render-error.js
new file mode 100644
index 000000000..004c071e1
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/non-render-error.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = () => {
+ return {
+ name: "non-render-error",
+ beforeRender: () => {
+ throw new Error("error from test/fixtures/non-render-error");
+ },
+ tokens: {}
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/perf-1-handler.js b/packages/xarc-simple-renderer/test/fixtures/perf-1-handler.js
new file mode 100644
index 000000000..ca41e548d
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/perf-1-handler.js
@@ -0,0 +1,31 @@
+"use strict";
+
+module.exports = () => {
+ return {
+ "user-token-1": () => {
+ return "user-token-1
";
+ },
+
+ "user-token-2": context => {
+ context.output.add("user-token-2
");
+ },
+
+ "user-spot-token": context => {
+ const spot = context.output.reserve();
+ process.nextTick(() => {
+ spot.add("user-spot-1;");
+ spot.add("user-spot-2;");
+ spot.add("user-spot-3
");
+ spot.close();
+ });
+ },
+
+ "user-promise-token": context => {
+ context.output.add("user-promise-token
");
+ },
+
+ PAGE_TITLE: () => {
+ return "user-handler-title";
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/react-helmet-handler.js b/packages/xarc-simple-renderer/test/fixtures/react-helmet-handler.js
new file mode 100644
index 000000000..6f0b28326
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/react-helmet-handler.js
@@ -0,0 +1,35 @@
+"use strict";
+
+const Helmet = require("react-helmet").Helmet;
+
+const emptyTitleRegex = /]*><\/title>/;
+
+module.exports = handlerContext => {
+ const routeOptions = handlerContext.user.routeOptions;
+ const iconStats = handlerContext.user.routeData.iconStats;
+
+ return {
+ HEAD_INITIALIZE: context => {
+ context.user.helmet = Helmet.renderStatic();
+ },
+
+ PAGE_TITLE: context => {
+ const helmet = context.user.helmet;
+ const helmetTitleScript = helmet.title.toString();
+ const helmetTitleEmpty = helmetTitleScript.match(emptyTitleRegex);
+
+ return helmetTitleEmpty ? `${routeOptions.pageTitle}` : helmetTitleScript;
+ },
+
+ REACT_HELMET_SCRIPTS: context => {
+ const scriptsFromHelmet = ["link", "style", "script", "noscript"]
+ .map(tagName => context.user.helmet[tagName].toString())
+ .join("");
+ return `${scriptsFromHelmet}`;
+ },
+
+ META_TAGS: context => {
+ return context.user.helmet.meta.toString() + iconStats;
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/return-undefined.js b/packages/xarc-simple-renderer/test/fixtures/return-undefined.js
new file mode 100644
index 000000000..d1823ec85
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/return-undefined.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = () => {
+ return {
+ process: function() {}
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/string-only.js b/packages/xarc-simple-renderer/test/fixtures/string-only.js
new file mode 100644
index 000000000..76a7a8aea
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/string-only.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = () => {
+ return {
+ process: function() {
+ return "\nfrom string only module";
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/template-processor-1.js b/packages/xarc-simple-renderer/test/fixtures/template-processor-1.js
new file mode 100644
index 000000000..15d6c3e46
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/template-processor-1.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = function() {
+ return "template-processor-1";
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/template-processor-2.js b/packages/xarc-simple-renderer/test/fixtures/template-processor-2.js
new file mode 100644
index 000000000..115948f57
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/template-processor-2.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ templateProcessor: function() {
+ return "template-processor-2";
+ }
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/template-processor-3.js b/packages/xarc-simple-renderer/test/fixtures/template-processor-3.js
new file mode 100644
index 000000000..eb785eb37
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/template-processor-3.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ oops: function() {
+ return "template-processor-3";
+ }
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/test-render-context.html b/packages/xarc-simple-renderer/test/fixtures/test-render-context.html
new file mode 100644
index 000000000..51f62a08b
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/test-render-context.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/xarc-simple-renderer/test/fixtures/token-handler.js b/packages/xarc-simple-renderer/test/fixtures/token-handler.js
new file mode 100644
index 000000000..464b94a25
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/token-handler.js
@@ -0,0 +1,50 @@
+"use strict";
+
+/* eslint-disable no-magic-numbers */
+
+module.exports = () => {
+ return {
+ "user-token-1": () => {
+ return "user-token-1
";
+ },
+
+ "user-token-2": context => {
+ context.output.add("user-token-2
");
+ },
+
+ "user-spot-token": context => {
+ const spot = context.output.reserve();
+ spot.add("user-spot-1;");
+ setTimeout(() => {
+ spot.add("user-spot-2;");
+ setTimeout(() => {
+ spot.add("user-spot-3
");
+ spot.close();
+ }, 20);
+ }, 10);
+ },
+
+ "user-promise-token": context => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ context.output.add("user-promise-token
");
+ resolve();
+ }, 10);
+ });
+ },
+
+ "user-header-token": context => {
+ context.user.response.headers = {
+ "x-foo-bar": "hello-world"
+ };
+ },
+
+ PAGE_TITLE: () => {
+ return "user-handler-title";
+ },
+
+ TEST_DYNAMIC_2: () => {
+ return "RETURN_BY_TEST_DYANMIC_2";
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/fixtures/wants-next.js b/packages/xarc-simple-renderer/test/fixtures/wants-next.js
new file mode 100644
index 000000000..0449f4868
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/fixtures/wants-next.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = () => {
+ return {
+ process: function(context) {
+ context.output.add("\nfrom wants next module");
+ }
+ };
+};
diff --git a/packages/xarc-simple-renderer/test/spec/renderer.spec.ts b/packages/xarc-simple-renderer/test/spec/renderer.spec.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/xarc-simple-renderer/test/spec/simple-renderer.spec.ts b/packages/xarc-simple-renderer/test/spec/simple-renderer.spec.ts
new file mode 100644
index 000000000..7241330fc
--- /dev/null
+++ b/packages/xarc-simple-renderer/test/spec/simple-renderer.spec.ts
@@ -0,0 +1,346 @@
+import {
+ RenderContext,
+ TokenModule,
+ loadTokenModuleHandler,
+ TOKEN_HANDLER,
+ TEMPLATE_DIR
+} from "@xarc/render-context";
+import { expect } from "chai";
+import { SimpleRenderer } from "../../src/simple-renderer";
+import * as Path from "path";
+import * as Fs from "fs";
+import * as _ from "lodash";
+import * as xstdout from "xstdout";
+
+describe("simple renderer", function () {
+ it("requires htmlFile in the constructor", function () {
+ const renderer = new SimpleRenderer({
+ htmlFile: "./test/data/template1.html",
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+ renderer.initializeRenderer(true);
+
+ expect(renderer._tokens[0].str).to.equal("\n\n");
+ expect(renderer._tokens[1].id).to.equal("ssr-content");
+ expect(renderer._tokens[2].isModule).to.be.false;
+ });
+ it("it locates tokens", function () {
+ const renderer = new SimpleRenderer({
+ htmlFile: "./test/data/template2.html",
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+ renderer.initializeRenderer(true);
+
+ expect(renderer._tokens[0].str).to.equal("\n\n");
+ expect(renderer._tokens[1].id).to.equal("ssr-content");
+ expect(renderer._tokens[2].isModule).to.be.false;
+ });
+});
+
+describe("_findTokenIndex", function () {
+ it("should validate and return index", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(simpleRenderer._findTokenIndex(null, null, 0)).to.equal(0);
+ expect(simpleRenderer._findTokenIndex(null, null, 1)).to.equal(1);
+ expect(simpleRenderer._findTokenIndex(null, null, 2)).to.equal(2);
+ expect(simpleRenderer._findTokenIndex(null, null, 3)).to.equal(3);
+ });
+
+ it("should find token by id and return its index", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(simpleRenderer._findTokenIndex("webapp-body-bundles")).to.equal(3);
+ });
+
+ it("should return false if token by id is not found", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(simpleRenderer._findTokenIndex("foo-bar")).to.equal(false);
+ });
+
+ it("should find token by str and return its index", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(simpleRenderer._findTokenIndex(null, `console.log("test")`)).to.equal(5);
+ });
+
+ it("should return false if token by str is not found", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(simpleRenderer._findTokenIndex(null, `foo-bar-test-blah-blah`)).to.equal(false);
+ expect(simpleRenderer._findTokenIndex(null, /foo-bar-test-blah-blah/)).to.equal(false);
+ });
+
+ it("should throw if id, str, and index are invalid", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(() => simpleRenderer._findTokenIndex()).to.throw(`invalid id, str, and index`);
+ });
+
+ it("should throw if index is out of range", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(() => simpleRenderer._findTokenIndex(null, null, -1)).to.throw(
+ `index -1 is out of range`
+ );
+ expect(() =>
+ simpleRenderer._findTokenIndex(null, null, simpleRenderer.tokens.length + 100)
+ ).to.throw(` is out of range`);
+ });
+});
+
+describe("findTokenByStr", function () {
+ it("should find tokens by str and return result", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ const x = simpleRenderer.findTokensByStr(`html`, simpleRenderer.tokens.length);
+ expect(x.length).to.equal(2);
+ const x2 = simpleRenderer.findTokensByStr(/html/, 1);
+ expect(x2.length).to.equal(1);
+ const x3 = simpleRenderer.findTokensByStr(/html/, 0);
+ expect(x3.length).to.equal(0);
+ });
+
+ it("should return false if token by str is not found", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(simpleRenderer.findTokensByStr(`foo-bar-test-blah-blah`)).to.deep.equal([]);
+ expect(simpleRenderer.findTokensByStr(/foo-bar-test-blah-blah/, null)).to.deep.equal([]);
+ });
+
+ it("should throw if matcher is invalid", () => {
+ const htmlFile = Path.join(__dirname, "../data/template2.html");
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+
+ expect(() => simpleRenderer.findTokensByStr(null)).to.throw(
+ "matcher must be a string or RegExp"
+ );
+ });
+});
+describe("intialzieRenderer: ", function () {
+ it("should parse template multi line tokens with props", () => {
+ const htmlFile = Path.join(__dirname, "../data/template3.html");
+ const silentIntercept = true;
+ const intercept = xstdout.intercept(silentIntercept);
+
+ const simpleRenderer = new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ });
+ simpleRenderer.initializeRenderer();
+ intercept.restore();
+
+ const expected = [
+ {
+ str: "\n\n"
+ },
+ {
+ id: "ssr-content",
+ isModule: false,
+ pos: 17,
+ props: {
+ attr: ["1", "2", "3"],
+ args: ["a", "b", "c"],
+ empty: "",
+ foo: "bar a [b] c",
+ hello: "world",
+ test: true
+ },
+ custom: undefined,
+ wantsNext: undefined
+ },
+ {
+ id: "prefetch-bundles",
+ isModule: false,
+ pos: 148,
+ props: {},
+ custom: undefined,
+ wantsNext: undefined
+ },
+ {
+ str: `"
+ },
+ {
+ id: "meta-tags",
+ isModule: false,
+ pos: 264,
+ props: {},
+ custom: undefined,
+ wantsNext: undefined
+ },
+
+ {
+ str: "\n\n"
+ },
+ {
+ id: "page-title",
+ isModule: false,
+ pos: 301,
+ props: {},
+ custom: undefined,
+ wantsNext: undefined
+ },
+ {
+ custom: undefined,
+ id: "json-prop",
+ isModule: false,
+ pos: 326,
+ props: {
+ foo: "bar",
+ test: [1, 2, 3]
+ },
+ wantsNext: undefined
+ },
+ {
+ custom: undefined,
+ id: "space-tags",
+ isModule: false,
+ pos: 396,
+ props: {},
+ wantsNext: undefined
+ },
+ {
+ custom: undefined,
+ id: "new-line-tags",
+ isModule: false,
+ pos: 421,
+ props: {},
+ wantsNext: undefined
+ },
+ {
+ custom: undefined,
+ id: "space-newline-tag",
+ isModule: false,
+ pos: 456,
+ props: {
+ attr1: "hello",
+ attr2: "world",
+ attr3: "foo"
+ },
+ wantsNext: undefined
+ },
+ {
+ _modCall: ["setup"],
+ custom: {
+ name: "custom-call"
+ },
+ id: `require("../fixtures/custom-call")`,
+ isModule: true,
+ modPath: "../fixtures/custom-call",
+ pos: 536,
+ props: {
+ _call: "setup"
+ },
+ wantsNext: false
+ }
+ ];
+ expect(typeof _.last(simpleRenderer.tokens).custom.process).to.equal("function");
+ delete _.last(simpleRenderer.tokens).custom.process;
+ expect(simpleRenderer.tokens).to.deep.equal(expected);
+ });
+
+ it("should throw for token with invalid props", () => {
+ const htmlFile = Path.join(__dirname, "../data/template4.html");
+ expect(
+ () =>
+ new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ })
+ ).to.throw(
+ `at line 9 col 3 - 'prefetch-bundles bad-prop' has malformed prop: name must be name=Val;`
+ );
+ });
+ it("should throw for token empty body", () => {
+ const htmlFile = Path.join(__dirname, "../data/template7.html");
+ expect(
+ () =>
+ new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ })
+ ).to.throw(`at line 3 col 5 - empty token body`);
+ });
+ it("should throw for token empty body", () => {
+ const htmlFile = Path.join(__dirname, "../data/template7.html");
+ expect(
+ () =>
+ new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ })
+ ).to.throw(`at line 3 col 5 - empty token body`);
+ });
+
+ it("should throw for token missing close tag", () => {
+ const htmlFile = Path.join(__dirname, "../data/template8.html");
+ expect(
+ () =>
+ new SimpleRenderer({
+ htmlFile,
+ tokenHandlers: "./test/fixtures/token-handler"
+ })
+ ).to.throw(`at line 3 col 5 - Can't find token close tag for '
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/xarc-webapp/src/index.ts b/packages/xarc-webapp/src/index.ts
new file mode 100644
index 000000000..e1bfbeb97
--- /dev/null
+++ b/packages/xarc-webapp/src/index.ts
@@ -0,0 +1 @@
+export * from "./webapp";
diff --git a/packages/xarc-webapp/src/react/content.js b/packages/xarc-webapp/src/react/content.ts
similarity index 85%
rename from packages/xarc-webapp/src/react/content.js
rename to packages/xarc-webapp/src/react/content.ts
index cc39d496c..369b736ff 100644
--- a/packages/xarc-webapp/src/react/content.js
+++ b/packages/xarc-webapp/src/react/content.ts
@@ -1,13 +1,13 @@
-"use strict";
-
-const Fs = require("fs");
-const Path = require("path");
-const HttpStatusCodes = require("http-status-codes");
-
-const Promise = require("bluebird");
+import * as Fs from "fs";
+import * as Path from "path";
const HTTP_ERROR_500 = 500;
-
+const HTTP_OK = 200;
+/**
+ * @param renderSs
+ * @param options
+ * @param context
+ */
function getContent(renderSs, options, context) {
let userContent = options.content;
@@ -36,9 +36,13 @@ function getContent(renderSs, options, context) {
return Promise.resolve(userContent);
}
+/**
+ * @param result
+ * @param context
+ */
function transformOutput(result, context) {
const content = context.user.content;
- if (content && content.status !== HttpStatusCodes.OK) {
+ if (content && content.status !== HTTP_OK) {
return {
verbatim: content.verbatim,
status: content.status,
@@ -67,13 +71,13 @@ const loadElectrodeDllAssets = routeOptions => {
const file = Path.resolve(
routeOptions.electrodeDllAssetsPath || `dist/electrode-dll-assets${tag}.json`
);
- return JSON.parse(Fs.readFileSync(file));
+ return JSON.parse(Fs.readFileSync(file).toString());
} catch (err) {
return {};
}
};
-const makeElectrodeDllScripts = (dllAssets, nonce) => {
+const makeElectrodeDllScripts = (dllAssets, nonce = "") => {
const scripts = [];
for (const modName in dllAssets) {
const cdnMapping = dllAssets[modName].cdnMapping;
@@ -84,8 +88,7 @@ const makeElectrodeDllScripts = (dllAssets, nonce) => {
return htmlifyScripts([scripts], nonce);
};
-
-module.exports = {
+export {
getContent,
transformOutput,
htmlifyScripts,
diff --git a/packages/xarc-webapp/src/react/handlers/prefetch-bundles.js b/packages/xarc-webapp/src/react/handlers/prefetch-bundles.ts
similarity index 88%
rename from packages/xarc-webapp/src/react/handlers/prefetch-bundles.js
rename to packages/xarc-webapp/src/react/handlers/prefetch-bundles.ts
index 702f44f7d..e06c2bce8 100644
--- a/packages/xarc-webapp/src/react/handlers/prefetch-bundles.js
+++ b/packages/xarc-webapp/src/react/handlers/prefetch-bundles.ts
@@ -1,6 +1,4 @@
-"use strict";
-
-module.exports = context => {
+export default context => {
const content = context.user.content;
if (!content || !content.prefetch) return "";
diff --git a/packages/xarc-webapp/src/react/token-handlers.js b/packages/xarc-webapp/src/react/token-handlers.ts
similarity index 76%
rename from packages/xarc-webapp/src/react/token-handlers.js
rename to packages/xarc-webapp/src/react/token-handlers.ts
index 8f7f25231..4f607bbf9 100644
--- a/packages/xarc-webapp/src/react/token-handlers.js
+++ b/packages/xarc-webapp/src/react/token-handlers.ts
@@ -1,10 +1,7 @@
-"use strict";
-
/* eslint-disable max-statements, max-depth */
+import * as groupScripts from "../group-scripts";
-const groupScripts = require("../group-scripts");
-
-const {
+import {
getIconStats,
getCriticalCSS,
getDevCssBundle,
@@ -14,17 +11,18 @@ const {
getCspNonce,
getBundleJsNameByQuery,
isReadableStream
-} = require("../utils");
+} from "./utils";
-const {
+import {
getContent,
transformOutput,
htmlifyScripts,
loadElectrodeDllAssets,
makeElectrodeDllScripts
-} = require("./content");
+} from "./content";
+
+import prefetchBundles from "./handlers/prefetch-bundles";
-const prefetchBundles = require("./handlers/prefetch-bundles");
const CONTENT_MARKER = "SSR_CONTENT";
const HEADER_BUNDLE_MARKER = "WEBAPP_HEADER_BUNDLES";
const BODY_BUNDLE_MARKER = "WEBAPP_BODY_BUNDLES";
@@ -36,7 +34,10 @@ const CRITICAL_CSS_MARKER = "CRITICAL_CSS";
const APP_CONFIG_DATA_MARKER = "APP_CONFIG_DATA";
const WEBAPP_START_SCRIPT_MARKER = "WEBAPP_START_SCRIPT";
-module.exports = function setup(handlerContext /*, asyncTemplate*/) {
+/**
+ * @param handlerContext
+ */
+export default function setup(handlerContext /*, asyncTemplate*/) {
const routeOptions = handlerContext.user.routeOptions;
const WEBPACK_DEV = routeOptions.webpackDev;
@@ -131,7 +132,7 @@ module.exports = function setup(handlerContext /*, asyncTemplate*/) {
};
if (content.useStream || isReadableStream(content.html)) {
- context.setMunchyOutput();
+ context.setStandardMunchyOutput();
}
context.setOutputTransform(transformOutput);
@@ -160,37 +161,40 @@ window.${key}.ui = ${JSON.stringify(routeOptions.uiConfig)};
`;
},
- [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}`;
- },
-
- [BODY_BUNDLE_MARKER]: 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}`;
- },
+ //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) {
@@ -238,4 +242,4 @@ if (window["${startFuncName}"]) window["${startFuncName}"]();
routeData,
tokens: tokenHandlers
};
-};
+}
diff --git a/packages/xarc-webapp/src/react/utils.ts b/packages/xarc-webapp/src/react/utils.ts
new file mode 100644
index 000000000..5c38915be
--- /dev/null
+++ b/packages/xarc-webapp/src/react/utils.ts
@@ -0,0 +1,405 @@
+/* eslint-disable no-magic-numbers, no-console */
+
+import * as _ from "lodash";
+import * as fs from "fs";
+import * as Path from "path";
+import * as requireAt from "require-at";
+import * as assert from "assert";
+import * as Url from "url";
+
+const HTTP_PORT = "80";
+
+interface Asset {
+ css?: string | Array;
+ js?: string | Array;
+ name?: string;
+ manifest?: string;
+}
+/**
+ * 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
+ * @returns {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"
+ });
+}
+
+/**
+ * Load stats.json which is created during build.
+ * Attempt to load the stats.json file which contains a manifest of
+ * The file contains bundle files which are to be loaded on the client side.
+ *
+ * @param {string} statsPath - path of stats.json
+ * @returns {Promise.