[next] Embed: Defer login/logout until ready (#2123)

* feat: Embed defer login/-out until ready

* fix: make remove work with lazy render

* fix: typo

* fix: another typo

* fix: test

* chore: replace query-string for querystringify

* fix: types

* chore: small refactor

* feat: added webpack analzyer

* chore: rename compile -> generate

* fix: fix scripts and improve bundle size

* fix: lodash webpack plugin
This commit is contained in:
Kiwi
2018-12-15 00:07:09 +00:00
committed by Wyatt Johnson
parent 6f538d3235
commit 097294909b
42 changed files with 531 additions and 280 deletions
+40 -9
View File
@@ -1,17 +1,29 @@
import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin";
import CompressionPlugin from "compression-webpack-plugin";
import HtmlWebpackPlugin, { Options } from "html-webpack-plugin";
import { identity } from "lodash";
import LodashModuleReplacementPlugin from "lodash-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import path from "path";
import InterpolateHtmlPlugin from "react-dev-utils/InterpolateHtmlPlugin";
import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModulesPlugin";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
import UglifyJsPlugin from "uglifyjs-webpack-plugin";
import webpack, { Configuration } from "webpack";
import webpack, { Configuration, Plugin } from "webpack";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
import ManifestPlugin from "webpack-manifest-plugin";
import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin";
import paths from "./paths";
import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin";
/**
* filterPlugins will filter out null values from the array of plugins, allowing
* easy embedded ternaries.
*
* @param plugins array of plugins and null values
*/
const filterPlugins = (plugins: Array<Plugin | null>): Plugin[] =>
plugins.filter(identity) as Plugin[];
interface CreateWebpackConfig {
publicPath?: string;
@@ -139,6 +151,9 @@ export default function createWebpackConfig({
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
];
// If the WEBPACK_STATS environment variable is specified, output the stats!
const includeStats = Boolean(process.env.WEBPACK_STATS);
const baseConfig: Configuration = {
// Set webpack mode.
mode: isProduction ? "production" : "development",
@@ -336,6 +351,9 @@ export default function createWebpackConfig({
{
loader: require.resolve("babel-loader"),
options: {
// This will ensure that all packages in node_modules that
// import lodash do so in a way that supports tree shaking.
plugins: ["lodash"],
presets: [
[
"@babel/env",
@@ -399,6 +417,7 @@ export default function createWebpackConfig({
],
},
plugins: [
new LodashModuleReplacementPlugin(),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
new webpack.DefinePlugin(envStringified),
@@ -473,7 +492,7 @@ export default function createWebpackConfig({
paths.appAdminIndex,
],
},
plugins: [
plugins: filterPlugins([
...baseConfig.plugins!,
// Generates an `stream.html` file with the <script> injected.
new HtmlWebpackPlugin({
@@ -527,16 +546,21 @@ export default function createWebpackConfig({
new ManifestPlugin({
fileName: "asset-manifest.json",
}),
],
// If stats are enabled, output them!
includeStats
? new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "report-assets.html",
})
: null,
]),
},
/* Webpack config for our embed */
{
...baseConfig,
entry: [
/* Use minimal amount of polyfills (for IE) */
"core-js/fn/object/assign",
"core-js/fn/symbol",
"core-js/fn/symbol/iterator",
"intersection-observer", // also for Safari
...devServerEntries,
paths.appEmbedIndex,
],
@@ -547,7 +571,7 @@ export default function createWebpackConfig({
// as this lives in a static template on the embed site.
filename: "assets/js/embed.js",
},
plugins: [
plugins: filterPlugins([
...baseConfig.plugins!,
...(isProduction
? []
@@ -583,7 +607,14 @@ export default function createWebpackConfig({
new ManifestPlugin({
fileName: "embed-manifest.json",
}),
],
// If stats are enabled, output them!
includeStats
? new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "report-embed.html",
})
: null,
]),
},
];
}
+1 -1
View File
@@ -3,7 +3,7 @@ module.exports = {
["@babel/env", { targets: "last 2 versions, ie 11", modules: false }],
"@babel/react",
],
plugins: ["@babel/syntax-dynamic-import"],
plugins: ["@babel/syntax-dynamic-import", "lodash"],
env: {
production: {
plugins: [],
+2 -2
View File
@@ -1,6 +1,6 @@
import qs from "query-string";
import { commitLocalUpdate, Environment } from "relay-runtime";
import { parseQuery } from "talk-common/utils";
import {
createAndRetain,
LOCAL_ID,
@@ -18,7 +18,7 @@ export default async function initLocalState(environment: Environment) {
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
// Parse query params
const query = qs.parse(location.search);
const query = parseQuery(location.search);
// Set default view.
localRecord.setValue(query.view || "SIGN_IN", "view");
+95 -45
View File
@@ -1,11 +1,11 @@
import { EventEmitter2 } from "eventemitter2";
import { omit } from "lodash";
import sinon from "sinon";
import sinon, { SinonMock } from "sinon";
import { PymControlConfig } from "./PymControl";
import { StreamEmbed, StreamEmbedConfig } from "./StreamEmbed";
it("should throw when calling pym dependent methods but was not rendered", () => {
it("should throw when calling remove but was not rendered", () => {
const config: StreamEmbedConfig = {
title: "StreamEmbed",
eventEmitter: new EventEmitter2(),
@@ -13,13 +13,7 @@ it("should throw when calling pym dependent methods but was not rendered", () =>
rootURL: "http://localhost/",
};
const streamEmbed = new StreamEmbed(config);
[
() => streamEmbed.login("token"),
() => streamEmbed.logout(),
() => streamEmbed.remove(),
].forEach(cb => {
expect(cb).toThrow();
});
expect(() => streamEmbed.remove()).toThrow();
});
it("should return rendered", () => {
const config: StreamEmbedConfig = {
@@ -65,44 +59,100 @@ it("should relay events methods to event emitter", () => {
streamEmbed.off("event", callback);
});
it("should send login message to PymControl", () => {
const config: StreamEmbedConfig = {
title: "StreamEmbed",
eventEmitter: new EventEmitter2(),
id: "container-id",
rootURL: "http://localhost/",
};
const pymControl = {
// tslint:disable-next-line:no-empty
sendMessage: () => {},
};
const pymControlMock = sinon.mock(pymControl);
pymControlMock.expects("sendMessage").withArgs("login", "token");
const fakeFactory: any = () => pymControl;
const streamEmbed = new StreamEmbed(config, fakeFactory);
streamEmbed.render();
streamEmbed.login("token");
pymControlMock.verify();
describe("should send login message to PymControl", () => {
let pymControlMock: SinonMock;
let streamEmbed: StreamEmbed;
let eventEmitter: EventEmitter2;
beforeEach(() => {
eventEmitter = new EventEmitter2();
const config: StreamEmbedConfig = {
title: "StreamEmbed",
eventEmitter,
id: "container-id",
rootURL: "http://localhost/",
};
const pymControl = {
// tslint:disable-next-line:no-empty
sendMessage: () => {},
};
const fakeFactory: any = () => pymControl;
pymControlMock = sinon.mock(pymControl);
pymControlMock.expects("sendMessage").withArgs("login", "token");
streamEmbed = new StreamEmbed(config, fakeFactory);
});
afterEach(() => {
pymControlMock.restore();
});
it("send login immediately when already ready", () => {
streamEmbed.render();
eventEmitter.emit("ready");
streamEmbed.login("token");
pymControlMock.verify();
});
it("defer login until ready", () => {
streamEmbed.login("token");
streamEmbed.render();
eventEmitter.emit("ready");
pymControlMock.verify();
});
it("do not call login when not ready", () => {
streamEmbed.login("token");
streamEmbed.render();
expect(() => pymControlMock.verify()).toThrow();
});
});
it("should send logout message to PymControl", () => {
const config: StreamEmbedConfig = {
title: "StreamEmbed",
eventEmitter: new EventEmitter2(),
id: "container-id",
rootURL: "http://localhost/",
};
const pymControl = {
// tslint:disable-next-line:no-empty
sendMessage: () => {},
};
const pymControlMock = sinon.mock(pymControl);
pymControlMock.expects("sendMessage").withArgs("logout");
const fakeFactory: any = () => pymControl;
const streamEmbed = new StreamEmbed(config, fakeFactory);
streamEmbed.render();
streamEmbed.logout();
pymControlMock.verify();
describe("should send logout message to PymControl", () => {
let pymControlMock: SinonMock;
let streamEmbed: StreamEmbed;
let eventEmitter: EventEmitter2;
beforeEach(() => {
eventEmitter = new EventEmitter2();
const config: StreamEmbedConfig = {
title: "StreamEmbed",
eventEmitter,
id: "container-id",
rootURL: "http://localhost/",
};
const pymControl = {
// tslint:disable-next-line:no-empty
sendMessage: () => {},
};
const fakeFactory: any = () => pymControl;
pymControlMock = sinon.mock(pymControl);
pymControlMock.expects("sendMessage").withArgs("logout");
streamEmbed = new StreamEmbed(config, fakeFactory);
});
afterEach(() => {
pymControlMock.restore();
});
it("send logout immediately when already ready", () => {
streamEmbed.render();
eventEmitter.emit("ready");
streamEmbed.logout();
pymControlMock.verify();
});
it("defer logout until ready", () => {
streamEmbed.logout();
streamEmbed.render();
eventEmitter.emit("ready");
pymControlMock.verify();
});
it("do not call logout when not ready", () => {
streamEmbed.logout();
streamEmbed.render();
expect(() => pymControlMock.verify()).toThrow();
});
});
it("should pass default values to pymControl", () => {
+30 -9
View File
@@ -1,6 +1,6 @@
import { EventEmitter2 } from "eventemitter2";
import qs from "query-string";
import { stringifyQuery } from "talk-common/utils";
import ensureNoEndSlash from "talk-common/utils/ensureNoEndSlash";
import urls from "talk-framework/helpers/urls";
import { ExternalConfig } from "talk-framework/lib/externalConfig";
@@ -15,7 +15,7 @@ import {
withPymStorage,
withSetCommentID,
} from "./decorators";
import onIntersect from "./onIntersect";
import onIntersect, { OnIntersectCancellation } from "./onIntersect";
import PymControl, {
defaultPymControlFactory,
PymControlFactory,
@@ -37,6 +37,8 @@ export class StreamEmbed {
private config: StreamEmbedConfig;
private pymControl?: PymControl;
private pymControlFactory: PymControlFactory;
private ready = false;
private cancelAutoRender: OnIntersectCancellation | null = null;
constructor(
config: StreamEmbedConfig,
@@ -53,13 +55,20 @@ export class StreamEmbed {
if (config.commentID) {
this.render();
} else {
onIntersect(document.getElementById(config.id)!, () => {
if (!this.rendered) {
this.render();
this.cancelAutoRender = onIntersect(
document.getElementById(config.id)!,
() => {
this.cancelAutoRender = null;
if (!this.rendered) {
this.render();
}
}
});
);
}
}
config.eventEmitter.once("ready", () => {
this.ready = true;
});
}
private assertRendered() {
@@ -77,16 +86,28 @@ export class StreamEmbed {
}
public login(token: string) {
this.assertRendered();
if (!this.ready) {
this.config.eventEmitter.once("ready", () => this.login(token));
return;
}
this.pymControl!.sendMessage("login", token);
}
public logout() {
this.assertRendered();
if (!this.ready) {
this.config.eventEmitter.once("ready", () => this.logout());
return;
}
this.pymControl!.sendMessage("logout");
}
public remove() {
// If lazy render was enabled, just cancel it.
if (this.cancelAutoRender) {
this.cancelAutoRender();
this.cancelAutoRender = null;
return;
}
this.assertRendered();
this.pymControl!.remove();
this.pymControl = undefined;
@@ -116,7 +137,7 @@ export class StreamEmbed {
withConfig(externalConfig),
];
const query = qs.stringify({
const query = stringifyQuery({
storyID: this.config.storyID,
storyURL: this.config.storyURL,
commentID: this.config.commentID,
+3 -2
View File
@@ -1,5 +1,6 @@
import { EventEmitter2 } from "eventemitter2";
import qs from "query-string";
import { parseQuery } from "talk-common/utils";
import { default as create, StreamEmbed } from "./StreamEmbed";
@@ -41,7 +42,7 @@ function resolveStoryURL() {
export function createStreamEmbed(config: Config): StreamEmbed {
// Parse query params
const query = qs.parse(location.search);
const query = parseQuery(location.search);
const eventEmitter = new EventEmitter2({ wildcard: true });
if (config.events) {
@@ -4,7 +4,7 @@ exports[`should pass correct values to pymControl 1`] = `
Object {
"id": "container-id",
"title": "StreamEmbed",
"url": "http://localhost/embed/stream?commentID=comment-id&storyID=story-id&storyURL=story-url",
"url": "http://localhost/embed/stream?storyID=story-id&storyURL=story-url&commentID=comment-id",
}
`;
@@ -11,7 +11,9 @@ it("should emit events from pym to eventEmitter", () => {
};
const fakePym = {
onMessage: (type: string, callback: (raw: string) => void) => {
expect(type).toBe("event");
if (type !== "event") {
return;
}
callback(JSON.stringify({ eventName: "eventName", value: "value" }));
},
el: document.createElement("div"),
@@ -19,3 +21,23 @@ it("should emit events from pym to eventEmitter", () => {
withEventEmitter(eventEmitterMock as any)(fakePym as any);
eventEmitterMock.emit.verify();
});
it("should emit ready event from pym to eventEmitter", () => {
const eventEmitterMock = {
emit: sinon
.mock()
.once()
.withArgs("ready"),
};
const fakePym = {
onMessage: (type: string, callback: () => void) => {
if (type !== "ready") {
return;
}
callback();
},
el: document.createElement("div"),
};
withEventEmitter(eventEmitterMock as any)(fakePym as any);
eventEmitterMock.emit.verify();
});
@@ -8,6 +8,11 @@ const withEventEmitter = (eventEmitter: EventEmitter2): Decorator => pym => {
const { eventName, value } = JSON.parse(raw);
eventEmitter.emit(eventName, value);
});
// Notify ready state.
pym.onMessage("ready", () => {
eventEmitter.emit("ready");
});
};
export default withEventEmitter;
@@ -1,6 +1,6 @@
import sinon from "sinon";
import { createInMemoryStorage } from "../testUtils";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import withPymStorage from "./withPymStorage";
class PymStub {
@@ -1,17 +1,17 @@
import qs from "query-string";
import { parseQuery, stringifyQuery } from "talk-common/utils";
import { buildURL } from "talk-framework/utils";
import { Decorator } from "./types";
function getCurrentCommentID() {
return qs.parse(location.search).commentID;
return parseQuery(location.search).commentID;
}
const withSetCommentID: Decorator = pym => {
// Add the permalink comment id to the query.
pym.onMessage("setCommentID", (id: string) => {
const search = qs.stringify({
...qs.parse(location.search),
const search = stringifyQuery({
...parseQuery(location.search),
commentID: id || undefined,
});
+7 -7
View File
@@ -1,10 +1,9 @@
export default function onIntersect(el: HTMLElement, callback: () => void) {
if (!IntersectionObserver) {
// tslint:disable-next-line:no-console
console.warn("IntersectionObserver not available");
callback();
return;
}
export type OnIntersectCancellation = () => void;
export default function onIntersect(
el: HTMLElement,
callback: () => void
): OnIntersectCancellation {
const options = {
rootMargin: "100px",
threshold: 1.0,
@@ -17,4 +16,5 @@ export default function onIntersect(el: HTMLElement, callback: () => void) {
}
}, options);
observer.observe(el);
return () => observer.disconnect();
}
@@ -1,34 +0,0 @@
import createInMemoryStorage from "./InMemoryStorage";
it("should set and unset values", () => {
const storage = createInMemoryStorage();
storage.setItem("test", "value");
expect(storage.getItem("test")).toBe("value");
storage.removeItem("test");
expect(storage.getItem("test")).toBeNull();
});
it("should return length", () => {
const storage = createInMemoryStorage();
storage.setItem("a", "value");
storage.setItem("b", "value");
storage.setItem("c", "value");
expect(storage.length).toBe(3);
});
it("should nth key", () => {
const storage = createInMemoryStorage();
storage.setItem("a", "0");
storage.setItem("b", "1");
storage.setItem("c", "2");
expect(storage.key(2)).toBe("c");
});
it("accepts predefined data", () => {
const storage = createInMemoryStorage({
a: "0",
b: "1",
c: "2",
});
expect(storage.toString()).toMatchSnapshot();
});
@@ -1,49 +0,0 @@
/**
* InMemoryStorage is a dumb implementation of the Storage interface that will
* not persist the data at all. It implements the Storage interface found:
*
* https://developer.mozilla.org/en-US/docs/Web/API/Storage
*/
class InMemoryStorage implements Storage {
private data: Record<string, string>;
constructor(data: Record<string, string> = {}) {
this.data = data;
}
get length() {
return Object.keys(this.data).length;
}
public clear() {
this.data = {};
}
public key(n: number) {
if (this.length <= n) {
return null;
}
return Object.keys(this.data)[n];
}
public getItem(key: string) {
return this.data[key] || null;
}
public setItem(key: string, value: string) {
this.data[key] = value;
}
public removeItem(key: string) {
delete this.data[key];
}
public toString() {
return JSON.stringify(this.data);
}
}
export default function createInMemoryStorage(data?: Record<string, string>) {
return new InMemoryStorage(data);
}
@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`accepts predefined data 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"c\\":\\"2\\"}"`;
-1
View File
@@ -1 +0,0 @@
export { default as createInMemoryStorage } from "./InMemoryStorage";
-18
View File
@@ -1,18 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": ["dom", "es6"],
"types": ["jest", "node"],
"baseUrl": "./",
"paths": {
"talk-common/*": ["../../common/*"],
"talk-framework/*": ["../framework/*"]
}
},
"include": [
"./**/*",
"../../../types/pym.d.ts",
"../../../types/simulant.d.ts"
],
"exclude": ["node_modules"]
}
@@ -1,5 +1,5 @@
import sinon from "sinon";
import { createInMemoryStorage } from "../testUtils";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import prefixStorage from "./prefixStorage";
it("should get nth key", () => {
@@ -0,0 +1,29 @@
import { Child } from "pym.js";
import React from "react";
import withContext from "./withContext";
interface Props {
pym: Child;
}
/**
* SendPymReady will notify the parent pym that
* we are ready and have setup all listeners.
*/
class SendPymReady extends React.Component<Props> {
private sent = false;
public componentDidMount() {
if (!this.sent) {
this.sent = true;
this.props.pym.sendMessage("ready", "");
}
}
public render() {
return null;
}
}
const enhanced = withContext(({ pym }) => ({ pym }))(SendPymReady);
export default enhanced;
@@ -23,6 +23,7 @@ import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
import { createNetwork, TokenGetter } from "../network";
import { PostMessageService } from "../postMessage";
import SendPymReady from "./SendPymReady";
import { TalkContext, TalkContextProvider } from "./TalkContext";
export type InitLocalState = ((
@@ -159,6 +160,7 @@ function createMangedTalkContextProvider(
return (
<TalkContextProvider value={this.state.context}>
{this.props.children}
{this.state.context.pym && <SendPymReady />}
</TalkContextProvider>
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import merge from "lodash/merge";
import { merge } from "lodash";
import { Overwrite } from "talk-framework/types";
const buildOptions = (inputOptions: RequestInit = {}) => {
@@ -1,11 +1,11 @@
import qs from "query-string";
import { parseQuery, stringifyQuery } from "talk-common/utils";
import buildURL from "./buildURL";
import parseURL from "./parseURL";
export default function modifyQuery(url: string, params: {}) {
const parsed = parseURL(url);
const query = qs.parse(parsed.search);
parsed.search = qs.stringify({ ...query, ...params });
const query = parseQuery(parsed.search);
parsed.search = stringifyQuery({ ...query, ...params });
return buildURL(parsed);
}
@@ -1,5 +1,9 @@
import qs from "query-string";
import { parseQuery } from "talk-common/utils";
export default function parseQueryHash(hash: string): Record<string, string> {
return qs.parse(hash);
let normalized = hash;
if (normalized[0] === "#") {
normalized = normalized.substr(1);
}
return parseQuery(normalized);
}
@@ -1,8 +1,8 @@
import { shallow } from "enzyme";
import qs from "query-string";
import React from "react";
import { Environment, RecordSource } from "relay-runtime";
import { parseQuery } from "talk-common/utils";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
@@ -38,7 +38,7 @@ it("Sets comment id", () => {
};
shallow(<OnPymSetCommentID {...props} />);
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
expect(qs.parse(location.search).commentID).toEqual(id);
expect(parseQuery(location.search).commentID).toEqual(id);
});
it("Sets comment id to null when empty", () => {
@@ -54,5 +54,5 @@ it("Sets comment id to null when empty", () => {
};
shallow(<OnPymSetCommentID {...props} />);
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
expect(qs.parse(location.search).commentID).toBeUndefined();
expect(parseQuery(location.search).commentID).toBeUndefined();
});
@@ -1,6 +1,6 @@
import qs from "query-string";
import { commitLocalUpdate, Environment } from "relay-runtime";
import { parseQuery } from "talk-common/utils";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { getExternalConfig } from "talk-framework/lib/externalConfig";
import { createAndRetain, initLocalBaseState } from "talk-framework/lib/relay";
@@ -26,7 +26,7 @@ export default async function initLocalState(
const localRecord = root.getLinkedRecord("local")!;
// Parse query params
const query = qs.parse(location.search);
const query = parseQuery(location.search);
if (query.storyID) {
localRecord.setValue(query.storyID, "storyID");
@@ -1,8 +1,8 @@
import qs from "query-string";
import { Environment, RecordSource } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { parseQuery } from "talk-common/utils";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
@@ -29,7 +29,7 @@ it("Sets comment id", () => {
const id = "comment1-id";
commit(environment, { id }, {} as any);
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
expect(qs.parse(location.search).commentID).toEqual(id);
expect(parseQuery(location.search).commentID).toEqual(id);
});
it("Should call setCommentID in pym", async () => {
@@ -60,6 +60,6 @@ it("Should call setCommentID in pym with empty id", async () => {
commit(environment, { id: null }, context as any);
await timeout();
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
expect(qs.parse(location.search).commentID).toBeUndefined();
expect(parseQuery(location.search).commentID).toBeUndefined();
context.pym.sendMessage.verify();
});
+2 -1
View File
@@ -10,6 +10,7 @@
"paths": {
"talk-admin/*": ["./admin/*"],
"talk-auth/*": ["./auth/*"],
"talk-embed/*": ["./embed/*"],
"talk-stream/*": ["./stream/*"],
"talk-framework/*": ["./framework/*"],
"talk-ui/*": ["./ui/*"],
@@ -18,5 +19,5 @@
}
},
"include": ["./**/*", "../../types/**/*.d.ts"],
"exclude": ["node_modules", "./embed"]
"exclude": ["node_modules"]
}
+1
View File
@@ -0,0 +1 @@
module.exports = require("../client/.babelrc.js");
+5 -5
View File
@@ -1,5 +1,5 @@
module.exports = {
presets: [
["@babel/env", { targets: "last 2 versions, ie 11", modules: "commonjs" }],
],
};
if (process.env.WEBPACK === "true") {
module.exports = require("./.babelrc.client.js");
} else {
module.exports = require("./.babelrc.server.js");
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
["@babel/env", { targets: "last 2 versions, ie 11", modules: "commonjs" }],
],
};
+2
View File
@@ -5,3 +5,5 @@ export { default as oncePerFrame } from "./oncePerFrame";
export { default as isBeforeDate } from "./isBeforeDate";
export { default as ensureEndSlash } from "./ensureEndSlash";
export { default as ensureNoEndSlash } from "./ensureNoEndSlash";
export { default as parseQuery } from "./parseQuery";
export { default as stringifyQuery } from "./stringifyQuery";
+8
View File
@@ -0,0 +1,8 @@
/**
* From the `querystringify` project:
* The parse method transforms a given query string in to an object.
* Parameters without values are set to empty strings.
* It does not care if your query string is prefixed with a ? or not.
* It just extracts the parts between the = and &:
*/
export { parse as default } from "querystringify";
+24
View File
@@ -0,0 +1,24 @@
import qs from "querystringify";
/**
* From the `querystringify` project:
* This transforms a given object in to a query string.
* By default we return the query string without a ? prefix.
* If you want to prefix it by default simply supply true as second argument.
* If it should be prefixed by something else simply supply a string with the
* prefix value as second argument.
*
* In addition keys that have an undefined value are removed from the query.
*/
export default function stringifyQuery(
obj: object,
prefix?: string | boolean
): string {
const copy: any = { ...obj };
Object.keys(copy).forEach(key => {
if (copy[key] === undefined) {
delete copy[key];
}
});
return qs.stringify(copy, prefix);
}
+1
View File
@@ -0,0 +1 @@
declare module "intersection-observer";
+4
View File
@@ -0,0 +1,4 @@
declare module "querystringify" {
export function parse(query: string): any;
export function stringify(obj: object, prefix?: string | boolean): string;
}