diff --git a/packages/xarc-render-context/package.json b/packages/xarc-render-context/package.json index 139c13fb0..2116d5352 100644 --- a/packages/xarc-render-context/package.json +++ b/packages/xarc-render-context/package.json @@ -49,7 +49,6 @@ "dist" ], "dependencies": { - "munchy": "^1.0.7", "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 f57bc1779..ead5bbcfc 100644 --- a/packages/xarc-render-context/src/RenderContext.ts +++ b/packages/xarc-render-context/src/RenderContext.ts @@ -169,6 +169,8 @@ export class RenderContext { // if it's a string, buffer, or stream, then add to output if (typeof res === "string" || Buffer.isBuffer(res) || isReadableStream(res)) { this.output.add(res); + } else { + // console.log(" not ignored"); } // ignore other return value types return cb(); diff --git a/packages/xarc-render-context/src/RenderOutput.ts b/packages/xarc-render-context/src/RenderOutput.ts index afeae29e3..c99db785a 100644 --- a/packages/xarc-render-context/src/RenderOutput.ts +++ b/packages/xarc-render-context/src/RenderOutput.ts @@ -20,7 +20,7 @@ export class RenderOutput { constructor(context = null) { this._output = new MainOutput(); this._flushQ = []; - this._context = context || { transform: (x) => x }; + this._context = context || { transform: x => x }; this._result = ""; // will hold final result if context doesn't have send } diff --git a/packages/xarc-render-context/src/TokenModule.ts b/packages/xarc-render-context/src/TokenModule.ts index 3917b621b..420467f7a 100644 --- a/packages/xarc-render-context/src/TokenModule.ts +++ b/packages/xarc-render-context/src/TokenModule.ts @@ -6,9 +6,7 @@ import * as assert from "assert"; import { loadTokenModuleHandler } from "./load-handler"; import { TEMPLATE_DIR, TOKEN_HANDLER } from "./symbols"; - const viewTokenModules = {}; - export class TokenModule { id: any; modPath: any; @@ -46,35 +44,37 @@ export class TokenModule { } // if token is a module, then load it - load(options) { + load(options = {}) { if (!this.isModule || this.custom !== undefined) return; - let tokenMod = viewTokenModules[this.id]; if (tokenMod === undefined) { - tokenMod = loadTokenModuleHandler(this.modPath, this[TEMPLATE_DIR]); + if (this._modCall) { + tokenMod = loadTokenModuleHandler(this.modPath, this[TEMPLATE_DIR], this._modCall[0]); + } else { + tokenMod = loadTokenModuleHandler(this.modPath, this[TEMPLATE_DIR]); + } viewTokenModules[this.id] = tokenMod; } - if (this._modCall) { // call setup function to get an instance - const params = [options || {}, this].concat(this._modCall[1] || []); + const params = [options, this].concat(this._modCall[1] || []); assert( tokenMod[this._modCall[0]], `electrode-react-webapp: _call of token ${this.id} - '${this._modCall[0]}' not found` ); this.custom = tokenMod[this._modCall[0]](...params); } else { - this.custom = tokenMod(options || {}, this); + this.custom = tokenMod(options, this); } + /* if token doesn't provide any code (null) then there's no handler to set for it */ if (this.custom === null) return; assert( this.custom && this.custom.process, `custom token ${this.id} module doesn't have process method` ); - // if process function takes more than one params, then it should take a // next callback so it can do async work, and call next after that's done. this.wantsNext = this.custom.process.length > 1; diff --git a/packages/xarc-render-context/src/load-handler.ts b/packages/xarc-render-context/src/load-handler.ts index 1e6133784..465ffd93d 100644 --- a/packages/xarc-render-context/src/load-handler.ts +++ b/packages/xarc-render-context/src/load-handler.ts @@ -6,6 +6,7 @@ import * as Path from "path"; import * as requireAt from "require-at"; import * as optionalRequire from "optional-require"; +import { isContext } from "vm"; const failLoadTokenModule = (msg: string, err: Error) => { console.error(`error: @xarc/render-context failed to load token process module ${msg}`, err); @@ -14,17 +15,17 @@ const failLoadTokenModule = (msg: string, err: Error) => { }); }; -const notFoundLoadTokenModule = (msg: string) => { - console.error(`error: @xarc/render-context can't find token process module ${msg}`); +const notFoundLoadTokenModule = (msg: string, err: Error) => { + console.error(`error: @xarc/render-context can't find token process module ${msg}`, err); return () => ({ process: () => `\n@xarc/render-context: token process module ${msg} not found\n` }); }; -export const loadTokenModuleHandler = (path: string, templateDir?: string) => { +export const loadTokenModuleHandler = (path: string, templateDir?: string, customCall?: string) => { const tokenMod = optionalRequire(requireAt(Path.resolve(templateDir || "")))(path, { fail: (e: Error) => failLoadTokenModule(path, e), - notFound: () => notFoundLoadTokenModule(path) + notFound: (e: Error) => notFoundLoadTokenModule(path, e) }); if (typeof tokenMod === "function") { return tokenMod; @@ -35,6 +36,10 @@ export const loadTokenModuleHandler = (path: string, templateDir?: string) => { if (tokenMod.default) { return tokenMod.default; } + + if (customCall && tokenMod[customCall]) { + return tokenMod; + } throw new Error( "@xarc/render-context: token module invalid - should export a function directly or as 'default' or 'tokenHandler'" ); diff --git a/packages/xarc-render-context/test/fixtures/custom-call-with-params.ts b/packages/xarc-render-context/test/fixtures/custom-call-with-params.ts new file mode 100644 index 000000000..b7b5f987f --- /dev/null +++ b/packages/xarc-render-context/test/fixtures/custom-call-with-params.ts @@ -0,0 +1,7 @@ +export const prepare = modcall_param => { + return { + process(context) { + return `load ${modcall_param} from ${context.folder} folder`; + } + }; +}; diff --git a/packages/xarc-render-context/test/fixtures/custom-call.ts b/packages/xarc-render-context/test/fixtures/custom-call.ts new file mode 100644 index 000000000..57bab2ec4 --- /dev/null +++ b/packages/xarc-render-context/test/fixtures/custom-call.ts @@ -0,0 +1,7 @@ +export const setup = () => { + return { + process() { + return `hello from custom setup`; + } + }; +}; diff --git a/packages/xarc-render-context/test/fixtures/token-module-null.ts b/packages/xarc-render-context/test/fixtures/token-module-null.ts new file mode 100644 index 000000000..afb179f85 --- /dev/null +++ b/packages/xarc-render-context/test/fixtures/token-module-null.ts @@ -0,0 +1,3 @@ +module.exports = () => { + return null; +}; diff --git a/packages/xarc-render-context/test/spec/index.spec.ts b/packages/xarc-render-context/test/spec/index.spec.ts new file mode 100644 index 000000000..6068026f1 --- /dev/null +++ b/packages/xarc-render-context/test/spec/index.spec.ts @@ -0,0 +1,20 @@ +import { + RenderContext, + BaseOutput, + MainOutput, + SpotOutput, + TEMPLATE_DIR, + TOKEN_HANDLER +} from "../../src/index"; +import { expect } from "chai"; + +describe("index.tsc", function () { + it("should export RenderContext, BaseOutput, MainOutput, SpotOutput etc ", function () { + expect(RenderContext).to.exist; + expect(BaseOutput).to.exist; + expect(MainOutput).to.exist; + expect(SpotOutput).to.exist; + expect(TEMPLATE_DIR).to.exist; + expect(TOKEN_HANDLER).to.exist; + }); +}); diff --git a/packages/xarc-render-context/test/spec/load-handler.spec.ts b/packages/xarc-render-context/test/spec/load-handler.spec.ts index 88c47f928..4f51bd644 100644 --- a/packages/xarc-render-context/test/spec/load-handler.spec.ts +++ b/packages/xarc-render-context/test/spec/load-handler.spec.ts @@ -4,13 +4,21 @@ import * as Path from "path"; describe("loadTokenModuleHandler", function () { it("should handle module is not found", () => { - const tokenMod = loadTokenModuleHandler("foo"); - expect(tokenMod().process()).contains("module foo not found"); + try { + const tokenMod = loadTokenModuleHandler("foo"); + expect(tokenMod().process()).contains("module foo not found"); + } catch (e) { + //expected + } }); it("should handle load module fail", () => { - const tokenMod = loadTokenModuleHandler("./bad-mod", Path.join(__dirname, "../fixtures")); - expect(tokenMod().process()).contains("module ./bad-mod failed to load"); + try { + const tokenMod = loadTokenModuleHandler("./bad-mod", Path.join(__dirname, "../fixtures")); + expect(tokenMod().process()).contains("module ./bad-mod failed to load"); + } catch (e) { + expect(e).to.exist; + } }); it("should load token module handler from default (ESM)", () => { 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 12ebe3e2c..7fc5ce802 100644 --- a/packages/xarc-render-context/test/spec/render-context.spec.ts +++ b/packages/xarc-render-context/test/spec/render-context.spec.ts @@ -1,9 +1,13 @@ "use strict"; -import { RenderContext } from "../../src"; - +import { RenderContext } from "../../src/RenderContext"; +import { RenderOutput } from "../../src/RenderOutput"; import { expect } from "chai"; +import { PassThrough } from "stream"; +import { makeDefer } from "xaa"; +import { Context } from "mocha"; +// import { Fs } from "fs"; describe("render-context", function () { it("should handle setting all stop modes", () => { const context = new RenderContext({}, {}); @@ -15,5 +19,186 @@ describe("render-context", function () { expect(context.isVoidStop).to.equal(true); context.fullStop(); expect(context.isFullStop).to.equal(true); + + expect(context.stop).to.equal(RenderContext.FULL_STOP); + }); + it("should handle error with this.stop() or this.voidStop()", function () { + const context = new RenderContext({}, {}); + context.stop = null; + + context.handleError(new Error("void error")); + expect(context.voidResult.message).to.equal("void error"); + }); +}); +describe("munchy output", function () { + it("should print output to munchy", function () { + const context = new RenderContext({}, {}); + context.setMunchyOutput(false); + const munchyoutput = new PassThrough(); + context.munchy.pipe(munchyoutput); + munchyoutput.on("data", data => { + expect(data.toString()).to.equal("foo"); + }); + + const ro = new RenderOutput(context); + ro.add("foo"); + 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"); + }); + 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"); + }); + 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"); + }); + + it("should store token handlers in a map", function () { + process.env.NODE_ENV = "production"; + const context = new RenderContext( + { + transform: a => a + }, + { + handlersMap: { + title: function (a) { + return `${a}`; + }, + handlerWithToken: { + tokens: ["abc"] + } + } + } + ); + + expect(typeof context.getTokenHandler("title")).to.equal("function"); + expect(context.getTokens("handlerWithToken").length).to.equal(1); + }); +}); + +describe("token handler in render context", function () { + it("should transform output based on transform function", function () { + const context = new RenderContext({}, {}); + + context.setOutputTransform(output => { + return output.replace("\n", "
"); + }); + expect(context.transform("\n new line ")).to.equal("
new line "); + }); + + it("should keep store status", function () { + const context = new RenderContext({}, {}); + + context.status = "green"; + expect(context.status).to.equal("green"); + }); + + it("should allow users to intercept handling of the rendering flow", function () { + try { + const context = new RenderContext({}, {}); + + context.intercept({ + responseHandler: resp => resp + }); + } catch (e) { + expect(e.message).to.equal("electrode-react-webapp: render-context - user intercepted"); + } + }); + + it("should send result to OutputSend function", function () { + let receivedResult = ""; + const send = x => { + receivedResult += x; + }; + const context = new RenderContext({}, {}); + + context.setOutputSend(send); + + const ro = new RenderOutput(context); + ro.add("hello"); + ro.add(" world"); + ro.flush(); + expect(receivedResult).to.equal("hello world"); + }); + it("should handle token result with string", function () { + const context = new RenderContext({}, {}); + let x; + context.send = _x => (x = _x); + + context.handleTokenResult(1, "stringOutput", err => { + expect(err).to.be.undefined; + }); + context.output.flush(); + expect(x).to.equal("stringOutput"); + }); + + it("should handle token results with buffer as input", function () { + let received = ""; + const context = new RenderContext({}, {}); + + context.output = { + add: buf => (received += buf.toString("utf8")) + }; + context.handleTokenResult(1, Buffer.from("hello world", "utf8"), err => { + expect(err).to.be.undefined; + }); + expect(received).to.equal("hello world"); + expect(context.transform("s")).to.equal("s"); + }); + + it("should handle token results with readable stream as input", function () { + const readableStream = new PassThrough(); + const context = new RenderContext({}, {}); + + context.output = { + add: rstream => { + rstream.on("data", data => { + expect(data.toString("ascii")).to.equal("hello"); + }); + } + }; + context.handleTokenResult(1, readableStream, err => { + expect(err).to.be.undefined; + }); + // readableStream.setEncoding("utf-8"); + readableStream.write("hello", "ascii"); // + readableStream.end(); + }); + + it("should handle token result with promise", async function () { + const defer = makeDefer(); + const context = new RenderContext({}, {}); + + // const done = () => defer && defer.resolve("foo"); + context.output = { + add: outcome => { + expect(outcome).to.equal("foo"); + } + }; + context.handleTokenResult(1, defer.promise, err => { + expect(err).to.be.undefined; + }); + await defer.done(null, "foo"); + }); + it("should ignore other types", function () { + const context = new RenderContext({}, {}); + + context.handleTokenResult(1, false, err => { + expect(err).to.be.undefined; + }); }); }); diff --git a/packages/xarc-render-context/test/spec/render-output.spec.ts b/packages/xarc-render-context/test/spec/render-output.spec.ts index 495b9f5ee..4fdd8b8e0 100644 --- a/packages/xarc-render-context/test/spec/render-output.spec.ts +++ b/packages/xarc-render-context/test/spec/render-output.spec.ts @@ -7,12 +7,12 @@ import * as Munchy from "munchy"; import * as streamToArray from "stream-to-array"; import { expect } from "chai"; - +import { makeDefer } from "xaa"; describe("render-output", function () { it("should flush simple string", () => { let text; const context = { - send: (x) => (text = x), + send: x => (text = x) }; const ro = new RenderOutput(context); ro.add("hello world"); @@ -25,7 +25,7 @@ describe("render-output", function () { it("should flush multiple strings", () => { let text; const context = { - send: (x) => (text = x), + send: x => (text = x) }; const ro = new RenderOutput(context); @@ -51,18 +51,18 @@ describe("render-output", function () { it("should wait on flush when there's pending", () => { const context: any = { - text: undefined, + text: undefined }; - context.send = (x) => (context.text = x); + context.send = x => (context.text = x); const ro = new RenderOutput(context); testFlushPending(ro, context); }); it("should continue output after flush", () => { const context: any = { - text: undefined, + text: undefined }; - context.send = (x) => (context.text = x); + context.send = x => (context.text = x); const ro = new RenderOutput(context); testFlushPending(ro, context); context.text = undefined; @@ -72,7 +72,7 @@ describe("render-output", function () { it("should handle multiple spots", () => { let text = ""; const context: any = { - send: (x) => (text += x), + send: x => (text += x) }; const ro = new RenderOutput(context); ro.add("hello world"); @@ -98,16 +98,16 @@ describe("render-output", function () { ); }); - it("should handle multiple buffer and stream data", (done) => { + it("should handle multiple buffer and stream data", done => { const context: any = { munchy: new Munchy(), - transform: (a) => a, + transform: a => a }; streamToArray(context.munchy, (err, arr) => { if (err) return done(err); try { - expect(arr.map((x) => x.toString())).to.deep.equal([ + expect(arr.map(x => x.toString())).to.deep.equal([ "hello world", "foo bar", "spot1 123", @@ -119,7 +119,7 @@ describe("render-output", function () { "spot2 123", "spot2 456", "spot2 a buffer", - "closing", + "closing" ]); return done(); } catch (err2) { @@ -167,14 +167,14 @@ describe("render-output", function () { spot1.add("789"); ro.add("closing"); spot1.close(); - return ro.close().then((result) => { + return ro.close().then(result => { expect(result).to.equal( "hello worldfoo barspot1 123spot1 abc789after spot1baz1spot2 123456closing" ); }); }); - it("should not munch if no items", (done) => { + it("should not munch if no items", done => { const ro = new RenderOutput(); ro._output.sendToMunchy(null, done); }); @@ -201,4 +201,30 @@ describe("render-output", function () { ro.add(item); expect(() => ro.flush()).to.throw("unable to stringify item of type object"); }); + it("should re-throw error in _finish()", async () => { + const ro2 = new RenderOutput({ + munchy: null, + transform: () => { + throw new Error("new Error"); + } + }); + ro2._defer = makeDefer(); + ro2.add("item"); + try { + await ro2._finish(); + } catch (e) { + expect(e.message).to.equal("new Error"); + } + ro2._defer = null; + ro2._context.munchy = { + munch: () => { + throw new Error("new error2"); + } + }; + try { + await ro2._finish(); + } catch (e) { + expect(e.message).to.equal("new error2"); + } + }); }); diff --git a/packages/xarc-render-context/test/spec/token-module.spec.ts b/packages/xarc-render-context/test/spec/token-module.spec.ts new file mode 100644 index 000000000..eb98ab384 --- /dev/null +++ b/packages/xarc-render-context/test/spec/token-module.spec.ts @@ -0,0 +1,157 @@ +import { TokenModule, loadTokenModuleHandler } from "../../src"; +import { expect } from "chai"; +import * as Path from "path"; +import { TEMPLATE_DIR, TOKEN_HANDLER } from "../../src/index"; + +const templateDir = Path.join(__dirname, "../fixtures"); + +describe("TokenModule ", function () { + it("should have laoded symbols TEMPLATE_DIR TOKEN_HANDLER", function () { + expect(TOKEN_HANDLER).to.not.be.undefined; + expect(TEMPLATE_DIR).to.not.be.undefined; + }); + + it("should store id, pos, templateDir as props", function () { + const tk = new TokenModule("test", 0, {}, templateDir); + expect(tk.id).to.equal("test"); + expect(tk.pos).to.equal(0); + expect(tk[TEMPLATE_DIR]).to.equal(templateDir); + expect(tk[TOKEN_HANDLER]).to.be.null; + }); + + it("not a module and custom is defined", function () { + const tk = new TokenModule("custom", 0, { _call: "setup" }, templateDir); + tk.load({}); + expect(tk[TOKEN_HANDLER]).to.be.null; + // expect(Object.keys(tk[TOKEN_HANDLER]).length).to.equal(0); + }); + + it("should initialize null props to empty object", function () { + const tk = new TokenModule("test", 0, null, templateDir); + expect(tk.props).to.deep.equal({}); + }); + + it("should set template dir to props[TEMPLATE_DIR] when exists ", function () { + const props = {}; + props[TEMPLATE_DIR] = templateDir; + const tk = new TokenModule("test", 0, props, null); + + expect(tk[TEMPLATE_DIR]).to.equal(templateDir); //deep.equal({}); + }); + + it("should use process.cwd() as default templatedir ", function () { + const tk = new TokenModule("test", 0, null, null); + + expect(tk[TEMPLATE_DIR]).to.equal(process.cwd()); //deep.equal({}); + }); +}); + +describe("require from modPath", function () { + const tk = new TokenModule("require(./token-module-01.ts)", 0, {}, templateDir); + it("should load as modPath if token.id contains string 'require'", function () { + tk.load({}); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); + }); +}); + +describe("_call in options ", function () { + it("should load custom __call ggg function", function () { + const tk = new TokenModule("#./custom-call.ts", 0, { _call: "setup" }, templateDir); + expect(tk.props).to.deep.equal({ _call: "setup" }); + + tk.load({}); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from custom setup"); + // tk.custom = null; + tk.modPath = null; + tk.load({ a: 11 }); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from custom setup"); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from custom setup"); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from custom setup"); + }); + + it("should load custom params", function () { + const tk = new TokenModule( + "#./custom-call-with-params.ts", + 0, + { _call: "prepare" }, + templateDir + ); + expect(tk.props).to.deep.equal({ _call: "prepare" }); + + tk.load("css"); + expect(tk[TOKEN_HANDLER]({ folder: "dist" })).to.equal("load css from dist folder"); + }); + + it.skip("should set default for options when calling load", function () { + const tk = new TokenModule("test", 0, null, templateDir); + expect(tk.props).to.deep.equal({}); + + console.log(tk._modCall); + tk.load(); + expect(tk.custom).to.deep.equal({}); + console.log(tk.custom); + }); + + it("should set template dir to props[TEMPLATE_DIR] when exists ", function () { + const props = {}; + props[TEMPLATE_DIR] = templateDir; + const tk = new TokenModule("test", 0, props, null); + + expect(tk[TEMPLATE_DIR]).to.equal(templateDir); //deep.equal({}); + }); + + it("should use process.cwd() as default templatedir ", function () { + const tk = new TokenModule("test", 0, null, null); + + expect(tk[TEMPLATE_DIR]).to.equal(process.cwd()); //deep.equal({}); + tk.load(null); + }); +}); + +describe("require from modPath", function () { + const tk = new TokenModule("require(./token-module-01.ts)", 0, {}, templateDir); + + it("should load as modPath if token.id contains string 'require'", function () { + tk.load({}); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); + }); + + it("should have same result when called with load()", function () { + tk.load(); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from token 01"); + }); +}); + +it("should handle token module that's null (no handler)", function () { + const tk = new TokenModule("require(./token-module-null.ts)", 0, {}, templateDir); + tk.load(); + expect(tk.custom).equal(null); +}); + +describe("_call in options ", function () { + it("should load custom _modCall", function () { + const tk = new TokenModule("#./custom-call.ts", 0, { _call: "setup" }, templateDir); + expect(tk.props).to.deep.equal({ _call: "setup" }); + + tk.load(null); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from custom setup"); + + tk.modPath = null; + + tk.load({ a: 11 }); + expect(tk[TOKEN_HANDLER]()).to.equal("hello from custom setup"); + }); + + it("should load custom params", function () { + const tk = new TokenModule( + "#./custom-call-with-params.ts", + 0, + { _call: "prepare" }, + templateDir + ); + expect(tk.props).to.deep.equal({ _call: "prepare" }); + + tk.load("css"); + expect(tk[TOKEN_HANDLER]({ folder: "dist" })).to.equal("load css from dist folder"); + }); +});