diff --git a/docusaurus/docs/main-concept/build-config.md b/docusaurus/docs/main-concept/build-config.md index a064ec1..54ba086 100644 --- a/docusaurus/docs/main-concept/build-config.md +++ b/docusaurus/docs/main-concept/build-config.md @@ -34,9 +34,11 @@ There's several configuration that could be setup here: - `publicPath` - This value would override publicPath definition for your asset files. - `client` - This value would be merged to Treats client Webpack configuration using [webpack-merge][webpack-merge-website] - `server` - This value would be merged to Treats server Webpack configuration using [webpack-merge][webpack-merge-website] + - `workbox` - This configuration can be used to enable and setting workbox configuration in treats. More info on workbox configuration can be seen on [here][workbox-build-configuration] - `postcss` -This configuration can be used to extends Treats PostCSS setup - `babel` -This configuration can be used to extends Treats Babel setup. All values would be merged to treats babel config using [babel-merge][babel-merge-website] [webpack-merge-website]: https://www.npmjs.com/package/webpack-merge [babel-merge-website]: https://www.npmjs.com/package/babel-merge +[workbox-build-configuration]: ./workbox.html diff --git a/docusaurus/docs/main-concept/workbox.md b/docusaurus/docs/main-concept/workbox.md new file mode 100644 index 0000000..984c18b --- /dev/null +++ b/docusaurus/docs/main-concept/workbox.md @@ -0,0 +1,41 @@ +--- +id: workbox +title: Treats with Workbox +sidebar_label: Workbox +--- +Treats provides [Workbox][workbox-main-page] support in building your application. Workbox is a libary that makes caching assets on your application much easier so it can improve your application's performance and resilience in unstable condition. + +Treats using a webpack plugin called [workbox-webpack-plugin][workbox-webpack-plugin-module-page] to implement Workbox. Workbox configuration can be set and enabled on your project from your project's treats build config (`treats.config.js`) as can be seen in here [build config][main-concept-build-config] file without needing to install and add any of workbox dependencies. + +## Configuring Workbox inside Treats Build Config file +Here's an example on how to enable your workbox configuration on `treats.config.js`: +``` +// treats.config.js +... +const config: { + ... + webpack: { + ... + workbox: { + pluginMode: "InjectManifest", + serviceWorkerFilename: "sw.js", + options: { + swSrc: "./src/sw.js", + include: /\.(html|css|js)$/ + } + } + } +} + +``` +Your configuration should be placed inside `workbox` object inside of `webpack`'s config. There's several variable that can be inputted here: +- `pluginMode`: plugin classes that are provided by `workbox-webpack-plugin`. There's two plugins that can be used such as: + - `GenerateSW`: Automatically generate a service worker. + - `InjectManifest`: Injecting list of precached assets into a service worker file. If you're using this plugin, `swSrc` must be included in option. +- `serviceWorkerFilename`: The name of the service worker output file. Will be defaulted to `service-worker.js` if this variable left empty. +- `options`: Configuration options that to be added as the plugin's option in `workbox-webpack-plugin`. More info on what options that available to be added can be found here. + + +[workbox-main-page]: https://developers.google.com/web/tools/workbox/ +[workbox-webpack-plugin-module-page]: https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin +[main-concept-build-config]: ./build-config.html \ No newline at end of file diff --git a/docusaurus/website/sidebars.json b/docusaurus/website/sidebars.json index 09d832c..ca4a03b 100644 --- a/docusaurus/website/sidebars.json +++ b/docusaurus/website/sidebars.json @@ -29,7 +29,8 @@ "main-concept/generator", "main-concept/scripts", "main-concept/addons", - "main-concept/typescript" + "main-concept/typescript", + "main-concept/workbox" ], "API Reference": [ "api-reference/overview", diff --git a/example/app-with-workbox/README.md b/example/app-with-workbox/README.md new file mode 100644 index 0000000..cd8e857 --- /dev/null +++ b/example/app-with-workbox/README.md @@ -0,0 +1,3 @@ +# App with Workbox Examples + +This directory contains examples on using workbox in Treats. Separated into two plugins which are GenerateSW and InjectManifest. \ No newline at end of file diff --git a/example/app-with-workbox/generate-sw/package.json b/example/app-with-workbox/generate-sw/package.json new file mode 100644 index 0000000..23ed9bf --- /dev/null +++ b/example/app-with-workbox/generate-sw/package.json @@ -0,0 +1,27 @@ +{ + "name": "app-with-workbox", + "version": "0.0.1", + "description": "My First Treats App", + "license": "ISC", + "scripts": { + "start": "treats start", + "build:client": "treats build --target client", + "build:server": "treats build --target server", + "build": "treats build", + "clean": "treats clean", + "generate": "treats generate", + "analyze:client": "treats build --target client --analyze", + "analyze:server": "treats build --target server --analyze", + "profile": "yarn build:server && node --prof dist/server.js", + "documentation:build": "treats documentation build src/", + "documentation:lint": "treats documentation lint src/", + "documentation:phriction": "treats documentation export src/ --target phriction", + "documentation:flush": "treats documentation flush src/", + "test": "treats test", + "test:watch": "treats test --watchAll", + "test:coverage": "treats test --coverage" + }, + "dependencies": { + "treats": "0.2.0" + } +} diff --git a/example/app-with-workbox/generate-sw/src/_locale/en.json b/example/app-with-workbox/generate-sw/src/_locale/en.json new file mode 100644 index 0000000..2712f04 --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/_locale/en.json @@ -0,0 +1,4 @@ +{ + "welcome_page__description1": "A development kit to make your experience on building fully customizable {React} app with built-in Server-side rendering, Code-splitting, i18n, {Redux} and {GraphQL} sweeter!", + "welcome_page__description2": "To get started, edit {Source} and save to reload, or learn more on our {Documentation}" +} diff --git a/example/app-with-workbox/generate-sw/src/_locale/id.json b/example/app-with-workbox/generate-sw/src/_locale/id.json new file mode 100644 index 0000000..789d228 --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/_locale/id.json @@ -0,0 +1,4 @@ +{ + "welcome_page__description1": "Sebuah development kit untuk membuat pengalaman anda dalam membangun sebuah aplikasi {React} yang sangat mudah dikustomisasi dengan Server-side rendering, Code-splitting, i18n, {Redux} dan {GraphQL} jadi lebih manis!", + "welcome_page__description2": "Untuk memulai, sunting {Source} dan simpan untuk me-reload, atau pelajari lebih lanjut di {Documentation} kami." +} diff --git a/example/app-with-workbox/generate-sw/src/_locale/index.js b/example/app-with-workbox/generate-sw/src/_locale/index.js new file mode 100644 index 0000000..aaa9091 --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/_locale/index.js @@ -0,0 +1,7 @@ +import en from "./en.json"; +import id from "./id.json"; + +export default { + en, + id +}; diff --git a/example/app-with-workbox/generate-sw/src/_route/module.js b/example/app-with-workbox/generate-sw/src/_route/module.js new file mode 100644 index 0000000..6bf78f5 --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/_route/module.js @@ -0,0 +1,9 @@ +import Welcome from "@page/welcome"; + +import { WELCOME } from "./path"; + +const module = { + [WELCOME]: Welcome +}; + +export default module; diff --git a/example/app-with-workbox/generate-sw/src/_route/path.js b/example/app-with-workbox/generate-sw/src/_route/path.js new file mode 100644 index 0000000..de3c00c --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/_route/path.js @@ -0,0 +1 @@ +export const WELCOME = "/"; diff --git a/example/app-with-workbox/generate-sw/src/_route/route.js b/example/app-with-workbox/generate-sw/src/_route/route.js new file mode 100644 index 0000000..fda5c99 --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/_route/route.js @@ -0,0 +1,12 @@ +import { WELCOME } from "./path"; + +const route = [ + { + name: "welcome", + path: WELCOME, + exact: true, + disabled: true + } +]; + +export default route; diff --git a/example/app-with-workbox/generate-sw/src/page/welcome/NotoSans-Bold.ttf b/example/app-with-workbox/generate-sw/src/page/welcome/NotoSans-Bold.ttf new file mode 100644 index 0000000..1db7886 Binary files /dev/null and b/example/app-with-workbox/generate-sw/src/page/welcome/NotoSans-Bold.ttf differ diff --git a/example/app-with-workbox/generate-sw/src/page/welcome/NotoSans-Regular.ttf b/example/app-with-workbox/generate-sw/src/page/welcome/NotoSans-Regular.ttf new file mode 100644 index 0000000..0a01a06 Binary files /dev/null and b/example/app-with-workbox/generate-sw/src/page/welcome/NotoSans-Regular.ttf differ diff --git a/example/app-with-workbox/generate-sw/src/page/welcome/index.js b/example/app-with-workbox/generate-sw/src/page/welcome/index.js new file mode 100644 index 0000000..a2e0a68 --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/page/welcome/index.js @@ -0,0 +1,5 @@ +import AsyncLoader from "@treats/component/async-loader"; + +const Welcome = AsyncLoader({ component: import("./welcome") }); + +export default Welcome; diff --git a/example/app-with-workbox/generate-sw/src/page/welcome/treats.png b/example/app-with-workbox/generate-sw/src/page/welcome/treats.png new file mode 100644 index 0000000..bc3b5f2 Binary files /dev/null and b/example/app-with-workbox/generate-sw/src/page/welcome/treats.png differ diff --git a/example/app-with-workbox/generate-sw/src/page/welcome/welcome.css b/example/app-with-workbox/generate-sw/src/page/welcome/welcome.css new file mode 100644 index 0000000..ee9943c --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/page/welcome/welcome.css @@ -0,0 +1,88 @@ +@font-face { + font-family: "Noto Sans"; + src: url("./NotoSans-Bold.ttf"); + font-weight: bold; +} +@font-face { + font-family: "Noto Sans"; + src: url("./NotoSans-Regular.ttf"); + font-weight: normal; +} + +a { + color: #42b549; + text-decoration: none; +} + +a:hover { + color: #ff5722; +} + +.welcome_page { + position: relative; + display: block; + font-family: "Noto Sans", sans-serif; + text-align: center; + height: 700px; + white-space: nowrap; + padding: 0 30px; +} + +.welcome_page:before { + content: ""; + display: inline-block; + vertical-align: middle; + height: 100%; + width: 0; +} + +.welcome_page__content { + display: inline-block; + vertical-align: middle; + white-space: normal; + width: 100%; +} + +.welcome_page__toped { + width: 200px; +} +.welcome_page__locale_switcher_container { + position: relative; + white-space: nowrap; + padding-top: 20px; + font-size: 12px; +} + +.welcome_page__locale_switcher { + display: inline-block; + vertical-align: middle; + padding: 10px; + min-width: 100px; + background: #fff; + color: #42b549; + border: 1px solid #f0f0f0; +} + +.welcome_page__locale_switcher:hover { + color: #42b549; + background: #EEE; +} + +.welcome_page__locale_switcher:global(.active) { + background: #42b549; + color: #fff; +} + +.welcome_page__locale_switcher:first-child { + border-top-left-radius: 30px; + border-bottom-left-radius: 30px; +} + +.welcome_page__locale_switcher:last-child { + border-top-right-radius: 30px; + border-bottom-right-radius: 30px; +} + +.welcome_page__locale_switcher:global(.active):hover { + color: #FFF; +} diff --git a/example/app-with-workbox/generate-sw/src/page/welcome/welcome.js b/example/app-with-workbox/generate-sw/src/page/welcome/welcome.js new file mode 100644 index 0000000..2e364b5 --- /dev/null +++ b/example/app-with-workbox/generate-sw/src/page/welcome/welcome.js @@ -0,0 +1,82 @@ +import React from "react"; +import Helmet from "@treats/helmet"; +import { FormattedMessage, injectIntl } from "@treats/intl"; +import AsyncComponent from "@treats/component/async-component"; + +import style from "./welcome.css"; +import treats from "./treats.png"; + +/** + * Welcome to treats component + * @param props React props + * @param props.intl Intl Object + * @author Tokopedia Engineering + */ +const Welcome = ({ intl }) => ( +
+ + Welcome to Treats! + +
+ Treats +

Welcome, let's have some treats!

+ + React + + ), + Redux: ( + + Redux + + ), + GraphQL: ( + + GraphQL + + ) + }} + /> +

+ src/page/welcome.js, + Documentation: ( + + documentation + + ) + }} + /> +

+
+ + English + + + Indonesian + +
+
+
+); + +export default AsyncComponent(module, injectIntl(Welcome)); diff --git a/example/app-with-workbox/generate-sw/treats.config.js b/example/app-with-workbox/generate-sw/treats.config.js new file mode 100644 index 0000000..40e73dd --- /dev/null +++ b/example/app-with-workbox/generate-sw/treats.config.js @@ -0,0 +1,23 @@ +const path = require("path"); + +const config = { + app: { + name: "app-with-workbox", + slug: "app-with-workbox" + }, + alias: { + "@page": path.resolve(__dirname, "./src/page") + }, + webpack: { + workbox: { + pluginMode: "GenerateSW", + options: { + swDest: "sw.js", + skipWaiting: true, + clientsClaim: true + } + } + } +}; + +module.exports = config; diff --git a/example/app-with-workbox/inject-manifest/package.json b/example/app-with-workbox/inject-manifest/package.json new file mode 100644 index 0000000..23ed9bf --- /dev/null +++ b/example/app-with-workbox/inject-manifest/package.json @@ -0,0 +1,27 @@ +{ + "name": "app-with-workbox", + "version": "0.0.1", + "description": "My First Treats App", + "license": "ISC", + "scripts": { + "start": "treats start", + "build:client": "treats build --target client", + "build:server": "treats build --target server", + "build": "treats build", + "clean": "treats clean", + "generate": "treats generate", + "analyze:client": "treats build --target client --analyze", + "analyze:server": "treats build --target server --analyze", + "profile": "yarn build:server && node --prof dist/server.js", + "documentation:build": "treats documentation build src/", + "documentation:lint": "treats documentation lint src/", + "documentation:phriction": "treats documentation export src/ --target phriction", + "documentation:flush": "treats documentation flush src/", + "test": "treats test", + "test:watch": "treats test --watchAll", + "test:coverage": "treats test --coverage" + }, + "dependencies": { + "treats": "0.2.0" + } +} diff --git a/example/app-with-workbox/inject-manifest/src/_locale/en.json b/example/app-with-workbox/inject-manifest/src/_locale/en.json new file mode 100644 index 0000000..2712f04 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/_locale/en.json @@ -0,0 +1,4 @@ +{ + "welcome_page__description1": "A development kit to make your experience on building fully customizable {React} app with built-in Server-side rendering, Code-splitting, i18n, {Redux} and {GraphQL} sweeter!", + "welcome_page__description2": "To get started, edit {Source} and save to reload, or learn more on our {Documentation}" +} diff --git a/example/app-with-workbox/inject-manifest/src/_locale/id.json b/example/app-with-workbox/inject-manifest/src/_locale/id.json new file mode 100644 index 0000000..789d228 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/_locale/id.json @@ -0,0 +1,4 @@ +{ + "welcome_page__description1": "Sebuah development kit untuk membuat pengalaman anda dalam membangun sebuah aplikasi {React} yang sangat mudah dikustomisasi dengan Server-side rendering, Code-splitting, i18n, {Redux} dan {GraphQL} jadi lebih manis!", + "welcome_page__description2": "Untuk memulai, sunting {Source} dan simpan untuk me-reload, atau pelajari lebih lanjut di {Documentation} kami." +} diff --git a/example/app-with-workbox/inject-manifest/src/_locale/index.js b/example/app-with-workbox/inject-manifest/src/_locale/index.js new file mode 100644 index 0000000..aaa9091 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/_locale/index.js @@ -0,0 +1,7 @@ +import en from "./en.json"; +import id from "./id.json"; + +export default { + en, + id +}; diff --git a/example/app-with-workbox/inject-manifest/src/_route/module.js b/example/app-with-workbox/inject-manifest/src/_route/module.js new file mode 100644 index 0000000..6bf78f5 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/_route/module.js @@ -0,0 +1,9 @@ +import Welcome from "@page/welcome"; + +import { WELCOME } from "./path"; + +const module = { + [WELCOME]: Welcome +}; + +export default module; diff --git a/example/app-with-workbox/inject-manifest/src/_route/path.js b/example/app-with-workbox/inject-manifest/src/_route/path.js new file mode 100644 index 0000000..de3c00c --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/_route/path.js @@ -0,0 +1 @@ +export const WELCOME = "/"; diff --git a/example/app-with-workbox/inject-manifest/src/_route/route.js b/example/app-with-workbox/inject-manifest/src/_route/route.js new file mode 100644 index 0000000..fda5c99 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/_route/route.js @@ -0,0 +1,12 @@ +import { WELCOME } from "./path"; + +const route = [ + { + name: "welcome", + path: WELCOME, + exact: true, + disabled: true + } +]; + +export default route; diff --git a/example/app-with-workbox/inject-manifest/src/page/welcome/NotoSans-Bold.ttf b/example/app-with-workbox/inject-manifest/src/page/welcome/NotoSans-Bold.ttf new file mode 100644 index 0000000..1db7886 Binary files /dev/null and b/example/app-with-workbox/inject-manifest/src/page/welcome/NotoSans-Bold.ttf differ diff --git a/example/app-with-workbox/inject-manifest/src/page/welcome/NotoSans-Regular.ttf b/example/app-with-workbox/inject-manifest/src/page/welcome/NotoSans-Regular.ttf new file mode 100644 index 0000000..0a01a06 Binary files /dev/null and b/example/app-with-workbox/inject-manifest/src/page/welcome/NotoSans-Regular.ttf differ diff --git a/example/app-with-workbox/inject-manifest/src/page/welcome/index.js b/example/app-with-workbox/inject-manifest/src/page/welcome/index.js new file mode 100644 index 0000000..a2e0a68 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/page/welcome/index.js @@ -0,0 +1,5 @@ +import AsyncLoader from "@treats/component/async-loader"; + +const Welcome = AsyncLoader({ component: import("./welcome") }); + +export default Welcome; diff --git a/example/app-with-workbox/inject-manifest/src/page/welcome/treats.png b/example/app-with-workbox/inject-manifest/src/page/welcome/treats.png new file mode 100644 index 0000000..bc3b5f2 Binary files /dev/null and b/example/app-with-workbox/inject-manifest/src/page/welcome/treats.png differ diff --git a/example/app-with-workbox/inject-manifest/src/page/welcome/welcome.css b/example/app-with-workbox/inject-manifest/src/page/welcome/welcome.css new file mode 100644 index 0000000..ee9943c --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/page/welcome/welcome.css @@ -0,0 +1,88 @@ +@font-face { + font-family: "Noto Sans"; + src: url("./NotoSans-Bold.ttf"); + font-weight: bold; +} +@font-face { + font-family: "Noto Sans"; + src: url("./NotoSans-Regular.ttf"); + font-weight: normal; +} + +a { + color: #42b549; + text-decoration: none; +} + +a:hover { + color: #ff5722; +} + +.welcome_page { + position: relative; + display: block; + font-family: "Noto Sans", sans-serif; + text-align: center; + height: 700px; + white-space: nowrap; + padding: 0 30px; +} + +.welcome_page:before { + content: ""; + display: inline-block; + vertical-align: middle; + height: 100%; + width: 0; +} + +.welcome_page__content { + display: inline-block; + vertical-align: middle; + white-space: normal; + width: 100%; +} + +.welcome_page__toped { + width: 200px; +} +.welcome_page__locale_switcher_container { + position: relative; + white-space: nowrap; + padding-top: 20px; + font-size: 12px; +} + +.welcome_page__locale_switcher { + display: inline-block; + vertical-align: middle; + padding: 10px; + min-width: 100px; + background: #fff; + color: #42b549; + border: 1px solid #f0f0f0; +} + +.welcome_page__locale_switcher:hover { + color: #42b549; + background: #EEE; +} + +.welcome_page__locale_switcher:global(.active) { + background: #42b549; + color: #fff; +} + +.welcome_page__locale_switcher:first-child { + border-top-left-radius: 30px; + border-bottom-left-radius: 30px; +} + +.welcome_page__locale_switcher:last-child { + border-top-right-radius: 30px; + border-bottom-right-radius: 30px; +} + +.welcome_page__locale_switcher:global(.active):hover { + color: #FFF; +} diff --git a/example/app-with-workbox/inject-manifest/src/page/welcome/welcome.js b/example/app-with-workbox/inject-manifest/src/page/welcome/welcome.js new file mode 100644 index 0000000..2e364b5 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/page/welcome/welcome.js @@ -0,0 +1,82 @@ +import React from "react"; +import Helmet from "@treats/helmet"; +import { FormattedMessage, injectIntl } from "@treats/intl"; +import AsyncComponent from "@treats/component/async-component"; + +import style from "./welcome.css"; +import treats from "./treats.png"; + +/** + * Welcome to treats component + * @param props React props + * @param props.intl Intl Object + * @author Tokopedia Engineering + */ +const Welcome = ({ intl }) => ( +
+ + Welcome to Treats! + +
+ Treats +

Welcome, let's have some treats!

+ + React + + ), + Redux: ( + + Redux + + ), + GraphQL: ( + + GraphQL + + ) + }} + /> +

+ src/page/welcome.js, + Documentation: ( + + documentation + + ) + }} + /> +

+
+ + English + + + Indonesian + +
+
+
+); + +export default AsyncComponent(module, injectIntl(Welcome)); diff --git a/example/app-with-workbox/inject-manifest/src/service-worker/sw.js b/example/app-with-workbox/inject-manifest/src/service-worker/sw.js new file mode 100644 index 0000000..2b7dd7c --- /dev/null +++ b/example/app-with-workbox/inject-manifest/src/service-worker/sw.js @@ -0,0 +1,9 @@ +workbox.core.skipWaiting(); +workbox.core.clientsClaim(); + +workbox.routing.registerRoute( + new RegExp("http://www.test.com"), + new workbox.strategies.StaleWhileRevalidate() +); + +workbox.precaching.precacheAndRoute(self.__precacheManifest); \ No newline at end of file diff --git a/example/app-with-workbox/inject-manifest/treats.config.js b/example/app-with-workbox/inject-manifest/treats.config.js new file mode 100644 index 0000000..599e4e0 --- /dev/null +++ b/example/app-with-workbox/inject-manifest/treats.config.js @@ -0,0 +1,22 @@ +const path = require("path"); + +const config = { + app: { + name: "app-with-workbox", + slug: "app-with-workbox" + }, + alias: { + "@page": path.resolve(__dirname, "./src/page") + }, + webpack: { + workbox: { + pluginMode: "InjectManifest", + options: { + // swSrc option is mandatory if 'injectManifest' plugin is used + swSrc: "./src/service-worker/sw.js" + } + } + } +}; + +module.exports = config; diff --git a/packages/treats/client/index.js b/packages/treats/client/index.js index 72ccac5..d94ba79 100644 --- a/packages/treats/client/index.js +++ b/packages/treats/client/index.js @@ -42,7 +42,8 @@ const initClient = params => { navigator.language || navigator.userLanguage || "en-US", - guardedRootDiv = rootDiv || "treats-root"; + guardedRootDiv = rootDiv || "treats-root", + swScript = document.querySelectorAll("link[data-treats-sw-script]"); let reduxStore, apolloConfig, apolloClient; @@ -65,6 +66,21 @@ const initClient = params => { language }; + if (swScript.length > 0 && "serviceWorker" in navigator) { + const serviceWorkerPath = swScript[0].getAttribute("src"); + window.addEventListener("load", () => { + navigator.serviceWorker + .register(serviceWorkerPath, { scope: "./" }) + .then(registration => + //eslint-disable-next-line no-console + console.log( + `Service worker registration on ${serviceWorkerPath} is successfull`, + registration + )) + .catch(error => console.error("Service worker registrations failed", error)); + }); + } + loadLocaleData(language).then(messages => hydrate( diff --git a/packages/treats/package.json b/packages/treats/package.json index 5897bb0..0b48048 100644 --- a/packages/treats/package.json +++ b/packages/treats/package.json @@ -129,6 +129,7 @@ "webpack-merge": "4.1.4", "webpack-source-map-support": "2.0.1", "winston": "2.4.2", + "workbox-webpack-plugin": "4.1.0", "yargs": "12.0.1", "yarn-install": "1.0.0" } diff --git a/packages/treats/scripts/config/util/workbox.js b/packages/treats/scripts/config/util/workbox.js new file mode 100644 index 0000000..df7922e --- /dev/null +++ b/packages/treats/scripts/config/util/workbox.js @@ -0,0 +1,35 @@ +const WorkboxWebpackPlugin = require("workbox-webpack-plugin"), + logger = require("../../util/logger"); + +const cancelWorkboxInit = () => { + logger("warn", "Cancelling workbox initialization"); + return []; +}; + +const configureWorkbox = workboxConfig => { + const { pluginMode, options } = workboxConfig; + + if (!(pluginMode in WorkboxWebpackPlugin)) { + logger("warn", `${pluginMode} is not a valid workblox plugin.`); + return cancelWorkboxInit(); + } + + if (pluginMode === "InjectManifest") { + const { swSrc } = options, + isSwSrcExist = !!swSrc; + + if (!isSwSrcExist) { + logger("warn", "swSrc is not allowed to be empty in InjectManifest mode."); + return cancelWorkboxInit(); + } + } + + return [new WorkboxWebpackPlugin[pluginMode](options)]; +}; + +const getSWFilename = workboxConfig => workboxConfig.serviceWorkerFilename || "service-worker.js"; + +module.exports = { + configureWorkbox, + getSWFilename +}; diff --git a/packages/treats/scripts/config/webpack.config.client.build.js b/packages/treats/scripts/config/webpack.config.client.build.js index 63151bd..9cbf870 100644 --- a/packages/treats/scripts/config/webpack.config.client.build.js +++ b/packages/treats/scripts/config/webpack.config.client.build.js @@ -8,7 +8,8 @@ const webpack = require("webpack"), webpackMerge = require("webpack-merge"), babelMerge = require("babel-merge"), extractEnv = require("./util/extract-env"), - useTypescript = fs.pathExistsSync(path.resolve(process.cwd(), "./tsconfig.json")); + useTypescript = fs.pathExistsSync(path.resolve(process.cwd(), "./tsconfig.json")), + { configureWorkbox, getSWFilename } = require("./util/workbox"); module.exports = ({ alias, @@ -22,12 +23,18 @@ module.exports = ({ wdsPort, webpack: { define: webpackDefineEnv, op: webpackOp } } = extractEnv(process.env), + { workbox: workboxConfig } = webpackConfig, publicPath = webpackConfig.publicPath || "/static/", clientOutputPath = webpackConfig.clientOutputPath || "public", resolve = { extensions: [".ts", ".tsx", ".js", ".css"] }; + let workboxPlugin = []; + if (workboxConfig) { + workboxPlugin = configureWorkbox(workboxConfig); + } + const bundleAnalyzerPlugin = webpackOp === "analyze" ? [new BundleAnalyzerPlugin()] : []; const defaultConfig = { @@ -241,6 +248,7 @@ module.exports = ({ orderWarning: true, // Disable to remove warnings about conflicting order between imports cssModules: true // if you use cssModules, this can help. }), + ...workboxPlugin, { apply: compiler => { compiler.plugin("after-emit", (compilation, done) => { @@ -264,6 +272,12 @@ module.exports = ({ publicPath: true }); delete stats.assets; + if (workboxPlugin.length > 0) { + stats.assetsByChunkName = { + ...stats.assetsByChunkName, + "service-worker": workboxPlugin[0].config.swDest + }; + } fs.outputFile("stats/stats.json", JSON.stringify(stats), done); }); } diff --git a/packages/treats/scripts/config/webpack.config.client.development.js b/packages/treats/scripts/config/webpack.config.client.development.js index f01f965..3aef726 100644 --- a/packages/treats/scripts/config/webpack.config.client.development.js +++ b/packages/treats/scripts/config/webpack.config.client.development.js @@ -7,7 +7,8 @@ const webpack = require("webpack"), webpackMerge = require("webpack-merge"), babelMerge = require("babel-merge"), extractEnv = require("./util/extract-env"), - useTypescript = fs.pathExistsSync(path.resolve(process.cwd(), "./tsconfig.json")); + useTypescript = fs.pathExistsSync(path.resolve(process.cwd(), "./tsconfig.json")), + { configureWorkbox, getSWFilename } = require("./util/workbox"); module.exports = ({ alias, @@ -21,12 +22,17 @@ module.exports = ({ wdsPort, webpack: { define: webpackDefineEnv } } = extractEnv(process.env), + { workbox: workboxConfig } = webpackConfig, publicPath = webpackConfig.publicPath || "/__TREATS_WDS__/", assetsOutputPath = webpackConfig.assetsOutputPath || "public", resolve = { extensions: [".ts", ".tsx", ".js", ".css", ".json", ".wasm", ".mjs"] }; + let workboxPlugin = []; + if (workboxConfig) { + workboxPlugin = configureWorkbox(workboxConfig); + } const defaultConfig = { name: "client", target: "web", @@ -255,6 +261,7 @@ module.exports = ({ reloadAll: true, // when desperation kicks in - this is a brute force HMR flag cssModules: true // if you use cssModules, this can help. }), + ...workboxPlugin, { apply: compiler => { compiler.plugin("after-emit", (compilation, done) => { @@ -278,6 +285,12 @@ module.exports = ({ publicPath: true }); delete stats.assets; + if (workboxPlugin.length > 0) { + stats.assetsByChunkName = { + ...stats.assetsByChunkName, + "service-worker": workboxPlugin[0].config.swDest + }; + } fs.outputFile("stats/stats.json", JSON.stringify(stats), done); }); } @@ -291,7 +304,8 @@ module.exports = ({ hot: true, open: true, headers: { - "Access-Control-Allow-Origin": "*" + "Access-Control-Allow-Origin": "*", + "Service-Worker-Allowed": "/" }, stats: { colors: true, children: false }, overlay: true, diff --git a/packages/treats/server/const.js b/packages/treats/server/const.js index 66109a0..2c9197a 100644 --- a/packages/treats/server/const.js +++ b/packages/treats/server/const.js @@ -17,3 +17,9 @@ export const DEFAULT_ENV = { * Treats assets path. */ export const ASSETS_PATH = path.join(__dirname, "../public"); + +export const JS_TAG_TEMPLATE = { + "service-worker": filepath => + ``, + default: filepath => `` +}; diff --git a/packages/treats/server/helper/assets/index.js b/packages/treats/server/helper/assets/index.js index 0e0329e..8b6651d 100644 --- a/packages/treats/server/helper/assets/index.js +++ b/packages/treats/server/helper/assets/index.js @@ -2,7 +2,7 @@ import type { $Application } from "express"; import type { HelperInterfaceType } from "@treats/flow-typed/helper"; import fs from "fs-extra"; - +import { isArray } from "@treats/util/typecheck"; import directory from "./directory"; type AssetsHelperType = { @@ -13,7 +13,7 @@ type AssetsHelperType = { }; let chokidar; -if(process.env.NODE_ENV === "development") { +if (process.env.NODE_ENV === "development") { chokidar = require("chokidar"); } @@ -26,6 +26,26 @@ const assets: AssetsHelperType = { `[Assets] Trying to get Asset Stats from ${directory.STATS_DIR}/stats.json` ); const assetsStats = JSON.parse(fs.readFileSync(`${directory.STATS_DIR}/stats.json`)); + if (assetsStats.assetsByChunkName) { + assetsStats.chunkNameByAssets = Object.keys(assetsStats.assetsByChunkName).reduce( + (acc: Object, chunkName: string | Array): Object => { + const currentChunk = assetsStats.assetsByChunkName[chunkName]; + if (isArray(currentChunk)) { + currentChunk.forEach((chunkAsset: string) => { + acc[chunkAsset] = chunkName; + }); + } else { + acc[currentChunk] = chunkName; + } + return acc; + }, + {} + ); + } else { + console.warn( + "[Assets] assetsByChunkName is not defined, your stats might be corrupted!" + ); + } this.stats = assetsStats; console.verbose("[Assets] Get Asset Stats finished"); } catch (err) { @@ -54,15 +74,17 @@ const assets: AssetsHelperType = { this.set(); if (process.env.NODE_ENV === "development" && this.stats) { const openBrowser = require("react-dev-utils/openBrowser"); - if(!process.env.TREATS_BROWSER_OPEN) { + if (!process.env.TREATS_BROWSER_OPEN) { process.env.TREATS_BROWSER_OPEN = true; openBrowser(`${process.env.TREATS_HOST}:${process.env.TREATS_PORT}`); } } - const watcher = chokidar.watch(`${directory.STATS_DIR}/stats.json`, { ignoreInitial: true }).on("all", () => { - console.verbose("Stats File changed, reloading..."); - this.set(); - }); + const watcher = chokidar + .watch(`${directory.STATS_DIR}/stats.json`, { ignoreInitial: true }) + .on("all", () => { + console.verbose("Stats File changed, reloading..."); + this.set(); + }); clearInterval(getStatsInterval); return; } else if (retries > 50) { diff --git a/packages/treats/server/renderer.js b/packages/treats/server/renderer.js index 1c9f733..b3e6dc5 100644 --- a/packages/treats/server/renderer.js +++ b/packages/treats/server/renderer.js @@ -13,6 +13,7 @@ import Provider from "@treats/component/provider"; import locale from "@@BUILD_LOCALE_PATH@@"; import App from "@@BUILD_REACT_APP_PATH@@"; import templates from "./template"; +import { JS_TAG_TEMPLATE } from "./const"; let withReduxStore, withApolloClient, @@ -85,6 +86,21 @@ const renderReactMarkup = (reactApp, customRenderers) => { return result; }; +//Custom js files tag generator based on webpack-flush-chunks API to modify service worker script tag +const jsTagGenerator = (flushedChunks, assetsStats) => { + const { scripts: jsFiles, publicPath } = flushedChunks, + { chunkNameByAssets } = assetsStats; + return jsFiles + .map(fileName => { + const chunkName = chunkNameByAssets[fileName], + filePath = `${publicPath}/${fileName}`; + return JS_TAG_TEMPLATE[chunkName] + ? JS_TAG_TEMPLATE[chunkName](filePath) + : JS_TAG_TEMPLATE.default(filePath); + }) + .join("\n"); +}; + /** * Renderer main function * @param req req object @@ -173,11 +189,12 @@ const renderer = async (req, res, routerContext, customRenderers) => { { template } = req.renderParams, flushedChunks = flushChunks(assetsStats, { chunkNames, - before: ["manifest", "vendor"], + before: ["manifest", "vendor", "service-worker"], after: ["main"] }), - { styles, js, cssHash: css } = flushedChunks; - const jsTags = js.toString(), + { styles, cssHash: css } = flushedChunks; + + const jsTags = jsTagGenerator(flushedChunks, assetsStats), cssTags = styles.toString(), cssHash = css.toString(), helmetData = Helmet.renderStatic(),