From cb1928d11b004ef93ed8ddcf8992ff3bf8542afe Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Wed, 10 Jul 2019 12:18:59 -0700 Subject: [PATCH 1/5] feat: add marko 3 support --- src/index-browser.ts | 33 ++++++++++++++++++++++++++------- src/index.ts | 11 +++++++++-- src/types.ts | 6 +++++- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/index-browser.ts b/src/index-browser.ts index 7487ec3..0c2c0f6 100644 --- a/src/index-browser.ts +++ b/src/index-browser.ts @@ -20,8 +20,17 @@ export async function render( container = document.body.appendChild(document.createElement("div")) } = options; - const result = await template.render(input); - const component = result.appendTo(container).getComponent(); + // Doesn't use promise API so that we can support Marko v3 + const renderResult = (await new Promise((resolve, reject) => + template.render(input, (err, result) => + err ? reject(err) : resolve(result) + ) + )) as any; + + const isV3 = !renderResult.getComponent; + const component = renderResult + .appendTo(container) + [isV3 ? "getWidget" : "getComponent"](); const eventRecord: EventRecord = {}; mountedComponents.add({ container, component }); @@ -63,12 +72,22 @@ export async function render( }, rerender(newInput?: typeof input): Promise { return new Promise(resolve => { - component.once("update", () => resolve()); - - if (newInput) { - component.input = newInput; + if (isV3) { + component.once("render", () => resolve()); + + if (newInput) { + component.setProps(newInput); + } else { + component.setStateDirty("__forceUpdate__"); + } } else { - component.forceUpdate(); + component.once("update", () => resolve()); + + if (newInput) { + component.input = newInput; + } else { + component.forceUpdate(); + } } }); }, diff --git a/src/index.ts b/src/index.ts index e16b125..3cb2613 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,10 +11,17 @@ export * from "@testing-library/dom"; export async function render( template: T, - input: Parameters[0] = {}, + input: Parameters[0] = {}, options?: RenderOptions ) { - const html = String(await template.render(input)); + // Doesn't use promise API so that we can support Marko v3 + const html = String( + await new Promise((resolve, reject) => + template.renderToString(input, (err, result) => + err ? reject(err) : resolve(result) + ) + ) + ); const container = JSDOM.fragment(html); (container as any).outerHTML = html; // Fixes prettyDOM for container diff --git a/src/types.ts b/src/types.ts index e2e6b61..1fbb392 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,7 +8,11 @@ export interface RenderOptions { } export interface Template { - render(input: unknown): Promise; + renderToString( + input: unknown, + cb: (err: Error | null, result: any) => void + ): any; + render(input: unknown, cb: (err: Error | null, result: any) => void): any; } export const INTERNAL_EVENTS = [ From f148d1578219c4f6c0ec17bf102ed25fdbb95732 Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Wed, 10 Jul 2019 12:26:03 -0700 Subject: [PATCH 2/5] test: ignore coverage for Marko 3 api --- src/index-browser.ts | 5 +++-- src/index.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/index-browser.ts b/src/index-browser.ts index 0c2c0f6..23dd7df 100644 --- a/src/index-browser.ts +++ b/src/index-browser.ts @@ -23,14 +23,14 @@ export async function render( // Doesn't use promise API so that we can support Marko v3 const renderResult = (await new Promise((resolve, reject) => template.render(input, (err, result) => - err ? reject(err) : resolve(result) + err ? /* istanbul ignore next */ reject(err) : resolve(result) ) )) as any; const isV3 = !renderResult.getComponent; const component = renderResult .appendTo(container) - [isV3 ? "getWidget" : "getComponent"](); + [isV3 ? /* istanbul ignore next */ "getWidget" : "getComponent"](); const eventRecord: EventRecord = {}; mountedComponents.add({ container, component }); @@ -72,6 +72,7 @@ export async function render( }, rerender(newInput?: typeof input): Promise { return new Promise(resolve => { + /* istanbul ignore if */ if (isV3) { component.once("render", () => resolve()); diff --git a/src/index.ts b/src/index.ts index 3cb2613..5c41b74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ export async function render( const html = String( await new Promise((resolve, reject) => template.renderToString(input, (err, result) => - err ? reject(err) : resolve(result) + err ? /* istanbul ignore next */ reject(err) : resolve(result) ) ) ); From 0b286028548d1457044520930db357c312b3b81a Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Wed, 10 Jul 2019 13:21:03 -0700 Subject: [PATCH 3/5] feat: expose RenderResult type for convenience --- src/index-browser.ts | 6 +++++- src/index.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/index-browser.ts b/src/index-browser.ts index 23dd7df..ccac75f 100644 --- a/src/index-browser.ts +++ b/src/index-browser.ts @@ -105,13 +105,17 @@ export async function render( } }, ...within((container as any) as HTMLElement) - }; + } as const; } export function cleanup() { mountedComponents.forEach(destroyComponent); } +export type RenderResult = Parameters< + NonNullable["then"]>[0]> +>[0]; + function destroyComponent({ container, component }) { component.destroy(); diff --git a/src/index.ts b/src/index.ts index 5c41b74..07b2754 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,8 +50,12 @@ export async function render( } }, ...within((container as any) as HTMLElement) - }; + } as const; } /* istanbul ignore next: There is no cleanup for SSR. */ export function cleanup() {} + +export type RenderResult = Parameters< + NonNullable["then"]>[0]> +>[0]; From 324977c517c8f3168c984b56aaa84ad4b20a9b90 Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Thu, 11 Jul 2019 10:06:41 -0700 Subject: [PATCH 4/5] fix: fallback to render if renderToString missing --- src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 07b2754..1b7a102 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,9 +15,12 @@ export async function render( options?: RenderOptions ) { // Doesn't use promise API so that we can support Marko v3 + const renderMethod = template.renderToString + ? "renderToString" + : /* istanbul ignore next */ "render"; const html = String( await new Promise((resolve, reject) => - template.renderToString(input, (err, result) => + template[renderMethod](input, (err, result) => err ? /* istanbul ignore next */ reject(err) : resolve(result) ) ) From 4c2f4652d9dd52e287c9bfeff793e0e5a901649c Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Thu, 11 Jul 2019 10:34:10 -0700 Subject: [PATCH 5/5] test: include a test for legacy compatibility layer --- jest.config.js | 3 +- package-lock.json | 67 +++++++++++++++++++ package.json | 1 + .../fixtures/legacy-counter/index.js | 10 +++ .../fixtures/legacy-counter/template.marko | 4 ++ src/__tests__/render.browser.ts | 10 +++ src/__tests__/render.server.ts | 9 +++ src/index.ts | 4 +- 8 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/fixtures/legacy-counter/index.js create mode 100644 src/__tests__/fixtures/legacy-counter/template.marko diff --git a/jest.config.js b/jest.config.js index 4ff55ca..26911f2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,6 +26,7 @@ function project(displayName, config) { transform: { "\\.ts$": "ts-jest" }, - coveragePathIgnorePatterns: ["/__tests__/"] + coveragePathIgnorePatterns: ["/__tests__/"], + transformIgnorePatterns: ["node_modules/(?!(marko)/)"] }; } diff --git a/package-lock.json b/package-lock.json index 4154cac..eb21da7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -962,6 +962,16 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, + "async-writer": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/async-writer/-/async-writer-2.1.3.tgz", + "integrity": "sha1-XCo4B8u3GwfSXX4kAhMnKvAwoo8=", + "dev": true, + "requires": { + "complain": "^1.0.0", + "events": "^1.0.2" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5018,6 +5028,16 @@ } } }, + "marko-widgets": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/marko-widgets/-/marko-widgets-7.0.1.tgz", + "integrity": "sha1-Dg3ytWCfRi9uk/UvhihTXy9LwD8=", + "dev": true, + "requires": { + "raptor-dom": "^1.1.0", + "raptor-renderer": "^1.4.4" + } + }, "matcher": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz", @@ -5825,6 +5845,15 @@ "integrity": "sha1-uDw8m2A9yYXCw6n3jStAc+b2Akw=", "dev": true }, + "raptor-dom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/raptor-dom/-/raptor-dom-1.1.1.tgz", + "integrity": "sha1-Xt4wpy3A+ZeiwXMfwfLESz+1rFo=", + "dev": true, + "requires": { + "raptor-pubsub": "^1.0.5" + } + }, "raptor-json": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/raptor-json/-/raptor-json-1.1.0.tgz", @@ -5858,12 +5887,50 @@ } } }, + "raptor-pubsub": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/raptor-pubsub/-/raptor-pubsub-1.0.5.tgz", + "integrity": "sha1-Fe4SoOUFnsFbcv+dYeyvn0IS70c=", + "dev": true, + "requires": { + "events": "^1.0.2", + "raptor-util": "^1.0.0" + }, + "dependencies": { + "raptor-util": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/raptor-util/-/raptor-util-1.1.2.tgz", + "integrity": "sha1-8u6AdqmuPq4uZWcuRqIgB0+i3/M=", + "dev": true + } + } + }, "raptor-regexp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/raptor-regexp/-/raptor-regexp-1.0.1.tgz", "integrity": "sha1-7PD2bGZxwM2fXkjDcFAmxVCZlcA=", "dev": true }, + "raptor-renderer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/raptor-renderer/-/raptor-renderer-1.5.0.tgz", + "integrity": "sha512-Bf6aIh/80cjFz9+xJ//5aV/rIDbqMNYZOsCxgXbXJfNLGjGPO4wn9nixdSASl/Pl3wCUTIixU542qjbCQBRYvQ==", + "dev": true, + "requires": { + "async-writer": "^2.0.0", + "raptor-dom": "^1.0.0", + "raptor-pubsub": "^1.0.2", + "raptor-util": "^1.0.0" + }, + "dependencies": { + "raptor-util": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/raptor-util/-/raptor-util-1.1.2.tgz", + "integrity": "sha1-8u6AdqmuPq4uZWcuRqIgB0+i3/M=", + "dev": true + } + } + }, "raptor-strings": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/raptor-strings/-/raptor-strings-1.0.2.tgz", diff --git a/package.json b/package.json index d42d4d1..9907e0e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "jest-dom": "^3.5.0", "lint-staged": "^8.2.1", "marko": "^4.18.2", + "marko-widgets": "^7.0.1", "prettier": "^1.18.2", "ts-jest": "^24.0.2", "tslint": "^5.18.0", diff --git a/src/__tests__/fixtures/legacy-counter/index.js b/src/__tests__/fixtures/legacy-counter/index.js new file mode 100644 index 0000000..c4f8fb2 --- /dev/null +++ b/src/__tests__/fixtures/legacy-counter/index.js @@ -0,0 +1,10 @@ +module.exports = require("marko-widgets").defineComponent({ + template: require("./template.marko"), + getInitialState() { + return { count: 0 }; + }, + + increment() { + this.setState("count", this.state.count + 1); + } +}); diff --git a/src/__tests__/fixtures/legacy-counter/template.marko b/src/__tests__/fixtures/legacy-counter/template.marko new file mode 100644 index 0000000..4261d3b --- /dev/null +++ b/src/__tests__/fixtures/legacy-counter/template.marko @@ -0,0 +1,4 @@ +
+ Value: ${data.count} + +
\ No newline at end of file diff --git a/src/__tests__/render.browser.ts b/src/__tests__/render.browser.ts index 5fd3013..3d2eb57 100644 --- a/src/__tests__/render.browser.ts +++ b/src/__tests__/render.browser.ts @@ -1,6 +1,7 @@ import "jest-dom/extend-expect"; import { render, fireEvent, wait, cleanup } from ".."; import Counter from "./fixtures/counter.marko"; +import LegacyCounter from "./fixtures/legacy-counter"; import UpdateCounter from "./fixtures/update-counter.marko"; import HelloWorld from "./fixtures/hello-world.marko"; import HelloName from "./fixtures/hello-name.marko"; @@ -17,6 +18,15 @@ test("renders interactive content in the document", async () => { await wait(() => expect(getByText("Value: 1"))); }); +test("renders interactive content in the document with Marko 3", async () => { + const { getByText } = await render(LegacyCounter); + expect(getByText(/Value: 0/)).toBeInTheDocument(); + + fireEvent.click(getByText("Increment")); + + await wait(() => expect(getByText("Value: 1"))); +}); + test("can be rerendered with new input", async () => { const { getByText, rerender } = await render(HelloName, { name: "Michael" }); diff --git a/src/__tests__/render.server.ts b/src/__tests__/render.server.ts index e48fcd5..1477f2f 100644 --- a/src/__tests__/render.server.ts +++ b/src/__tests__/render.server.ts @@ -1,5 +1,6 @@ import { render, fireEvent } from ".."; import Counter from "./fixtures/counter.marko"; +import LegacyCounter from "./fixtures/legacy-counter"; import Clickable from "./fixtures/clickable.marko"; import HelloName from "./fixtures/hello-name.marko"; @@ -11,6 +12,14 @@ test("renders static content in a document without a browser context", async () ); }); +test("renders static content from a Marko 3 component", async () => { + const { getByText } = await render(LegacyCounter); + expect(getByText("Value: 0")).toHaveProperty( + ["ownerDocument", "defaultView"], + null + ); +}); + test("fails when rerendering", async () => { const { rerender } = await render(HelloName, { name: "Michael" }); await expect(rerender({ name: "Dylan" })).rejects.toThrow(/cannot re-render/); diff --git a/src/index.ts b/src/index.ts index 1b7a102..2e5c1b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,9 +15,7 @@ export async function render( options?: RenderOptions ) { // Doesn't use promise API so that we can support Marko v3 - const renderMethod = template.renderToString - ? "renderToString" - : /* istanbul ignore next */ "render"; + const renderMethod = template.renderToString ? "renderToString" : "render"; const html = String( await new Promise((resolve, reject) => template[renderMethod](input, (err, result) =>