<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Kirill Konshin on Medium]]></title>
        <description><![CDATA[Stories by Kirill Konshin on Medium]]></description>
        <link>https://medium.com/@kirill.konshin?source=rss-133065105ba4------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*Sw353ElbPMN4PNYmU9iPIQ.jpeg</url>
            <title>Stories by Kirill Konshin on Medium</title>
            <link>https://medium.com/@kirill.konshin?source=rss-133065105ba4------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 02:31:02 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@kirill.konshin/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[The ultimate Electron app with Next.js and React Server Components]]></title>
            <link>https://medium.com/@kirill.konshin/the-ultimate-electron-app-with-next-js-and-react-server-components-a5c0cabda72b?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/a5c0cabda72b</guid>
            <category><![CDATA[electron]]></category>
            <category><![CDATA[react-server-component]]></category>
            <category><![CDATA[reactjs]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[nextjs]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Mon, 21 Oct 2024 07:04:09 GMT</pubDate>
            <atom:updated>2024-10-22T17:00:46.426Z</atom:updated>
            <cc:license>http://creativecommons.org/licenses/by/4.0/</cc:license>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*ZYgHxkkKlNK0t3AKeHEtfQ.png" /></figure><p>With the emergence of <a href="https://react.dev/reference/rsc/server-components">React Server Components</a> and <a href="https://react.dev/reference/rsc/server-actions">Server Actions</a> writing Web apps became easier than ever. The simplicity when developer has all server APIs right inside the Web app, natively, with types and full support from Next.js framework for example (and other RSC frameworks too, of course) is astonishing.</p><p>At the same time, Electron is a de-facto standard for modern desktop apps written using web technologies, especially when application must have filesystem and other system API access, while being written in JS (<a href="https://tauri.app/">Tauri</a> receives an honorable mention here if you know Rust or if you only need a simple WebView2 shell).</p><blockquote><em>I asked myself, why not to combine best of both worlds, and run usual Next.js application right inside the Electron and enjoy all benefits that comes with React Server Components?</em></blockquote><h3>Demo</h3><p>I have explored all options available and haven’t found a suitable one, so I wrote a <a href="https://www.npmjs.com/package/next-electron-rsc">small lib</a> <a href="https://www.npmjs.com/package/next-electron-rsc">next-electron-rsc</a> that can bridge the gap between Next.js and Electron without running a server or opening any ports.</p><p>All you need to use the lib is to add following to your main.js in Electron:</p><pre>import { app, protocol } from &#39;electron&#39;;<br>import { createHandler } from &#39;next-electron-rsc&#39;;<br><br>const appPath = app.getAppPath();<br>const isDev = process.env.NODE_ENV === &#39;development&#39;;<br><br>const { createInterceptor } = createHandler({<br>    standaloneDir: path.join(appPath, &#39;.next&#39;, &#39;standalone&#39;),<br>    localhostUrl: &#39;&lt;http://localhost:3000&gt;&#39;, // must match Next.js dev server<br>    protocol,<br>});<br><br>if (!isDev) createInterceptor();</pre><p>And configure your Next.js build in next.config.js:</p><pre>module.exports = {<br>  output: &#39;standalone&#39;,<br>  experimental: {<br>    outputFileTracingIncludes: {<br>      &#39;*&#39;: [<br>          &#39;public/**/*&#39;,<br>          &#39;.next/static/**/*&#39;,<br>      ],<br>    },<br>  },<br>};</pre><p>Here’s the repository: <a href="https://github.com/kirill-konshin/next-electron-rsc">https://github.com/kirill-konshin/next-electron-rsc</a> and the <a href="https://github.com/kirill-konshin/next-electron-rsc/tree/main/demo">demo</a> with all files.</p><p>And here’s my journey to create <a href="https://github.com/kirill-konshin/next-electron-rsc/blob/main/lib/src/index.ts">this lib</a>.</p><h3>Motivation to use React Server Components in Electron</h3><p>Electron’s native way of providing access to system APIs is via IPC or, god forbid, Electron Remote (which was considered even <a href="https://nornagon.medium.com/electrons-remote-module-considered-harmful-70d69500f31">harmful</a>). Both were always a bit cumbersome. Don’t get me wrong, you can get the job done: <a href="https://blog.logrocket.com/electron-ipc-response-request-architecture-with-typescript/">this</a> and <a href="https://github.com/JichouP/electron-typescript-ipc">this</a> typed IPC interfaces were the best I found. But with IPC in large apps you’ll end up designing some handshake protocol for simple request-response interaction, handling of errors and loading states and so on, so in real enterprise grade applications it will quickly become too heavy. Not even close to elegance of RSC.</p><p>Important benefit of React Server Components in traditional client-server web development has same nature: absence of dedicated RESTful API or GraphQL API (if the only API consumer is the website itself). So developer does not need to design these APIs, maintain them, and app can just and just talk to backend as if it’s just another async function.</p><p>With RSC application all logic can be colocated in the Web app, so Electron itself becomes a very thin layer, that just opens a window.</p><p>Here’s an example, we use Electron’s safe storage and read from/write to file system right in the React Component:</p><pre>import { safeStorage } from &#39;electron&#39;;<br>import Preview from &#39;./text&#39;;<br>import fs from &#39;fs/promises&#39;;<br><br>async function Page({page}) {<br>  const secretText = await safeStorage.decryptString(await fs.readFile(&#39;path-to-file&#39;));<br>  <br>  async function save(newText) {<br>    fs.writeFile(&#39;path-to-file&#39;, await safeStorage.encryptString(newText));<br>  }<br>  <br>  return &lt;Preview secretText={secretText} save={save} /&gt;;<br>}</pre><p>Such colocation allows much more rapid development and much less maintenance of the protocol between Web and Electron apps. And of course you can use Electron APIs directly from server components, as it’s the same Node.js process, thus removing the necessity to use IPC or Remote, or any sort of client-server API protocol like REST or GQL.</p><p>Basically, this magically removes the boundary between Electron’s Renderer and Main processes, while still keeping everything secure. Besides you can shift execution of heavy tasks from browser to Node.js, which is more flexible how you distribute the load. The only problem is… you need to run an RSC server in Electron. Or do you?</p><h3>Requirements</h3><p>I had a few and very strict requirements that I wanted to achieve:</p><ol><li>No open ports! Safety first.</li><li>Complete support Next.js: React Server Components, API Routes (App router) and Server Side Rendering, Static Site Rendering and Route Handlers (Pages router), you name it, with strict adherence established patterns</li><li>Minimal, easy to use, based on standards, basically an enterprise-grade, production ready stack for commercial use, mature and well known set of technologies</li><li>Performance</li></ol><p>After some research I found an obvious choice called <a href="https://github.com/saltyshiomix/nextron">Nextron</a>. Unfortunately, seems like it does not utilize the full power of Next.js, and <a href="https://github.com/saltyshiomix/nextron/issues/263">does not support SSR</a> (ticket remained open in Oct 2024). On the other hand there are articles like <a href="https://www.saybackend.com/blog/03-electron-nextjs-ssr/">this</a> or <a href="https://www.58bits.com/blog/nextjs-14-app-router-custom-server-express">this</a>, both very close, except for usage of server with an open port. <em>Unfortunately I only found it after I came up with the approach I’m about to present, but the article validated it. Luckily I found it before writing this post, so I can give kudos to the author here.</em></p><p>So I started exploring on my own. Turned out, the approach is pretty simple. And all the tools are already available, I only needed to wire them together in some unorthodox way.</p><h3>Next.js</h3><p>First step would be to <a href="https://nextjs.org/docs/pages/api-reference/next-config-js/output#automatically-copying-traced-files">build Next.js app as a standalone</a>. This will create an optimized build which contains all modules and files that can possibly be required in runtime, and removes everything that’s unnecessary.</p><pre>module.exports = {<br>  output: &#39;standalone&#39;,<br>  experimental: {<br>    outputFileTracingIncludes: {<br>      &#39;*&#39;: [<br>          &#39;public/**/*&#39;,<br>          &#39;.next/static/**/*&#39;,<br>      ],<br>    },<br>  },<br>};</pre><p>Aaand, this is it for Next.js.</p><p>outputFileTracingIncludes is needed so that optional public and .next/static folders will be copied to standalone build. Next.js assumes you should publish this to CDN, but in this case everything is local.</p><p>Next step is a little trickier.</p><h3>Electron</h3><p>Now I need to let Electron know that I have Next.js.</p><p>One possible solution is Electron’s Custom Protocol or Schema. Or a Protocol Intercept. I chose the latter as I’m perfectly fine to pretend to load web from <a href="http://localhost/">http://localhost</a> (emphasis on pretend as there should be no real server with an open port).</p><p>Besides, this also ensures relaxed policy of one “popular video service”, that forbids embedding on pages served via custom protocols 😅.</p><p>Please note that I purposely excluded a lot of unnecessary code to focus on what matters to show the concept.</p><p>To implement the intercept I added following:</p><pre>const localhostUrl = &#39;&lt;http://localhost:3000&gt;&#39;;<br><br>function createInterceptor() {<br>    protocol.interceptStreamProtocol(&#39;http&#39;, async (request, callback) =&gt; {<br>        if (!request.url.startsWith(localhostUrl)) return;<br>        try {<br>            const response = await handleRequest(request);<br>            callback(response);<br>        } catch (e) {<br>            callback(e);<br>        }<br>    });<br>}</pre><p>This interceptor serves static files and forwards requests to Next.js.</p><p>Honorable mention here goes to awesome <a href="https://github.com/sindresorhus/electron-serve">Electron Serve</a>, which implements a custom schema for serving static files.</p><h3>Bridging Electron and Next.js</h3><p>Next step would be to create a file to provide some convenience to use the non-existing port-less “server”:</p><pre>import type { ProtocolRequest, ProtocolResponse } from &#39;electron&#39;;<br>import { IncomingMessage } from &#39;node:http&#39;;<br>import { Socket } from &#39;node:net&#39;;<br><br>function createRequest({ socket, origReq }: { socket: Socket; origReq: ProtocolRequest }): IncomingMessage {<br>    const req = new IncomingMessage(socket);<br>    req.url = origReq.url;<br>    req.method = origReq.method;<br>    req.headers = origReq.headers;<br>    origReq.uploadData?.forEach((item) =&gt; {<br>        req.push(item.bytes);<br>    });<br>    req.push(null);<br>    return req;<br>}</pre><p>createRequest uses a Socket to create an instance of Node.js IncomingMessage, then it transfers the information from Electron’s ProtocolRequest into the IncomingMessage, including the body of POST|PUT requests.</p><pre>import { ServerResponse, IncomingMessage } from &#39;node:http&#39;;<br>import { PassThrough } from &#39;node:stream&#39;;<br>import type { Protocol, ProtocolRequest, ProtocolResponse } from &#39;electron&#39;;<br><br>class ReadableServerResponse extends ServerResponse {<br>    private passThrough = new PassThrough();<br>    private promiseResolvers = Promise.withResolvers&lt;ProtocolResponse&gt;();<br>    constructor(req: IncomingMessage) {<br>        super(req);<br>        this.write = this.passThrough.write.bind(this.passThrough);<br>        this.end = this.passThrough.end.bind(this.passThrough);<br>        this.passThrough.on(&#39;drain&#39;, () =&gt; this.emit(&#39;drain&#39;));<br>    }<br>    writeHead(statusCode: number, ...args: any): this {<br>        super.writeHead(statusCode, ...args);<br>        this.promiseResolvers.resolve({<br>            statusCode: this.statusCode,<br>            mimeType: this.getHeader(&#39;Content-Type&#39;) as any,<br>            headers: this.getHeaders() as any,<br>            data: this.passThrough as any,<br>        });<br>        return this;<br>    }<br>    async createProtocolResponse() {<br>        return this.promiseResolvers.promise;<br>    }<br>}</pre><p>ReadableServerResponse is basically just a regular Node.js ServerResponse from which I can read the body once Next.js finishes the processing. createProtocolResponse converts the ReadableServerResponse into Electron’s ProtocolResponse.</p><p>createProtocolResponse method returns a Promise which waits for the body and resolves into converted ReadableServerResponse as ProtocolResponse.</p><p>Next step is finally the “server” itself.</p><h3>No server, no ports</h3><pre>import type { ProtocolRequest, ProtocolResponse } from &#39;electron&#39;;<br><br>export function createHandler({<br>    standaloneDir,<br>    localhostUrl = &#39;&lt;http://localhost:3000&gt;&#39;,<br>    protocol,<br>    debug = false,<br>}) {<br>    const next = require(resolve.sync(&#39;next&#39;, { basedir: standaloneDir }));<br><br>    const app = next({<br>        dev: false,<br>        dir: standaloneDir,<br>    }) as NextNodeServer;<br><br>    const handler = app.getRequestHandler();<br><br>    const socket = new Socket();<br><br>    async function handleRequest(origReq: ProtocolRequest): Promise&lt;ProtocolResponse&gt; {<br>        try {<br>            const req = createRequest({ socket, origReq });<br>            const res = new ReadableServerResponse(req);<br>            const url = parse(req.url, true);<br><br>            handler(req, res, url);<br><br>            return await res.createProtocolResponse();<br>        } catch (e) {<br>            return e;<br>        }<br>    }<br><br>    function createInterceptor() { /* ... */ }<br><br>    return { createInterceptor };<br>}</pre><p>I use the NextServer from Next.js app’s standalone build to create a handler, a regular Express-like route handler which takes Request and Response as arguments.</p><p>Key function here is handleRequest.</p><p>It provides a dummy Socket to createRequest to create a dummy IncomingMessage, creates a dummy ReadableServerResponse. I feed both request and response to Next.js’s handler, so Next.js can work its magic, not knowing that there’s no actual server, just dummy mocks. Once handler finishes its job the ProtocolResponse is ready for Electron to send to browser. And this is it.</p><p>Note that I don’t actually start the Next.js or any other server anywhere, so <strong>Requirement #1</strong> is achieved, no ports are open. You can take a look at <a href="https://nextjs.org/docs/pages/building-your-application/configuring/custom-server">Next.js documentation</a> to learn more about regular way of setting up a handler with the server. And since I use regular Next.js way, <strong>Requirement #2</strong> is achieved.</p><p>And since this whole approach works fine on highly loaded servers, and with Electron there’s just one user at any time, the performance <strong>Requirement #4</strong> is achieved as well.</p><h3>Bundling and publishing</h3><p>I suggest to use <a href="https://electron.build/">Electron Builder</a> to bundle the Electron app. Just add some configuration to electron-builder.yml:</p><pre>includeSubNodeModules: true<br><br>files:<br>  - build<br>  - from: &#39;.next/standalone/demo/&#39;<br>    to: &#39;.next/standalone/demo/&#39;</pre><p>For convenience, you can add following scripts to package.json:</p><pre>{<br>  &quot;scripts&quot;: {<br>    &quot;build&quot;: &quot;yarn build:next &amp;&amp; yarn build:electron&quot;,<br>    &quot;build:next&quot;: &quot;next build&quot;,<br>    &quot;build:electron&quot;: &quot;electron-builder --config electron-builder.yml&quot;,<br>    &quot;start:next&quot;: &quot;next dev&quot;,<br>    &quot;start:electron&quot;: &quot;electron .&quot;<br>  }<br>}</pre><p>For separation of concerns I recommend to keep Next.js sources in src of and Electron soures in and src-electron, this ensures Next.js does not try to compile Electron.</p><h3>Conclusion</h3><p><strong>Requirement #3</strong> is achieved in full glory since it’s just one file, and it only uses standard APIs.</p><p>I was amazed when it actually worked… I was quite skeptical that it would be this simple and yet so elegant.</p><p>Now I can enjoy full access to file &amp; operating systems directly from Next.js Server Components or Route Handlers, with all the benefits of Next.js ecosystem, established patterns, and while using Electron to deliver the complete app experience to users, since the app can be bundled and published.</p><p>P.S. I have done my due diligence and I have not found any articles that cover usage of Next.js with mocked requests and responses, especially in conjunction with Electron. Shame on me if otherwise, I must have forgotten how to Google 🤓… But even if I missed something, this article should help to explain why this approach is good.</p><p>P.P.S. <a href="https://mswjs.io/">MSW</a> is a bit overkill and is used for different purposes, like other HTTP mocking libraries.</p><p>P.P.P.S. Few shady things in the code are using buffers to read response and synchronous reading static files, both can be improved with streaming, but for simplicity it’s good enough.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a5c0cabda72b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web Apps: Micro Frontend framework with support of Webpack Module Federation]]></title>
            <link>https://medium.com/ringcentral-developers/web-apps-micro-frontend-framework-with-support-of-webpack-module-federation-376e0b1ea6c6?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/376e0b1ea6c6</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[vue]]></category>
            <category><![CDATA[angular]]></category>
            <category><![CDATA[micro-frontends]]></category>
            <category><![CDATA[microservices]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Mon, 15 Jun 2020 17:13:43 GMT</pubDate>
            <atom:updated>2020-06-15T21:09:49.988Z</atom:updated>
            <content:encoded><![CDATA[<h3>Web Apps: Micro Frontend framework with support of Module Federation</h3><p>I would like to present a multi-framework, multi-bundle orchestration layer, supporting the oldest to the newest technologies of micro frontends. Framework leverages the power of Webpack to work hand in hand to coordinate and route multi-bundle applications without the overhead.</p><p>These days micro frontends are <a href="https://www.thoughtworks.com/radar/techniques?blipid=1035">hot and trendy</a>. Here’s how <a href="https://martinfowler.com/articles/micro-frontends.html#InANutshell">Martin Fowler defines</a> them:</p><blockquote>In short, micro frontends are all about slicing up big and scary things into smaller, more manageable pieces, and then being explicit about the dependencies between them. Our technology choices, our codebases, our teams, and our release processes should all be able to operate and evolve independently of each other, without excessive coordination.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EiNqeCEPWo9UNTWf4z-8mA.jpeg" /></figure><p>This approach becomes crucial when an application outgrows all reasonable limits — the build process is slow, every minor change results in minutes of compilation in watch mode, and the codebase contains thousands of modules and NPM dependencies. At this point, the support of this monster becomes too expensive.</p><p>The problem becomes even worse if the team decides to try a new, improved technology stack. You can attempt to gradually replace the old system with the new one. At <a href="https://ringcentral.com/">RingCentral</a> we’ve been through that: we attempted to refactor one monolith and replace it with another monolith. Both were rapidly growing and receiving addition of new features, so when we finished, the final monolith was enormous. At some point it became obvious that it’s time to divide it before it’s too late, so we introduced embeddable sub-apps, but each was developed by a different team and had its own interface to connect to the host, its own bus, and some of them were IFrames. This approach helped in the short term, but was suboptimal in terms of further scaling.</p><blockquote>How to plug an app written using one framework into another, incompatible app, that uses different framework? <strong><em>You need an abstract orchestrator!</em></strong></blockquote><p>Before we start, let’s first figure out the basic terms:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/602/1*nv4OmVE_Rl__N5_kLy1q5A.png" /><figcaption>Apps can’t talk directly to each other, but host wire them together</figcaption></figure><ul><li>A Host is an app that contains a bunch of smaller apps</li><li>A Child App, or just an App, is an <a href="https://github.com/ringcentral/web-apps#iframe-apps">IFrame App</a>, a <a href="https://github.com/ringcentral/web-apps#global-apps">Global App</a>, or a <a href="https://github.com/ringcentral/web-apps#web-component-apps">Web Component App</a> which we embed in the Host or another Child App (which can act like a Host, too)</li><li>An Orchestrator: Web Apps Framework is an agent who helps them talk together</li></ul><h3>The Web Apps Framework</h3><p>The Web Apps framework provides exactly this:</p><blockquote><strong>A framework agnostic way to inject any app into any app, with a unified communication interface along with other perks.</strong></blockquote><p>Links to repository: <a href="https://github.com/ringcentral/web-apps">https://github.com/ringcentral/web-apps</a> and demo: <a href="https://ringcentral-web-apps.vercel.app/">https://ringcentral-web-apps.vercel.app</a>.</p><p>Consider this screenshot, taken from the <a href="https://ringcentral-web-apps.herokuapp.com/">demo</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CnGcyKW39uUJJjrY2R2zwg.png" /></figure><p>Arrows are pointing at different apps, that are seamlessly presented together but can be deployed and developed separately. Two of them are IFrames, which is a useful approach in case you need to deal with legacy code, <em>that could be written entirely without JavaScript</em>, or an app that needs maximum isolation of CSS and code.</p><p>Of course, there are other frameworks that do similar things, but Web Apps is using a very elegant and minimalistic approach to loading and wrapping different kinds of apps, which provides a unified interface no matter which underlying technologies are being used: Angular, Vue, React, jQuery, no JS at all, you name it.</p><p>And Web Apps provides <a href="https://github.com/ringcentral/web-apps#react-host">first class support for React</a>, just point to the app’s URL and voila, it will be included in the DOM and all events and handlers will be wired automagically:</p><pre><em>import {</em><strong><em>useApplication</em></strong><em>, </em><strong><em>eventType</em></strong><em>, </em><strong><em>useListenerEffect</em></strong><em>, </em><strong><em>dispatchEvent</em></strong><em>} from &#39;@ringcentral/web-apps-host-react&#39;;<br><br>const Page = () =&gt; {<br>    const {</em><strong><em>Component, node</em></strong><em>} = </em><strong><em>useApplication</em></strong><em>({<br>        id: &#39;xxx&#39;,<br>        type: &#39;script&#39;,<br>        url: [ <br>           &#39;http://example.com/styles.css&#39;, // yep, with styles<br>           &#39;http://example.com/bundle.js&#39;,<br>           &#39;http://example.com/entry.js&#39;<br>        ]<br>    });</em></pre><pre><em>    </em><strong><em>useListenerEffect</em></strong><em>(</em><strong><em>node</em></strong><em>, eventType.</em><strong><em>message</em></strong><em>, (message) =&gt; {<br>        alert(`message from app: ${message}`);<br>    });</em></pre><pre><em>    return &lt;&gt;<br>        &lt;</em><strong><em>Component someProp=&quot;someValue&quot; </em></strong><em>/&gt;<br>        &lt;button onClick={e =&gt; {<br>            </em><strong><em>dispatchEvent</em></strong><em>(</em><strong><em>node</em></strong><em>, eventType.</em><strong><em>message</em></strong><em>, {foo: &#39;bar&#39;})<br>        }&gt;Send Message&lt;/button&gt;<br>    &lt;/&gt;;<br>};</em></pre><p>The concept of this framework is to:</p><ul><li>Load an App with certain type &amp; URL (one or many)</li><li>Render the App on the page in any place</li><li>Wire events between Host and the App, including navigation events</li></ul><p>Communication can be event-based or through props (or HTML attributes). <strong><em>Events with will be transmitted to any type of app, including IFrame, through the same interface, so the Host can swap them at any time and should not care what type of app it’s showing.</em></strong> The Host can even swap URL and type of an App dynamically, let’s say, based on location, and show different apps in the main content area (just like in the <a href="https://ringcentral-web-apps.vercel.app/">demo</a>).</p><figure><img alt="Transferring packages" src="https://cdn-images-1.medium.com/max/480/0*djmvUb3xosr3qCGt.gif" /></figure><p>For other frameworks, <a href="https://github.com/ringcentral/web-apps#hosts-without-react">Web Component helper</a> can be used:</p><pre>// somewhere in your app<br><em>import &#39;@ringcentral/web-apps-host-web-component&#39;;</em></pre><pre>// then just add this<br><em>&lt;web-app id=&#39;appId&#39; url=&#39;[&quot;http://example.com&quot;]&#39; type=&quot;iframe&quot; /&gt;</em></pre><h3>How to define an app?</h3><p>There are no special techniques to prepare apps for embedding; just a tiny wrapper, depending on the type of app:</p><ul><li><a href="https://github.com/ringcentral/web-apps#global-apps"><strong>Global Apps</strong></a><strong> </strong>— a script that injects something into a given DOM node</li><li><a href="https://github.com/ringcentral/web-apps#web-component-apps"><strong>Web Component Apps</strong></a>—loaded as script and wrapped in Custom Element, provides a good isolation, thanks to Shadow DOM and Shadow CSS</li><li><a href="https://github.com/ringcentral/web-apps#iframe-apps"><strong>IFrame Apps</strong></a> — for extra security and isolation, depending on the degree of trust between host and app</li></ul><h4><a href="https://github.com/ringcentral/web-apps#global-apps">Global Apps</a></h4><p>Let’s say you have a React app that you’d like to embed. With <a href="https://module-federation.github.io/">Module Federation</a>, the entry point will look like just a simple default export of a callback that will inject the app in a given node:</p><pre><em>import React from &quot;react&quot;;<br>import {render, unmountComponentAtNode} from &quot;react-dom&quot;;<br>import {registerAppCallback} from &quot;@ringcentral/web-apps-react&quot;;<br>import App from &quot;./App&quot;;<br><br>export default (node) =&gt; {<br>    ReactDOM.render(&lt;App foo={node.getAttribute(&#39;foo&#39;)} /&gt;, node);<br>    return () =&gt; ReactDOM.unmountComponentAtNode(node);<br>});</em></pre><figure><img alt="Whoaaa!" src="https://cdn-images-1.medium.com/max/498/0*397-h5Fov8mypZRm.gif" /></figure><p>We’ve worked with <a href="https://module-federation.github.io/">Module Federation</a> (kudos to <a href="https://twitter.com/ScriptedAlchemy">Zack Jackson</a>) to make sure Web Apps have seamless integration.</p><p>With a bit of additional code using MutationObserver, all attributes can be synchronized from host to app.</p><p>If Module Federation is not available on the host inject your app into a given DOM Node, with a JSONP-style callback, as follows:</p><pre><em>import React from &quot;react&quot;;<br>import {render, unmountComponentAtNode} from &quot;react-dom&quot;;<br>import {registerAppCallback} from &quot;@ringcentral/web-apps-react&quot;;<br>import App from &quot;./App&quot;;</em></pre><pre><em>registerAppCallback(&#39;%appId%&#39;, (node) =&gt; {<br>    ReactDOM.render(&lt;App foo={node.getAttribute(&#39;foo&#39;)} /&gt;, node);<br>    return () =&gt; ReactDOM.unmountComponentAtNode(node);<br>});</em></pre><h4><a href="https://github.com/ringcentral/web-apps#web-component-apps">Web Component Apps</a></h4><p>Another way is to wrap the app in a Web Component for better isolation of CSS styles using Shadow DOM and Shadow CSS, as follows:</p><pre><em>import React from &quot;react&quot;;<br>import {render, unmountComponentAtNode} from &quot;react-dom&quot;;<br>import {App} from &#39;./app&#39;;<br><br>const template = document.createElement(&#39;template&#39;);<br><br>template.innerHTML = `<br>    &lt;style&gt;/* shadow CSS */&lt;/style&gt;<br>    &lt;div class=&quot;container&quot;&gt;&lt;/div&gt;<br>`;<br><br>customElements.define(&#39;web-app-%appId%&#39;, class extends HTMLElement {<br>    constructor() {<br>        super();<br>        this.attachShadow({mode: &#39;open&#39;});<br>        this.shadowRoot.appendChild(<br>            document.importNode(template.content, true)<br>        );<br>        this.mount = this.shadowRoot.querySelector(&#39;.container&#39;);<br>    }<br><br>    connectedCallback() { render(<br>        &lt;App <br>            authtoken={this.getAttribute(&#39;authtoken&#39;)}<br>            node={this}<br>        /&gt;, <br>        this.mount<br>    ) }<br><br>    disconnectedCallback() { unmountComponentAtNode(this) }    <br>});</em></pre><p>With observable attributes you can sync them to React app as props.</p><h4><a href="https://github.com/ringcentral/web-apps#iframe-apps">IFrame Apps</a></h4><p>Using IFrames does not require any code at all, but if you want to communicate just add this:</p><pre>import {<br>    <strong>IFrameSync, dispatchEvent, eventType<br></strong>} from &#39;@ringcentral/web-apps-sync-iframe&#39;;<br><br>export const <em>sync </em>= new <strong>IFrameSync</strong>({<br>    <strong>history</strong>: &#39;html5&#39;,<br>    <strong>id</strong>: &#39;iframe&#39;,<em><br>    </em><strong>origin</strong>: window.location,<em><br></em>});</pre><pre>// now you can listen and dispatch:<br>const<strong> node</strong> = <strong><em>sync</em></strong>.<strong>getEventTarget</strong>();</pre><pre><strong>dispatchEvent</strong>(<strong>node</strong>, eventType.<strong>message</strong>, &#39;Hello from IFrame&#39;);</pre><pre><strong>node</strong>.addEventListener(eventType.<strong>message</strong>, message =&gt; alert(message));</pre><h3>What about sharing?</h3><p>Obviously, if each app begins to have its own build, it also carries all the common dependencies, like react or react-dom. This, at times, can be quite heavy. Luckily, there’s a fancy new technology that aims to solve this problem:</p><blockquote><a href="https://indepth.dev/webpack-5-module-federation-a-game-changer-in-javascript-architecture/">Webpack 5 Module Federation</a>: A game changer in JavaScript architecture</blockquote><p>The big win is that the Web Apps framework has out of the box support for <a href="https://webpack.js.org/concepts/module-federation">Module Federation</a> which allows sharing of modules between host and apps, and apps between other apps. This especially plays well with small footprint of the framework. Module Federation has many features including automatic healing, redundancy and proper versioning. You can just follow the example <a href="https://github.com/ringcentral/web-apps/blob/master/demo/host/webpack.config.js#L34">host</a> and <a href="https://github.com/ringcentral/web-apps/blob/master/demo/fed/webpack.config.js#L27">app</a>:</p><pre>// host&#39;s webpack.config.js<br>module.exports = {<br>    ..., // all the usual stuff<br>    plugins: [<br>        new ModuleFederationPlugin({<br>            name: &#39;web-app-host&#39;,<br>            library: {type: &#39;var&#39;, name: &#39;web-app-host&#39;},<br>            shared: [&#39;react&#39;, &#39;react-dom&#39;],<br>        }),<br>    ]<br>};</pre><pre>// app&#39;s webpack.config.js<br>module.exports = {<br>    ..., // all the usual stuff<br>    plugins: [<br>        new ModuleFederationPlugin({<br>            name: &#39;web_app_federated&#39;,<br>            library: {type: &#39;var&#39;, name: &#39;web_app_federated&#39;},<br>            filename: &#39;remoteEntry.js&#39;,<br>            exposes: {<br>                &#39;./index&#39;: &#39;./src/index&#39;,<br>            },<br>            shared: {<br>                &#39;react-dom&#39;: &#39;react-dom&#39;,<br>                moment: &#39;^2.24.0&#39;,<br>                react: &#39;react&#39;,<br>            },<br>        }),<br>    ]<br>};</pre><p>And this is it! Now your host and app can share dependencies in a clean and transparent way.</p><figure><img alt="Sharing is caring!" src="https://cdn-images-1.medium.com/max/480/0*AzYxH6zmIObJpHRG.gif" /></figure><h3>More about IFrames</h3><p>Of course, one can just display an IFrame with a legacy app, no big deal. But big problems may occur when sub-apps are contained within IFrames and wish to send and receive messages from the host and show popups.</p><p>Messaging can be solved via PostMessage, but Web Apps simplifies and standardizes the interface, making sure there’s no flood of broadcast post messages and that only proper targets will receive them.</p><p>Dealing with the popups problem is a bit trickier:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FRwGSzNydG0fFJzYvI48UQ.png" /><figcaption>Note the visual separation of app and host</figcaption></figure><p>… the gray backdrop behind the popup is contained within the IFrame, while the rest of the website has a white background, meh, ugly!</p><p>Can we tell the host that the IFrame’d app is showing a popup? What if the host wants the IFrame’d app to hide it?</p><p>This is exactly what the Web Apps Framework is doing: it makes sure the Host is aware when to show the backdrop and what color it should be. It can send an event to the IFrame’d App when it’s time to close the popup, so it looks seamless:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XpyyRUfpmir5L7XVJe4ang.png" /><figcaption>Backgrounds inside and outside of IFrame are the same</figcaption></figure><figure><img alt="Magic" src="https://cdn-images-1.medium.com/max/275/0*k3OkRloPx9379d2I.gif" /></figure><p>Framework also supports synchronization of IFrame height and it’s contents, so that long texts in frames aren’t cut.</p><h3>Quick Recap</h3><p>Let me summarize the key features of the Web Apps Framework:</p><ul><li>Module Federation support</li><li>Location synchronization between app and host</li><li>Ability to deep-link “app to app” or “app to host” or “host to app”</li><li>Consistent event-based interaction between apps and host</li><li>IFrame resizing based on the content of an IFrame</li><li>IFrame popup support</li><li>Maximum adherence to Web Standards</li><li>Written in TypeScript</li><li>React and Web Component host helpers</li><li>Unlimited nesting of apps within other apps, e.g. each app can become a host for more apps</li><li>Seamless navigation support</li></ul><p>Check out the repository with examples: <a href="https://github.com/ringcentral/web-apps/blob/master/demo/host/webpack.config.js#L34">https://github.com/ringcentral/web-apps</a> and demo: <a href="https://ringcentral-web-apps.vercel.app/">https://ringcentral-web-apps.vercel.app</a>.</p><h3>Kudos</h3><p><a href="https://twitter.com/ScriptedAlchemy">Zack Jackson</a>, <a href="https://www.linkedin.com/in/chrisvanrensburg/">Chris Van Rensburg</a> and <a href="https://www.linkedin.com/in/bulyonov">Anton Bulyonov</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=376e0b1ea6c6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ringcentral-developers/web-apps-micro-frontend-framework-with-support-of-webpack-module-federation-376e0b1ea6c6">Web Apps: Micro Frontend framework with support of Webpack Module Federation</a> was originally published in <a href="https://medium.com/ringcentral-developers">RingCentral Developers</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Speed up NPM/Yarn install in Gitlab]]></title>
            <link>https://medium.com/disdj/speed-up-npm-yarn-install-in-gitlab-1434437f9857?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/1434437f9857</guid>
            <category><![CDATA[code]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[pipeline]]></category>
            <category><![CDATA[cache]]></category>
            <category><![CDATA[gitlab]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Wed, 22 Apr 2020 08:33:55 GMT</pubDate>
            <atom:updated>2020-04-22T20:19:18.817Z</atom:updated>
            <content:encoded><![CDATA[<p>This article is the sum of my findings in scope of <a href="https://gitlab.com/gitlab-org/gitlab-runner/-/issues/1797">Gitlab Issue</a> that’s still unresolved at this moment (April 2020). In short, when the node_modules becomes large Gitlab is experiencing huge performance bottleneck due to great amount of files it needs to archive and upload during caching procedures. I have came up with few ways to improve caching performance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/990/1*NhZ8Ts1Xe0SlreW2Ljzo6g.png" /></figure><p>At RingCentral we use runners and local S3 cache server, we have 120K files after yarn install of one of our monorepos, with overall size about 400–600 megabytes. Monorepo is a git repository which has multiple packages at once, packages can have own dependencies and usually are linked together. This is the worst case scenario since amount of Node modules can become insanely big. But this article can help you to deal with regular repositories too if they have lots of dependencies.</p><p><strong>Advice zero would be to use </strong><a href="https://classic.yarnpkg.com/en/docs/workspaces/"><strong>Yarn Workspaces</strong></a><strong> to deal with monorepo instead of Lerna, as latter is 2–3 times slower.</strong> Install and caching phases timings roughly are:</p><ul><li>Cache download takes about 1 minute</li><li>Cache create 4 minutes (zipping of hundreds of thousands files)</li><li>Cache upload 1 minutes</li><li>Bare Yarn install 3 minutes</li><li>Yarn install on top of cache 1 minute</li></ul><p>This is our baseline, now let’s analyze how we can speed things up.</p><h3>Solution 1: Create cache in first job, run others with read-only cache</h3><pre><strong>before_script</strong>:<br>  - yarn install<br><br><strong>cache</strong>:<br>  <strong>key</strong>: $CI_PROJECT_ID<br>  <strong>policy</strong>: pull<br>  <strong>untracked</strong>: true<br><br><strong>install</strong>:<br>  <strong>stage</strong>: install<br>  <strong>script</strong>: echo &#39;Warming the cache&#39;<br>  <strong>cache</strong>:<br>    <strong>key</strong>: $CI_PROJECT_ID<br>    <strong>policy</strong>: pull-push<br>    <strong>paths</strong>:<br>      - .yarn<br>      - node_modules<br>      - &#39;packages/*/node_modules&#39;<br><br><strong>lint</strong>:<br>  <strong>stage</strong>: test<br>  <strong>script</strong>: yarn run lint:all</pre><p>Key measurements:</p><ul><li>Install, cache create &amp; upload: 3+4+1 = <strong>7 min</strong></li><li>Download, install, skip upload: 1+1 = <strong>2 min</strong></li></ul><p>Best case scenario without cache upload it actually speeds things up a lot. It’s OK to waste time in ONE job to create and upload cache, if you have many other jobs that only download.</p><p>Given cache create+upload takes <strong>5min</strong>, and Yarn install with cache saves us (3min pure install - 1min cached install) = <strong>2min</strong>, it means that <strong>at least two jobs must successfully download cache and utilize it in Yarn install to justify time spent on uploading</strong>. More jobs use the cache — the better.</p><h4>Solution 1, but with per-job cache for selected jobs</h4><p><strong>Down side is that cache can’t be configured “per-job”</strong> since you can have only one job that uploads cache<strong>. </strong>If you need to cache something else besides node_modules you&#39;re stuck. And unfortunately, jobs like ESLint spend more time on actual analysis than on installation etc., so caching those artifacts is even more important than node_modules.</p><p>Solution for this could be to disable Yarn workspaces for such jobs, only top level packages will be installed, <strong>which allowed to cut Yarn install time from 3.5min to ~1min</strong>, so no need to cache node_modules at all, just cache ESLint artifacts.</p><pre><strong>lint</strong>:<br>  <strong>stage</strong>: test<br>  <strong>before_script</strong>:<br>  - sed -i &#39;s/workspaces/workspaces-disabled/g&#39; package.json<br>  - yarn install --no-lockfile<br>  - sed -i &#39;s/workspaces-disabled/workspaces/g&#39; package.json<br>  <strong>script</strong>: yarn lint<br>  <strong>cache</strong>:  # here we use per-job cache<br>    <strong>key</strong>: $CI_COMMIT_REF_NAME-lint<br>    <strong>paths</strong>:<br>      - .eslint</pre><h3>Solution 2: Build a docker image and use it in other jobs</h3><p>Since each job is running in a container we can build an image for the container as first step. We will be using Kaniko because it is faster than regular Docker-in-Docker (DIND):</p><pre><strong>stages</strong>:<br>  - docker<br>  - build<br>  - test</pre><pre><strong>variables</strong>:<br>  IMAGE_TAG: <strong>$CI_REGISTRY_IMAGE</strong>:<strong>$CI_COMMIT_REF_NAME</strong><br><br><strong>docker</strong>:<br>  <strong>stage</strong>: docker<br>  <strong>image</strong>:<br>    <strong>name</strong>: gcr.io/kaniko-project/executor:debug<br>    <strong>entrypoint</strong>: [&quot;&quot;]<br>  <strong>script</strong>:<br>  - echo &quot;{\&quot;auths\&quot;:{\&quot;<strong>${CI_REGISTRY</strong>}\&quot;:{\&quot;username\&quot;:\&quot;<strong>${CI_REGISTRY_USER}</strong>\&quot;,\&quot;password\&quot;:\&quot;<strong>${CI_REGISTRY_PASSWORD}</strong>\&quot;}}}&quot; &gt; /kaniko/.docker/config.json<br>  - time /kaniko/executor --context=&quot;<strong>${CI_PROJECT_DIR}</strong>&quot; --dockerfile=&quot;<strong>${CI_PROJECT_DIR}</strong>/Dockerfile&quot; --destination=&quot;<strong>${</strong><strong>IMAGE_TAG}</strong>&quot; --cache=true</pre><pre><strong>build</strong>:<br>  <strong>image</strong>: &quot;<strong>${IMAGE_TAG}</strong>&quot;<br>  <strong>stage</strong>: build<br>  <strong>script</strong>: yarn build<br>  <strong>artifacts</strong>:<br>    <strong>paths</strong>:<br>      - &#39;packages/*/lib&#39;  # all artifacts that are used by tests<strong><br></strong>  <strong>variables</strong>:<br>    GIT_STRATEGY: none</pre><pre><strong>lint</strong>:<br>  <strong>image</strong>: &quot;<strong>${</strong><strong>IMAGE_TAG}</strong>&quot;<br>  <strong>before_script</strong>: cd /opt/workspace<br>  <strong>script</strong>: yarn lint<br>  <strong>variables</strong>:<br>    GIT_STRATEGY: none<br>  <strong>cache</strong>:  # here we use per-job cache<br>    <strong>key</strong>: $CI_COMMIT_REF_NAME-lint<br>    <strong>paths</strong>:<br>      - .eslint</pre><p>We’re using Gitlab’s own <a href="https://docs.gitlab.com/ee/user/packages/container_registry/">Container Registry</a> via <a href="https://docs.gitlab.com/ee/ci/variables/">environment variables</a>. But you can use Docker Hub or anything else too, just define your own ENV variables.</p><p>We can skip the git checkout (GIT_STRATEGY: none) since the code is already a part of image.</p><p>In order to get maximum optimization we will build a special image manually (as described in the article <a href="https://medium.com/@dSebastien/speeding-up-your-ci-cd-build-times-with-a-custom-docker-image-3bfaac4e0479">https://medium.com/@dSebastien/speeding-up-your-ci-cd-build-times-with-a-custom-docker-image-3bfaac4e0479</a>) and push it from local computer to registry once in a while. <strong>Dependencies of this base image may become outdated, so on CI we will use base image just as a foundation for real image which will be used in jobs. Pre-warmed cache of the base image will help to speed up build of the second one.</strong></p><p>Let’s create a shared.Dockerfile for the base image.</p><pre><strong>FROM </strong>ringcentral<strong>/</strong>web-tools:alpine<br><br><strong>WORKDIR /</strong>opt<strong>/</strong>workspace<br><br><strong>ADD </strong>yarn.lock package.json lerna.json .<strong>/</strong></pre><pre>#<strong> </strong><em>Note!<br># Add one by one, you can&#39;t use glob because of </em><a href="https://github.com/moby/moby/issues/15858"><em>this bug in Docker</em></a><em><br></em><strong>ADD </strong>packages/a<strong>/</strong>package.json packages/a<strong>/</strong>package.json<br><strong>ADD </strong>packages/b<strong>/</strong>package.json packages/b<strong>/</strong>package.json<br><strong>ADD </strong>packages/c<strong>/</strong>package.json packages/c<strong>/</strong>package.json</pre><pre><strong>RUN </strong>yarn install <strong>--</strong>link-duplicates</pre><p>You can build and push this image like so:</p><pre>$ <strong>docker</strong> <strong>login YOUR-GITLAB-OR-OTHER-REGISTRY</strong><br>$ <strong>docker</strong> <strong>build</strong> --tag <strong>SOMENAME</strong> --file <strong>shared.Dockerfile</strong> .<br>$ <strong>docker</strong> <strong>tag</strong> <strong>SOMENAME</strong> <strong>SOMENAME</strong>:latest<br>$ <strong>docker</strong> <strong>push</strong> <strong>SOMENAME</strong>:latest</pre><p>This will require a DOCKERFILE, in which we will do pretty much the same but it would be based on the previous image:</p><pre><strong>FROM</strong> <strong>SOMENAM</strong>E:latest</pre><pre><strong>WORKDIR</strong> /opt/workspace</pre><pre><strong>ADD</strong> yarn.lock package.json lerna.json ./</pre><pre>#<strong> </strong><em>Note!<br># Add one by one, you can&#39;t use glob because of </em><a href="https://github.com/moby/moby/issues/15858"><em>this bug in Docker</em></a><em><br></em><strong>ADD</strong> packages/a/package.json packages/a/package.json<br><strong>ADD</strong> packages/b/package.json packages/b/package.json<br><strong>ADD</strong> packages/c/package.json packages/c/package.json</pre><pre><strong>RUN</strong> yarn install --link-duplicates  # this cuts size of node_modules</pre><pre><strong>ADD</strong> . ./  # now we add all the files</pre><pre><strong>RUN</strong> rm -rf $(yarn cache dir)</pre><p>Make sure to have .dockerignore and ban all node_modules and build artifacts, otherwise your image will become too huge.</p><p>In our case timings were:</p><ul><li>Install step of docker build on CI: 3 min</li><li>Image upload: 1 minute</li><li>Image download: 1.5 minutes</li></ul><p>Which gives similar time as solution 1: <strong>Kaniko build job overall takes 5 min, regular about 6–7 min.</strong></p><p><strong>Overall looks like a viable approach for heavy jobs that do require full install</strong>. Image download + quick install (1.5 min+1 min) take less time than full Yarn install (3.5 min) or Gitlab cache download, so having image is beneficial.</p><p><em>For quick jobs like ESLint, which does not require full install of monorepo, just top-level, it is an overkill though, since these jobs won’t need all dependencies anyway, but that’s micromanagement…</em></p><h3>Solution 3: Mount global Yarn cache to runner &amp; don’t use Gitlab cache for node_modules</h3><p>Gitlab jobs are running in Docker containers so we can mount a directory used by Yarn to globally cache packages in container to a directory in the runner. This will make any container to always have pre-warmed global Yarn cache. Unfortunately, this saves time on network downloads, but Yarn still copy tons of files into place during install.</p><p>To do that we need to add following to config.toml:</p><pre># /etc/gitlab-runner/config.toml<br>[[runners]]<br>  ...<br>  [runners.docker]<br>    volumes = [&quot;/cache:/usr/local/share/.cache:rw&quot;</pre><p>After that disable caching of node_modules in all jobs.</p><p>This produced steady 2 minutes installation time, and now you can cache individual artifacts per-job. You can speed it up by using top-level installs for certain jobs too.</p><pre><strong>stages</strong>:<br>  - docker<br>  - build<br>  - test</pre><pre><strong>image</strong>: node:lts</pre><pre><strong>before_script</strong>:<br>  - yarn install</pre><pre><strong>build</strong>:<br>  <strong>stage</strong>: build<br>  <strong>script</strong>: yarn build<br>  <strong>artifacts</strong>:<br>    <strong>paths</strong>:<br>      - &#39;packages/*/lib&#39;  # all artifacts that are used by tests</pre><pre><strong>lint</strong>:<br>  <strong>stage</strong>: test<br>  <strong>script</strong>: yarn lint<br>  <strong>cache</strong>:  # here we use per-job cache<br>    <strong>key</strong>: $CI_COMMIT_REF_NAME-lint<br>    <strong>paths</strong>:<br>      - .eslint</pre><h3>Solution 4. Yarn 2 with PNP</h3><p>Yarn 2 with Plug’n’play remediates caching issue dramatically: <a href="https://dev.to/arcanis/introducing-yarn-2-4eh1">https://dev.to/arcanis/introducing-yarn-2-4eh1</a>.</p><p>You even can use regular cache because it’s still fast enough, Yarn’s cache contains zip files instead of thousands of small sources. And install itself is much faster because Plug’n’play eliminates the time spent on copying from cache to workspace.</p><p>This approach requires quite a lot of effort to resolve all the bugs of early software, but as a reward you can use the cache as intended:</p><pre><strong>stages</strong>:<br>  - docker<br>  - build<br>  - test</pre><pre><strong>image</strong>: node:lts</pre><pre><strong>before_script</strong>:<br>  - yarn install</pre><pre># here we can use one cache for all jobs<br><strong>cache</strong>:<br>  <strong>paths</strong>:<br>    - node_modules<br>    - &quot;packages/*/node_modules&quot;<br>    - .eslint</pre><pre><strong>build</strong>:<br>  <strong>stage</strong>: build<br>  <strong>script</strong>: yarn build<br>  <strong>artifacts</strong>:<br>    <strong>paths</strong>:<br>      - &#39;packages/*/lib&#39;  # all artifacts that are used by tests</pre><pre><strong>test</strong>:<br>  <strong>stage</strong>: test<br>  <strong>script</strong>: yarn test</pre><p>Speed is amazing. <strong>First time w/o cache it takes same 1–2 minutes, but cache creation and upload take ~20 seconds each.</strong> But magic happens on next installs, <strong>when cache is warm: install takes 10 seconds</strong> (!!!), cache download same ~20 seconds. <strong>If we disable cache upload overall freeload on subsequent jobs is just 30 seconds!!!</strong></p><h3>Summary</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/1*cNWoR83TT0lETeV3vpiNGQ.png" /></figure><p>As you see, <strong>worst performance is regular Gitlab Cache</strong>. Even quite fast execution with blocked cache uploads does not justify the performance failure on creation and upload.</p><p><strong>Second comes Kaniko.</strong> Quite fast overall, faster w/o cached image, but slower on subsequent jobs. Requires juggling with Docker images. Allows to have cache per job.</p><p><strong>Simple idea to mount global Yarn cache to runner resulted in surprisingly good and stable results</strong>, a bit worse than regular Gitlab cache but it depends how many subsequent jobs you have. If not many then it’s your best bet.</p><p><strong>But the absolute winner undoubtedly is Yarn 2 with PNP.</strong> It’s much faster on first run, and 30 seconds on subsequent jobs is the best thing ever. And you can make it even faster if you commit it’s local cache ;)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1434437f9857" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/speed-up-npm-yarn-install-in-gitlab-1434437f9857">Speed up NPM/Yarn install in Gitlab</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[React, JSX, ES module imports (dynamic too) in browser without Webpack]]></title>
            <link>https://medium.com/disdj/react-jsx-es-module-imports-dynamic-too-in-browser-without-webpack-9cf39520f20f?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/9cf39520f20f</guid>
            <category><![CDATA[code]]></category>
            <category><![CDATA[import]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[modules]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Fri, 17 Jan 2020 19:52:40 GMT</pubDate>
            <atom:updated>2020-01-17T19:52:40.942Z</atom:updated>
            <content:encoded><![CDATA[<p>This article is an attempt to put together everything available at the moment and see if it is really possible to implement production ready React-based website without pre-compilation like Webpack/Parcel or at least to have as less pre-compilation as possible.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*4aqPNIVJLrnEfQZ9YsSi2g.jpeg" /></figure><h3>TL;DR</h3><p>It is possible to serve React project with minimal compilation of JSX without using bundlers like Webpack or Parcel.</p><p>Bundlers pack code into chunks, which has drawbacks: chunks have to be small enough to deliver only what’s needed and big enough to make better use of HTTP1.1 connections. Finding the right balance may be tricky and involves some automatic and manual tuning. In addition it slows down the build and dev mode.</p><p>We will use Service Workers to do all the work in runtime and some Babel for production build.</p><p>Everything described in the article is highly experimental and I am cutting corners everywhere. By no means I recommend to use any of that in production :)</p><h3>Current state</h3><p>Ability to use ECMAScript modules (&lt;script type=&quot;module&quot;/&gt; with imports like import Foo from &#39;./foo&#39;; and import(&#39;./Foo&#39;)) directly in browser is quite well known at the moment and has good browser support: <a href="https://caniuse.com/#feat=es6-module">https://caniuse.com/#feat=es6-module</a>.</p><p>But in reality we don’t just import our own modules, we import libraries. There is a great article on this topic: <a href="https://salomvary.com/es6-modules-in-browsers.html">https://salomvary.com/es6-modules-in-browsers.html</a>. Also there’s another worth to mention project <a href="https://github.com/stken2050/esm-bundlerless">https://github.com/stken2050/esm-bundlerless</a>.</p><p>Among other important things from the articles, these ones will be crucial in order to make React app work:</p><ul><li>Package specifier imports support (or import maps): when we import react in reality we should import something like <a href="https://cdn.com/react/react.production.js">https://cdn.com/react/react.production.js</a></li><li>UMD support: React is still distributed as UMD and so far there’s still an ongoing discussion how to publish it using ES modules</li><li>JSX</li><li>Import CSS</li></ul><p>Let’s solve these issues one by one.</p><h3>Project structure</h3><p>First things first, let’s assume the project will have the following structure:</p><ul><li>node_modules obviously a place where we will install all dependencies</li><li>src dir with index*.html and service scripts</li><li>app app source code</li></ul><h3>Package specifier imports support</h3><p>In order to use React like so import React from &#39;react&#39;; we need to tell the browser where to find the actual source. This is quite simple, there&#39;s a shim for that: <a href="https://github.com/guybedford/es-module-shims">https://github.com/guybedford/es-module-shims</a>.</p><p>Let’s install the shim and React:</p><pre>$ npm i es-module-shims react react-dom --save</pre><p>In order to launch the app we can do something like this in public/index-dev.html:</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html&gt;<br>&lt;body&gt;</pre><pre>  &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;</pre><pre>  &lt;script defer src=&quot;../node_modules/es-module-shims/dist/es-module-shims.js&quot;&gt;&lt;/script&gt;</pre><pre>  &lt;script type=&quot;importmap-shim&quot;&gt;<br>    {<br>      &quot;imports&quot;: {<br>        &quot;react&quot;: &quot;../node_modules/react/umd/react.development.js&quot;,<br>        &quot;react-dom&quot;: &quot;../node_modules/react-dom/umd/react-dom.development.js&quot;<br>      }<br>    }<br>  &lt;/script&gt;</pre><pre>  &lt;script type=&quot;module-shim&quot;&gt;<br>    import &#39;./app/index.jsx&#39;;<br>  &lt;/script&gt;</pre><pre>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>Where in src/app/index.jsx we will have:</p><pre>import React from &#39;react&#39;;<br>import ReactDOM from &#39;react-dom&#39;;<br>import &#39;./index.css&#39;;</pre><pre>(async () =&gt; {<br>  const {Button} = await import(&#39;./Button.jsx&#39;);<br>  const root = document.getElementById(&#39;root&#39;);<br>  ReactDOM.render((<br>    &lt;div&gt;<br>      &lt;Button&gt;Direct&lt;/Button&gt;<br>    &lt;/div&gt;<br>  ), root);<br>})();</pre><p>And the src/app/Button.jsx:</p><pre>import React from &#39;react&#39;;<br>export const Button = ({children}) =&gt; &lt;button&gt;{children}&lt;/button&gt;;</pre><p>Does it work? Of course, no. Even though we’ve successfully imported everything.</p><p>Let’s move on to the next challenge.</p><h3>UMD support</h3><h4>Dynamic way</h4><p>The issue now is that React is distributed as UMD, it cannot be consumed by imports, <a href="https://github.com/guybedford/es-module-shims/issues/18">even by the shimmed ones</a> (if the ticket is resolved, just skip this step). So we need to somehow patch the distributable to convince browser that it’s a legit ES modules.</p><p>The above mentioned article led me to an idea that we can use Service Workers to intercept and pre-process network requests. Let’s create the main endpoint src/index.js, which will bootstrap the SW and App and use it instead of the App directly (src/app/index.jsx):</p><pre>(async () =&gt; {</pre><pre>  try {<br>    const registration = await navigator.serviceWorker.register(&#39;sw.js&#39;);<br>    await navigator.serviceWorker.ready;</pre><pre>    const launch = async () =&gt; import(&quot;./app/index.jsx&quot;);</pre><pre>    // this launches the React app if the SW has been installed before or immediately after registration<br>    // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim<br>    if (navigator.serviceWorker.controller) {<br>      await launch();<br>    } else {</pre><pre>navigator.serviceWorker.addEventListener(&#39;controllerchange&#39;, launch);<br>    }</pre><pre>  } catch (error) {<br>    console.error(&#39;Service worker registration failed&#39;, error);<br>  }<br>})();</pre><p>And then let’s create the Service Worker (src/sw.js):</p><pre>//this is needed to activate the worker immediately without reload<br>//@see https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim<br>self.addEventListener(&#39;activate&#39;, event =&gt; event.waitUntil(clients.claim()));</pre><pre>const globalMap = {<br>    &#39;react&#39;: &#39;React&#39;,<br>    &#39;react-dom&#39;: &#39;ReactDOM&#39;<br>};</pre><pre>const getGlobalByUrl = (url) =&gt; Object.keys(globalMap).reduce((res, key) =&gt; {<br>    if (res) return res;<br>    if (matchUrl(url, key)) return globalMap[key];<br>    return res;<br>}, null);</pre><pre>const matchUrl = (url, key) =&gt; url.includes(`/${key}/`);</pre><pre>self.addEventListener(&#39;fetch&#39;, (event) =&gt; {</pre><pre>  const {request: {url}} = event;</pre><pre>  console.log(&#39;Req&#39;, url);</pre><pre>  const fileName = url.split(&#39;/&#39;).pop();<br>  const ext = fileName.includes(&#39;.&#39;) ? url.split(&#39;.&#39;).pop() : &#39;&#39;;</pre><pre>  if (!ext &amp;&amp; !url.endsWith(&#39;/&#39;)) {<br>    url = url + &#39;.jsx&#39;;<br>  }</pre><pre>  if (globalMap &amp;&amp; Object.keys(globalMap).some(key =&gt; matchUrl(url, key))) {<br>    event.respondWith(<br>      fetch(url)<br>        .then(response =&gt; response.text())<br>        .then(body =&gt; new Response(`<br>          const head = document.getElementsByTagName(&#39;head&#39;)[0];<br>          const script = document.createElement(&#39;script&#39;);<br>          script.setAttribute(&#39;type&#39;, &#39;text/javascript&#39;);<br>          script.appendChild(document.createTextNode(<br>            ${JSON.stringify(body)}<br>          ));<br>          head.appendChild(script);<br>          export default window.${getGlobalByUrl(url)};<br>        `, {<br>          headers: new Headers({<br>            &#39;Content-Type&#39;: &#39;application/javascript&#39;<br>          })<br>        })<br>      )<br>    )<br>  } else if (url.endsWith(&#39;.js&#39;)) { // rewrite for import(&#39;./Panel&#39;) with no extension<br>    event.respondWith(<br>      fetch(url)<br>        .then(response =&gt; response.text())<br>        .then(body =&gt; new Response(<br>          body,<br>          {<br>            headers: new Headers({<br>              &#39;Content-Type&#39;: &#39;application/javascript&#39;<br>            })<br>        })<br>      )<br>    )<br>  }</pre><pre>});</pre><p>Here’s what we’ve done here:</p><ol><li>We have created the export map, which associates package id with global var name</li><li>We have created a script tag in head with contents of UMD-packaged script</li><li>We’ve exported the mapped global var as default export of module</li></ol><p>For the sake of tech demo this method of patching should be enough, but it may break with other UMD declaration. <a href="https://github.com/jonbretman/amd-to-as6/blob/master/index.js">Something more robust</a> can be used to process sources.</p><p>Now let’s adjust the src/index-dev.html to use the bootstrap entry point:</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html&gt;<br>&lt;body&gt;</pre><pre>  &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;</pre><pre>  &lt;script defer src=&quot;../node_modules/es-module-shims/dist/es-module-shims.js&quot;&gt;&lt;/script&gt;</pre><pre>  &lt;script type=&quot;importmap-shim&quot;&gt;... same as before&lt;/script&gt;</pre><pre>  &lt;!-- change the file from app/index.jsx to index.js --&gt;<br>  &lt;script type=&quot;module-shim&quot; src=&quot;index.js&quot;&gt;&lt;/script&gt;</pre><pre>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>Now we’re able to import React and React DOM.</p><h4>Static way</h4><p>It’s worth to mention, that there’s also another way. We can install ESM distributable:</p><pre>npm install esm-react --save</pre><p>And then use following map:</p><pre>{<br>  &quot;imports&quot;: {<br>    &quot;react&quot;: &quot;../node_modules/esm-react/src/react.js&quot;,<br>    &quot;react-dom&quot;: &quot;../node_modules/esm-react/src/react-dom.js&quot;<br>  }<br>}</pre><p>But unfortunately this project is quite stale, latest is 16.8.3 whereas React is 16.10.2.</p><h3>JSX</h3><p>There are two ways to do the JSX compilation. We can either go traditional way and use Babel to pre-compile or we can use it in runtime. Of course for production it would make much more sense to pre-compile, development mode can be more brutal. Since we already use Service Worker let’s enhance it.</p><p>Let’s install a special Babel package that can do it:</p><pre>$ npm install @babel/standalone --save-dev</pre><p>Now let’s add following to the Service Worker (src/sw.js):</p><pre># src/sw.js<br>// at the very top of the file<br>importScripts(&#39;../node_modules/@babel/standalone/babel.js&#39;);</pre><pre>// activation stuff as before</pre><pre>self.addEventListener(&#39;fetch&#39;, (event) =&gt; {</pre><pre>  // whatever we had before</pre><pre>  } else if (url.endsWith(&#39;.jsx&#39;)) {<br>    event.respondWith(<br>      fetch(url)<br>        .then(response =&gt; response.text())<br>        .then(body =&gt; new Response(<br>          //TODO Cache<br>          Babel.transform(body, {<br>            presets: [<br>              &#39;react&#39;,<br>            ],<br>            plugins: [<br>              &#39;syntax-dynamic-import&#39;<br>            ],<br>              sourceMaps: true<br>            }).code,<br>            { <br>              headers: new Headers({<br>                &#39;Content-Type&#39;: &#39;application/javascript&#39;<br>              })<br>            })<br>        )<br>    )<br>  }</pre><pre>});</pre><p>Here we’ve used same approach to intercept the network request and respond with slightly different content, in this case we use Babel to transform the original response. Please note that plugin for dynamic import has a different name syntax-dynamic-import, not a usual @babel/plugin-syntax-dynamic-import due to Standalone usage.</p><h3>CSS</h3><p>In the above mentioned article author used text transformation, here we will go a bit further and inject the CSS in the page. For that we again will use the Service Worker (src/sw.js):</p><pre>// same as before</pre><pre>self.addEventListener(&#39;fetch&#39;, (event) =&gt; {</pre><pre>  // whatever we had before + Babel stuff</pre><pre>  } else if (url.endsWith(&#39;.css&#39;)) {<br>    event.respondWith(<br>      fetch(url)<br>        .then(response =&gt; response.text())<br>        .then(body =&gt; new Response(<br>          //TODO We don&#39;t track instances<br>          //so 2x import will result in 2x &lt;style&gt; tags<br>          `<br>            const head = document.getElementsByTagName(&#39;head&#39;)[0];<br>            const style = document.createElement(&#39;style&#39;);<br>            style.setAttribute(&#39;type&#39;, &#39;text/css&#39;);<br>            style.appendChild(document.createTextNode(<br>              ${JSON.stringify(body)}<br>            ));<br>            head.appendChild(style);<br>            export default null;<br>          `,<br>          {<br>            headers: new Headers({<br>              &#39;Content-Type&#39;: &#39;application/javascript&#39;<br>            })<br>          })<br>        )<br>    );<br>  }</pre><pre>});</pre><p>Et voila! If you now open the src/index-dev.html in the browser you&#39;ll see the buttons. Make sure the proper Service Worker is being picked up, if you&#39;re not sure, open Dev Tools, go to Application tab and Service Workers section, Unregister everything and reload the page.</p><h3>More production friendly case</h3><p>The above mentioned code works fine for dev mode, but realistically we don’t want all of the app users to compile the code in their browsers, it’s impractical. So let’s push things a bit further and make the minimalistic production mode.</p><p>In order to do that we will create one more index file src/index.html with following content:</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html&gt;<br>&lt;body&gt;</pre><pre>&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;</pre><pre>&lt;script type=&quot;module&quot; src=&quot;index.js&quot;&gt;&lt;/script&gt;</pre><pre>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>As you see, no shims here, we will use different technique to do rewrites. Since we still have to use Babel to compile JSX we can also rewrite module paths there instead of using importMap.json for the shim. Let&#39;s install everything:</p><pre>$ npm install @babel/cli @babel/core @babel/preset-react @babel/plugin-syntax-dynamic-import babel-plugin-module-resolver --save-dev</pre><p>Let’s add the scripts section to package.json:</p><pre>{<br>  &quot;scripts&quot;: {<br>    &quot;start&quot;: &quot;npm run build -- --watch&quot;,<br>    &quot;build&quot;: &quot;babel src/app --out-dir build/app --source-maps --copy-files&quot;<br>  }<br>}</pre><p>Next let’s add .babelrc.js:</p><pre>module.exports = {<br>  presets: [<br>    &#39;@babel/preset-react&#39;<br>  ],<br>  plugins: [<br>    &#39;@babel/plugin-syntax-dynamic-import&#39;,<br>    [<br>      &#39;babel-plugin-module-resolver&#39;,<br>      {<br>        alias: {<br>          &#39;react&#39;: &#39;./node_modules/react/umd/react.development.js&#39;,<br>          &#39;react-dom&#39;: &#39;./node_modules/react-dom/umd/react-dom.development.js&#39;<br>        },<br>        // we replace as follows to make sure we stay in build dir<br>        resolvePath: (sourcePath, currentFile, opts) =&gt; resolvePath(sourcePath, currentFile, opts).replace(&#39;../../&#39;, &#39;../&#39;)<br>      }<br>    ]<br>  ]<br>}</pre><p>Keep in mind that this file will be used only for production, for dev we configure Babel in the Service Worker.</p><p>Also let’s add production mode support to Service Worker:</p><pre>// src/index.js<br>if (&#39;serviceWorker&#39; in navigator) {<br>    (async () =&gt; {</pre><pre>        try {</pre><pre>            // adding this<br>            const production = !window.location.toString().includes(&#39;index-dev.html&#39;);</pre><pre>            const config = {<br>                globalMap: {<br>                    &#39;react&#39;: &#39;React&#39;,<br>                    &#39;react-dom&#39;: &#39;ReactDOM&#39;<br>                },<br>                production<br>            };</pre><pre>            const registration = await navigator.serviceWorker.register(&#39;sw.js?&#39; + JSON.stringify(config));</pre><pre>            await navigator.serviceWorker.ready;</pre><pre>            const launch = async () =&gt; {<br>                if (production) {<br>                    await import(&quot;./app/index.js&quot;);<br>                } else {<br>                    await import(&quot;./app/index.jsx&quot;);<br>                }<br>            };</pre><pre>            // this launches the React app if the SW has been installed before or immediately after registration<br>            // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim<br>            if (navigator.serviceWorker.controller) {<br>                await launch();<br>            } else {</pre><pre>navigator.serviceWorker.addEventListener(&#39;controllerchange&#39;, launch);<br>            }</pre><pre>        } catch (error) {<br>            console.error(&#39;Service worker registration failed&#39;, error);<br>        }</pre><pre>    })();<br>} else {<br>    alert(&#39;Service Worker is not supported&#39;);<br>}</pre><p>And use the condition in src/sw.js:</p><pre>// src/sw.js<br>const {globalMap, production} = JSON.parse((decodeURIComponent(self.location.search) || &#39;?{}&#39;).substr(1));</pre><pre>if (!production) importScripts(&#39;../node_modules/@babel/standalone/babel.js&#39;);</pre><p>Also replace</p><pre>// src/sw.js<br>if (!ext &amp;&amp; !url.endsWith(&#39;/&#39;)) {<br>  url = url + &#39;.jsx&#39; with<br>}</pre><p>with</p><pre>// src/sw.js<br>if (!ext &amp;&amp; !url.endsWith(&#39;/&#39;)) {<br>  url = url + &#39;.&#39; + (production ? &#39;js&#39; : &#39;jsx&#39;);<br>}</pre><p>Now let’s add a build script build.sh which will copy everything needed to build dir:</p><pre># cleanup<br>rm -rf build</pre><pre># create directories<br>mkdir -p build/scripts<br>mkdir -p build/node_modules</pre><pre># copy used node modules<br>cp -r ./node_modules/react       ./build/node_modules/react<br>cp -r ./node_modules/react-dom   ./build/node_modules/react-dom</pre><pre># copy files that are not built<br>cp ./src/*.js        ./build<br>cp ./src/index.html  ./build/index.html</pre><pre># build<br>npm run build</pre><p>We do this to make node_modules leaner by skipping build dependencies.</p><p>Here’s the final state: <a href="https://github.com/kirill-konshin/pure-react-with-dynamic-imports">kirill-konshin/pure-react-with-dynamic-imports</a>.</p><p>Now if you open build/index.html you will see the same output as for src/index-dev.html but this time browser won&#39;t build Babel, it will use pre-built files.</p><p>As you see, the solution now has duplicates, like importMap.json, the alias section in .babelrc.js and list of files to be copied in build.sh. For demo purposes it&#39;s fine but for real usage it would be better to automate this.</p><p>Here’s the published build: <a href="https://kirill-konshin.github.io/pure-react-with-dynamic-imports/index.html">https://kirill-konshin.github.io/pure-react-with-dynamic-imports/index.html</a></p><h3>Conclusion</h3><p>HTTP2 should take care of those small source files sent over the network, hopefully.</p><p>Here’s the repo where I have put everything together: <a href="https://github.com/kirill-konshin/pure-react-with-dynamic-imports">https://github.com/kirill-konshin/pure-react-with-dynamic-imports</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9cf39520f20f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/react-jsx-es-module-imports-dynamic-too-in-browser-without-webpack-9cf39520f20f">React, JSX, ES module imports (dynamic too) in browser without Webpack</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Monorepo scripts strategies & naming conventions]]></title>
            <link>https://medium.com/disdj/monorepo-scripts-strategies-naming-conventions-691c64b51acb?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/691c64b51acb</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[lerna]]></category>
            <category><![CDATA[npm]]></category>
            <category><![CDATA[code]]></category>
            <category><![CDATA[monorepo]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Tue, 29 Jan 2019 17:38:55 GMT</pubDate>
            <atom:updated>2019-08-30T22:18:44.380Z</atom:updated>
            <content:encoded><![CDATA[<p>Mono repository is a popular approach where some libraries or other fairly independent projects are colocated in one repository. One of the benefits you get is simpler control over changes that should be synchronized across all involved packages. As a downside, you get less independence of each package.</p><p>The vast majority of everyday monorepo management tasks boil down to running package scripts in a certain order and under certain conditions, both locally and on CI. This article explains one of many possible approaches. This approach worked fairly well in some of RingCentral’s monorepo projects. The size of projects varies: from less than a hundred files and a handful of devs up to thousands of files and 50+ developers.</p><p>In this article, I will use Lerna as a tool that helps to manage such mono repositories. It allows you to run scripts and execute code in packages, as well as takes care of some publishing activities. You may read more about Lerna concepts here: <a href="https://github.com/lerna/lerna#concepts">https://github.com/lerna/lerna#concepts</a>.</p><p><em>A short remark about package versions: they could be completely independent, or they could be synchronized to a certain degree. For example, if a package did have changes, its version will be updated, whereas non-updated packages will not get a version bump. But for simplicity, we will assume fully synchronized version bumps of all published packages (even unchanged), so we will be using exact versions in </em><em>lerna.json and </em><em>--force-publish=* flag in </em><em>publish commands.</em></p><h3>Main ideas</h3><ul><li>Scripts should eliminate the possibility of error in critical tasks like test coverage collection and publishing</li><li>Default scripts (like install, test and publish) should be no-brainers: do all the things automatically with the <a href="https://en.wikipedia.org/wiki/Principle_of_least_astonishment">least astonishment</a></li><li>Full control mode when needed: ability to run scripts granularly or in a quicker fashion</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ghfdYSUCsNdWSHpLEHu1oQ.jpeg" /></figure><h3>Phases</h3><p>Overall, any interaction with a repository, both locally and on the CI, can be presented as scenarios that can be split into some granular phases (or stages in Gitlab terminology). Scenarios may skip phases or have some phases substituted with other different phases. Scenarios usually have certain goals and expectations — here are the most common ones:</p><ul><li>A developer checks out the repo and runs npm install — the goal is to have a fully functional and ready-to-use dev setup at this point, the expectation is that right after this command the developer may run other commands with no additional actions related to getting things ready to work</li><li>A developer runs the npm start script — the goal is to run start in all packages, the expectation is that it just starts working, with no need to pre-build anything</li><li>CI determines that a tag has been pushed, checks out the repo, runs linter, runs build scripts, runs tests, and collects coverage, and if all is good — publishes to NPM. The goal is the delivery of high-quality code. The expectation is that if any phase breaks it stops the process and there is zero possibility of any unwanted publishes of incomplete/broken code</li></ul><p>Let’s dive deeper into those phases.</p><h4>Install</h4><p>In this phase, CI or the developer sets up the environment to run other scripts.</p><ul><li>CI should run npm install --ignore-scripts so that we can control when and how to do bootstrapping in the CI environment, for example, the lint task does not require packages to be bootstrapped, but the test task does (more about this in the next section)</li><li>Developers should use regular npm install, which must run full bootstrap</li></ul><h4>Lint</h4><p>This is the cheapest check that makes sure the codebase is in good condition.</p><ul><li>CI should run lint right after install as the first cheap check</li><li>CI must stop all further expensive checks if the cheap lint check failed</li></ul><h4>Packages bootstrapping</h4><p>In this phase, dependencies of underlying packages are installed — this is a post install on the dev machine or a post lint phase in the CI environment, but it depends on the setup.</p><ul><li>In general the postinstall script should run lerna bootstrap --hoist --no-ci so that devs will get a fully working repo right after running npm install, the --no-ci flag is needed due to a bug <a href="https://github.com/lerna/lerna/issues/1324">https://github.com/lerna/lerna/issues/1324</a></li><li>CI may not require demos to be built/tested and thus neither to be bootstrapped unless you use demos for e2e tests. In this case, we should bootstrap only the truly used packages by scoping the bootstrap script: npm run bootstrap -- --scope=@ringcentral/* (we usually have libraries in this scope). This is a nice optimization that saves CI time.</li></ul><h4>Building</h4><p>A phase needed to obtain an artifact, something that later will be published. Also, it may be a prerequisite for tests.</p><ul><li>The build script on package level should run the clean script beforehand to make sure no previous artifacts exist on subsequent builds</li><li>The build:quick can be used to build all libs that will be used in demos</li></ul><h4>Tests</h4><p>A phase that does the majority of code quality verification and makes sure things work as expected, not just written as expected (linter phase).</p><p>Sometimes code has to be built before running tests. If there are multiple libs in monorepo which depend on each other it is safer to run tests once all dependencies are built instead of using magic to run tests using only sources.</p><ul><li>CI runs tests by calling the test script, which is a maximum set of all tests<strong> with e2e tests and coverage collection</strong></li><li>Locally devs may run test:quick<strong> </strong>which does not collect coverage or does not run e2e tests</li><li>CI should also run test:coverage script to upload coverages after test</li><li>Optionally there could also be a test:watch script which will run quick tests in watch mode</li></ul><h4>Committing</h4><p>This is a phase that occurs on dev machines when developers commit and push the code, some minor quick checks should happen on this phase.</p><ul><li>Obviously, we don’t want to make CI run potentially invalid code so we can use test:quick and lint:staged (or lint:quick)scripts in pre-commit hooks</li></ul><h4>Publishing</h4><p>This delivers artifacts to some package management system that distributes them to end users. Code must be built and tested before publishing, which is CI responsibility.</p><ul><li>Publish scripts actually do the publishing (<strong>usually on CI</strong>)</li><li>Canary means a pre-release (something you can run nightly)</li><li>Release means a regular versioned release</li><li>CI may add --yes flag to publish:* scripts: it’s better to add them here in order to prevent unwanted local publishes</li></ul><p>Alternatively, on publish, CI can skip regular flow (build, test, coverage) and only run publish in packages, which should do build, test, coverage to make sure that if publish is run locally from dev machine it still goes through all phases, but we recommend to <strong>always publish from CI only as it is the only way to ensure all proper checks.</strong></p><h4>Starting/watching</h4><p>Developer-specific phase, day to day activities like website or library/demo/storybook development, on this phase codebase is constantly watched for changes and re-built once changes occur.</p><p>Assume the following setup:</p><pre>packages/<br>- demo (depends on lib1 and lib2)<br>- lib1<br>- lib2 (depends on lib1)</pre><ul><li>Demos that use libraries often need those libs to be pre-built in order to run start correctly without errors about missing files so root’s start script must run build script scoped for libs and then run start:quick script (see examples below), unfortunately, this brings overhead until <a href="https://github.com/webpack/webpack/issues/4991">https://github.com/webpack/webpack/issues/4991</a> is fixed (previously TS was prone too: <a href="https://github.com/Microsoft/TypeScript/issues/12996">bug 12996</a>, see update below)</li><li>start:quick simply runs start in all packages which starts Webpack and Babel watchers</li><li><strong>UPDATE</strong> In TypeScript 3.4 new <a href="https://github.com/Microsoft/TypeScript/wiki/What&#39;s-new-in-TypeScript#faster-subsequent-builds-with-the---incremental-flag">incremental option</a> has been introduced: it will produce a build and a cache, so no matter how often you restart watchers it will be ready much faster. Unfortunately it’s not yet supported by <a href="https://github.com/TypeStrong/ts-loader/issues/913">TS Loader for Webpack</a> It still does not eliminate the necessity to pre-build libraries.</li></ul><h4>Cleaning</h4><p>Developer-specific phase before builds or when switching branches.</p><ul><li>Root’s clean should first run clean scripts in packages (e.g. clean <strong>artifacts</strong>), then remove node_modules in <strong>packages</strong>, then remove node_modules in the <strong>root</strong>, this will bring repository to ground zero</li><li>Cleaning is useful when devs switch branches, then command npm run clean &amp;&amp; npm install makes sure they have working setup after branch switch</li></ul><h3>Examples</h3><h4>Main package scripts</h4><pre>{<br>  &quot;<strong>postinstall</strong>&quot;: &quot;npm run bootstrap&quot;,<br>  &quot;<strong>bootstrap</strong>&quot;: &quot;lerna bootstrap --no-ci --hoist&quot;,<br>  &quot;<strong>bootstrap:quick</strong>&quot;: &quot;npm run bootstrap -- --scope=<a href="http://twitter.com/ringcentral">@ringcentral</a>/*&quot;,<br>  <br>  &quot;<strong>clean</strong>&quot;: &quot;npm run clean:artifacts &amp;&amp; npm run clean:packages &amp;&amp; npm run clean:root&quot;,<br>  &quot;<strong>clean:artifacts</strong>&quot;: &quot;lerna run clean --parallel&quot;,<br>  &quot;<strong>clean:packages</strong>&quot;: &quot;lerna clean --yes&quot;,<br>  &quot;<strong>clean:root</strong>&quot;: &quot;rimraf node_modules&quot;,</pre><pre>  &quot;<strong>start</strong>&quot;: &quot;npm run build:quick &amp;&amp; npm run start:quick&quot;,<br>  &quot;<strong>start:quick</strong>&quot;: &quot;dotenv lerna run start -- --parallel&quot;,</pre><pre>  &quot;<strong>build</strong>&quot;: &quot;lerna run build --concurrency=1 --stream&quot;,<br>  &quot;<strong>build:quick</strong>&quot;: &quot;npm run build -- --scope=@ringcentral/*&quot;,</pre><pre>  &quot;<strong>test</strong>&quot;: &quot;lerna run test --concurrency=1 --stream&quot;,<br>  &quot;<strong>test:quick</strong>&quot;: &quot;lerna run test:quick --concurrency=1 --stream&quot;,<br>  &quot;<strong>test:coverage</strong>&quot;: &quot;lerna run test:coverage --parallel&quot;,<br>  &quot;<strong>test:watch</strong>&quot;: &quot;lerna run test:watch --parallel&quot;,</pre><pre>  &quot;<strong>publish:</strong><strong>release</strong>&quot;: &quot;lerna publish --force-publish=* --no-push --no-git-tag-version&quot;,</pre><pre>  &quot;<strong>lint</strong>&quot;: &quot;eslint --cache --cache-location node_modules/.cache/eslint --fix&quot;,<br>  &quot;<strong>lint:all</strong>&quot;: &quot;npm run lint &#39;packages/*/src/**/*.ts*&#39;&quot;,<br>  &quot;<strong>lint:staged</strong>&quot;: &quot;lint-staged&quot;<br>}</pre><p>We prefer to run tests one by one in topological order (maintained by Lerna) to see the nice structured output. Technically --concurrency=1 --stream can be replaced with --parallel which disregards topology and runs everything in parallel.</p><p>Notes:</p><ul><li>You can publish all packages together using --force-publish=* flag for simplicity.</li></ul><p>Scripts test:quick and lint:staged should be part of pre-commit hook.</p><h4>Package scripts</h4><pre>  &quot;<strong>mocha</strong>&quot;: &quot;mocha --opts mocha.opts&quot;,<br>  &quot;<strong>karma</strong>&quot;: &quot;karma start --singleRun&quot;,<br>  &quot;<strong>nyc</strong>&quot;: &quot;nyc mocha --opts mocha.opts&quot;,<br>  <br>  &quot;<strong>build</strong>&quot;: &quot;npm run clean &amp;&amp; npm run build:tsc &amp;&amp; npm run build:wp&quot;,<br>  &quot;<strong>build:tsc</strong>&quot;: &quot;tsc&quot;,<br>  &quot;<strong>build:wp</strong>&quot;: &quot;webpack --progress&quot;,<br>  <br>  &quot;<strong>start</strong>&quot;: &quot;npm-run-all -p watch:tsc watch:webpack&quot;,<br>  &quot;<strong>start:tsc</strong>&quot;: &quot;npm run build:tsc -- --watch --preserveWatchOutput&quot;,<br>  &quot;<strong>start:webpack</strong>&quot;: &quot;npm run build:webpack -- --watch&quot;<br>}</pre><h4>GitLab CI config</h4><pre><strong>image</strong>: node:lts</pre><pre><strong>stages</strong>:<br>  - validation<br>  - bootstrap<br>  - test<br>  - publish</pre><pre><strong>variables</strong>:<br>  <strong>BRANCH</strong>: $(echo ${<strong>CI_COMMIT_REF_NAME</strong>} | sed -e &#39;s/\(.*\)/\L\1/&#39; | sed -r &#39;s/[^a-z0-9-]/-/g&#39;)</pre><pre><strong>before_script</strong>:<br>  - npm config set //registry.npmjs.org/:_authToken=<strong>${NPM_TOKEN}<br></strong>  #or<br>  - npm config set repository <strong>$PRIVATE_NPM</strong><br>  - npm config set _auth <strong>$PRIVATE_NPM_AUTH</strong><br>  - npm config set email <strong>$PRIVATE_NPM_EMAIL</strong><br>  - npm <strong>install</strong> --progress=false --ignore-scripts</pre><pre><strong>cache</strong>:<br>  <strong>key</strong>: <strong>&quot;$CI_COMMIT_REF_NAME&quot;<br>  paths</strong>:<br>    - node_modules/<br>    - packages/*/node_modules</pre><pre><strong>job_lint</strong>:<br>  <strong>stage</strong>: validation<br>  <strong>script</strong>: DEBUG=eslint:cli-engine npm run <strong>lint:all</strong></pre><pre><strong>job_bootstrap:</strong><br>  <strong>stage:</strong> bootstrap<br>  <strong>script: <br>    </strong>- npm run <strong>bootstrap:quick</strong><br>    - npm run <strong>build:quick<br>  artifacts</strong>:<br>    <strong>paths</strong>:<br>      - <strong>&quot;*/es&quot;<br>      </strong>- <strong>&quot;*/lib&quot;<br>    expire_in</strong>: 1 day</pre><pre><strong>job_test:</strong><br>  <strong>stage:</strong> test<br>  <strong>script:<br>    </strong>-<strong> </strong>npm <strong>test</strong><br>    - npm run <strong>test:coverage</strong></pre><pre><strong>job_canary:</strong><br>  <strong>stage:</strong> publish<br>  <strong>script: </strong>npm run <strong>publish:release </strong>--<strong> --canary --preid=$BRANCH.$CI_PIPELINE_IID --dist-tag=$BRANCH --yes</strong><br>  <strong>only: <br>    </strong>-<strong> master</strong><br>    - <strong>feature/*</strong><br>    - <strong>release/*</strong></pre><pre><strong>job_publish</strong>:<br>  <strong>stage</strong>: publish<br>  <strong>script</strong>: npm run <strong>publish:release</strong> -- <strong>$CI_COMMIT_TAG</strong> <strong>--yes</strong><br>  <strong>only</strong>: <br>    - <strong>tags</strong></pre><p>This setup assumes you have NPM_TOKEN (and others) in GitLab ENV variables. Also note the --yes flags for publish scripts.</p><p>Due to <a href="https://github.com/lerna/lerna/issues/2171">https://github.com/lerna/lerna/issues/2171</a> we have to explicitly set the pipeline ID in the canary preid.</p><h4>Travis CI config</h4><pre><strong>language</strong>: node_js</pre><pre><strong>node_js</strong>:<br>- stable</pre><pre><strong>cache</strong>:<br>  <strong>directories</strong>:<br>  - $HOME/.npm<br>  - node_modules<br>  - packages/*/node_modules</pre><pre><strong>before_install</strong>:<br><strong>- BRANCH</strong>=$(echo ${<strong>TRAVIS_BRANCH</strong>} | sed -e &#39;s/\(.*\)/\L\1/&#39; | sed -r &#39;s/[^a-z0-9-]/-/g&#39;)<br>- npm config set //registry.npmjs.org/:_authToken=<strong>${NPM_TOKEN}</strong></pre><pre><strong>before_script</strong>:<br>- DEBUG=eslint:cli-engine npm run <strong>lint:all</strong><br>- npm run <strong>build:quick</strong></pre><pre><strong>deploy</strong>:<br>- <strong>provider</strong>: script<br>  <strong>script</strong>: npm run <strong>publish:release</strong> -- <strong>$TRAVIS_TAG</strong> <strong>--yes</strong><br>  <strong>skip_cleanup</strong>: true<br>  <strong>on</strong>:<br>    <strong>branch</strong>: master<br>    <strong>tags</strong>: false<br>    <strong>repo</strong>: <strong>xxx</strong><br>- <strong>provider</strong>: script<br>  <strong>script</strong>: npm run <strong>publish:release </strong>--<strong> --canary --preid=$</strong>BRANCH.$TRAVIS_JOB_NUMBER<strong> --dist-tag=$</strong>BRANCH<strong> --yes</strong><br>  <strong>skip_cleanup</strong>: true<br>  <strong>on</strong>:<br>    <strong>tags</strong>: true<br>    <strong>repo</strong>: xxx<br>- <strong>provider</strong>: releases<br>  <strong>api_key</strong>: <strong>$GITHUB_TOKEN</strong><br>  <strong>skip_cleanup</strong>: true<br>  <strong>file</strong>:<br>  - packages/xxx/dist/xxx.js<br>  - packages/yyy/dist/yyy.js<br>  <strong>on</strong>:<br>    <strong>tags</strong>: true<br>    <strong>repo</strong>: <strong>xxx</strong></pre><pre><strong>after_success</strong>:<br>- npm run <strong>test:coverage</strong></pre><p>This setup also assumes you have NPM_TOKEN and GITHUB_TOKEN in Travis ENV variables. Also, note the --yes flags for publish scripts. Travis by default will do npm install which will do npm bootstrap, so there’s some room for optimization like we did in GitLab example.</p><p>Due to <a href="https://github.com/lerna/lerna/issues/2171">https://github.com/lerna/lerna/issues/2171</a> we have to explicitly set the Travis build number in the canary preid.</p><h3>Packages used</h3><p>Since we mention a lot of packages it does make sense to tell what they are doing.</p><ul><li><a href="https://github.com/lerna/lerna">Lerna</a> — manages JavaScript projects with multiple packages</li><li><a href="https://github.com/entropitor/dotenv-cli">Dotenv-cli</a> — loads .env files that stores environment variables</li><li><a href="https://github.com/eslint/eslint">ESLint</a> — a fully pluggable tool for identifying and reporting on patterns in JavaScript</li><li><a href="https://github.com/mochajs/mocha">Mocha</a> — simple &amp;flexible javascript test framework for Node.js &amp; the browser</li><li><a href="https://github.com/istanbuljs/nyc">NYC</a> — command line interface for Istanbul, a JS code coverage tool that computes statement, line, function and branch coverage</li><li><a href="https://github.com/karma-runner/karma">Karma</a> — test runner for JavaScript, which is able to run Mocha and others</li><li><a href="https://coveralls.io/">Coveralls</a> — code coverage tracking system</li><li><a href="https://www.typescriptlang.org/">TSC</a> — TypeScript compiler</li><li><a href="https://webpack.js.org/">Webpack</a> — module bundler, bundles JavaScript files for usage in a browser</li><li><a href="https://github.com/typicode/husky">Husky</a> — runs NPM scripts from git hooks</li><li><a href="https://github.com/okonet/lint-staged">Lint-staged</a> — runs linters on git staged files</li><li><a href="https://github.com/facebook/jest">Jest</a> — JavaScript testing framework with a focus on simplicity, can be used as replacement for Karma+Mocha+Istanbul</li></ul><h3>Conclusion</h3><p>This approach with minor variations (mostly alternate phases) was successfully implemented and tested in different projects and proven to be consistent and complete. I hope it will help you to build, test and interact with your projects more efficiently.</p><p>This whole thing definitely requires some library to make things easier :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=691c64b51acb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/monorepo-scripts-strategies-naming-conventions-691c64b51acb">Monorepo scripts strategies &amp; naming conventions</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[React, Redux, Auth state and API]]></title>
            <link>https://medium.com/disdj/react-redux-auth-state-and-api-bfa2b9c99dc3?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/bfa2b9c99dc3</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[auth]]></category>
            <category><![CDATA[redux]]></category>
            <category><![CDATA[redux-thunk]]></category>
            <category><![CDATA[code]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Fri, 20 Apr 2018 16:51:36 GMT</pubDate>
            <atom:updated>2018-10-31T20:13:16.954Z</atom:updated>
            <content:encoded><![CDATA[<p>One of the most frequently asked questions about React, Redux and REST is where to put authentication (OAuth tokens for example), how to wire it to Redux and how to add tokens to API calls.</p><p>Since user auth is a critical part of app state it makes sense to put it into Redux store. I suggest to add an API call method to Redux by utilizing the redux-thunk middleware’s functionality and dispatch Promise from API actions by using redux-promise-middleware. Let’s begin with store configuration:</p><pre>import {applyMiddleware, createStore} from &quot;redux&quot;;<br>import thunkMiddleware from &quot;redux-thunk&quot;;<br>import promiseMiddleware from &quot;redux-promise-middleware&quot;;<br>import isPromise from &quot;is-promise&quot;;<br>import {request} from &quot;./lib&quot;;<br>import reducers from &quot;./reducers&quot;;</pre><pre>const <strong>HTTP_REJECTED</strong> = &#39;HTTP_REJECTED&#39;;</pre><pre>const <strong>suppressedTypes</strong> = [<strong>HTTP_REJECTED</strong>];</pre><pre>// this middleware suppresses uncaught rejections for HTTP actions<br>const <strong>errorMiddleware</strong> = () =&gt; next =&gt; <strong>action</strong> =&gt; {<br>    <br>    if (!isPromise(<strong>action</strong>.payload)) return next(<strong>action</strong>);<br>    <br>    if (<strong>suppressedTypes</strong>.includes(action.type)) {<br>        // Dispatch initial pending promise, but catch any errors<br>        return next(action).catch(error =&gt; {<br>            console.warn(&#39;Middleware has caught an error&#39;, error);<br>            return error;<br>        });<br>    }</pre><pre>    return next(action);</pre><pre>};</pre><pre>export default (initialState) =&gt; {</pre><pre>    let <strong>store</strong>;<br>    <br>    // this is some library that makes requests<br>    const <strong>api</strong> = ({method = &#39;GET&#39;, url, data, query}) =&gt; request({ <br>        method,<br>        url,<br>        data,<br>        query,<br>        token: <strong>store</strong>.getState().token // here goes your selector<br>    }).catch(e =&gt; {</pre><pre>// service action for reducers to capture HTTP errors<br>        <strong>store</strong>.dispatch({<br>            type: <strong>HTTP_REJECTED</strong>,<br>            payload: e<br>        });</pre><pre>        throw e; // re-throw an error</pre><pre>    });</pre><pre>    <strong>store</strong> = createStore(<br>        reducers,<br>        initialState,<br>        applyMiddleware(<br>            <strong>thunkMiddleware</strong>.withExtraArgument(<strong>api</strong>),<br>            <strong>errorMiddleware</strong>, // must be before promiseMiddleware<br>            <strong>promiseMiddleware</strong>()<br>        )<br>    );</pre><pre>    return <strong>store</strong>;</pre><pre>};</pre><p>Note that we first initialize empty store that we will access later in api function because we need api function as parameter for thunkMiddleware.withExtraArgument which is used for store creation. This can be done in a more elegant way, of course, but for simplicity we do it this brutal way.</p><p>Then you can dispatch a Promise returned from API method that becomes available in actions (redux-thunk makes it possible):</p><pre>export const loadUser = (id) =&gt; (dispatch, getState, api) =&gt; dispatch({<br>    type: &#39;LOAD_USER&#39;,<br>    payload: api({url: `/users/${id}`})<br>});</pre><pre>export const login = (username, password) =&gt; <br>    (dispatch, getState, api) =&gt; dispatch({<br>        type: &#39;LOGIN&#39;,<br>        payload: api({<br>            method: &#39;POST&#39;,<br>            url: `/login`,<br>            body: {username, password}<br>        })<br>    });</pre><p>In reducer you can now also invalidate token on HTTP 401 errors:</p><pre>import {combineReducers} from &quot;redux&quot;;</pre><pre>const <strong>isHttpAuthError</strong> = (payload) =&gt; (<br>    // make sure request library error has response in it<br>    payload &amp;&amp; payload.response &amp;&amp; payload.response.status === 401<br>);</pre><pre>const token = (state = null, {type, payload}) =&gt; {<br>    switch (type) {<br>        case (&#39;LOGIN_FULLFILLED&#39;):<br>            // here you get a token from login API response            <br>            return payload.token; <br>        case (<strong>HTTP_REJECTED</strong>):<br>            return (<strong>isHttpAuthError</strong>(payload)) ? null : state;<br>        case (&#39;LOGIN_REJECTED&#39;):<br>        case (&#39;LOGOUT_FULLFILLED&#39;):<br>            // here you reset the token<br>            return null;<br>        default:<br>            return state;<br>    }<br>};</pre><pre>export default combineReducers({<br>    token<br>});</pre><p>This all makes your app state to be always consistent, you can capture network auth-related errors in the same place where you take care of other token-related processes.</p><p>This repo <a href="https://github.com/kirill-konshin/react-redux-router-auth">https://github.com/kirill-konshin/react-redux-router-auth</a> contains all above mentioned concepts and a bit more.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bfa2b9c99dc3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/react-redux-auth-state-and-api-bfa2b9c99dc3">React, Redux, Auth state and API</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Pismo Beach Sand Dunes]]></title>
            <link>https://medium.com/disdj/pismo-beach-sand-dunes-67591dc5d89b?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/67591dc5d89b</guid>
            <category><![CDATA[california]]></category>
            <category><![CDATA[sand-dunes]]></category>
            <category><![CDATA[travel]]></category>
            <category><![CDATA[usa]]></category>
            <category><![CDATA[pismo-beach]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Thu, 29 Jun 2017 21:21:01 GMT</pubDate>
            <atom:updated>2017-06-29T21:21:01.363Z</atom:updated>
            <content:encoded><![CDATA[<p>If you ever would like to see the decorations and ambience of Mad Max movie you should try renting a buggy and ride those sand dunes!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iWyLb9LO4iI7TjLk0JzaBA.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ogBEoE08RtXdyLW8CZgPrQ.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2rjjIcBZ4ib87lZSZyZ6GA.jpeg" /></figure><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FjFqiQCUUtrE%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DjFqiQCUUtrE&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FjFqiQCUUtrE%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/af6a4b1a4c66dd0b74802261183bff96/href">https://medium.com/media/af6a4b1a4c66dd0b74802261183bff96/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FZ-F5L35JNT0%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DZ-F5L35JNT0&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FZ-F5L35JNT0%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/d0fc165092058a95f0c50460c622724d/href">https://medium.com/media/d0fc165092058a95f0c50460c622724d/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=67591dc5d89b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/pismo-beach-sand-dunes-67591dc5d89b">Pismo Beach Sand Dunes</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rule]]></title>
            <link>https://medium.com/disdj/rule-9eae2773a58a?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/9eae2773a58a</guid>
            <category><![CDATA[electronic-music]]></category>
            <category><![CDATA[trance]]></category>
            <category><![CDATA[music]]></category>
            <category><![CDATA[breakbeats]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Sun, 11 Jun 2017 07:00:00 GMT</pubDate>
            <atom:updated>2017-06-29T19:32:41.427Z</atom:updated>
            <content:encoded><![CDATA[<p>Haven’t written anything for quite a while, so here is a brand new track that I’ve just finished: <a href="https://soundcloud.com/dis/rule">https://soundcloud.com/dis/rule</a>, also available for free download.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fw.soundcloud.com%2Fplayer%2F%3Furl%3Dhttp%253A%252F%252Fapi.soundcloud.com%252Ftracks%252F326912062%26show_artwork%3Dtrue&amp;url=https%3A%2F%2Fsoundcloud.com%2Fdis%2Frule&amp;image=http%3A%2F%2Fi1.sndcdn.com%2Fartworks-000227724707-pwxs5t-t500x500.jpg&amp;key=d04bfffea46d4aeda930ec88cc64b87c&amp;type=text%2Fhtml&amp;schema=soundcloud" width="800" height="166" frameborder="0" scrolling="no"><a href="https://medium.com/media/38c61f9bfaa8ed2fffe12141c2cb1582/href">https://medium.com/media/38c61f9bfaa8ed2fffe12141c2cb1582/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9eae2773a58a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/rule-9eae2773a58a">Rule</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Teotihuacan, Mexico]]></title>
            <link>https://medium.com/disdj/teotihuacan-mexico-eadc066fb41c?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/eadc066fb41c</guid>
            <category><![CDATA[teotihuacan]]></category>
            <category><![CDATA[mexico]]></category>
            <category><![CDATA[travel]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Sun, 28 May 2017 07:00:00 GMT</pubDate>
            <atom:updated>2017-06-29T06:12:27.158Z</atom:updated>
            <content:encoded><![CDATA[<p>Teotihuacan is a small city near Mexico city, it’s conveniently located so one day trip is certainly a possibility.</p><p>The Pyramid of the Sun is the largest building in Teotihuacan (height is 71 meters), believed to have been constructed about 200 CE, and one of the largest in Mesoamerica. Found along the Avenue of the Dead, half way to the Pyramid of the Moon.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*19JXSVm_q5XP_YjyVU_fKA.jpeg" /><figcaption>Piramide Del Sol, Teotihuacan</figcaption></figure><p>I’ve heard that steps on pyramids are made huge on purpose, back in the days people were a bit shorter so huge steps forced them to climb on knees when they were reaching the top, so that when they face gods they would be already on their knees.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*18kpfiO3CJl_mZfFxRkhEQ.jpeg" /><figcaption>Upper section of stairs on Piramide Del Sol, Teotihuacan, quite steep ;)</figcaption></figure><p>As a reward for all the efforts you can enjoy picturesque views from the top of the pyramid.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O0fytu-niwzyr5fJHyahCA.jpeg" /><figcaption>Pano of the entire Teotihuacan, the main road is called Road Of The Dead</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VVDCszCY5sn-hnIWu2H6Ug.jpeg" /><figcaption>Piramide De La Luna, Teotihuacan</figcaption></figure><p>At the end of Road Of The Dead you can find some small pyramids and the big one called Piramide De La Luna.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9K6uVVRjkbDLrWHoLH4Mqw.jpeg" /><figcaption>View from lower section of Piramide De La Luna</figcaption></figure><p>On my way back to main entrance I decided to stay off the main path and explore surroundings a bit. Nothing special, just burned desert and cactuses. From above this looked more promising.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3H7Nxy9vP5kFKjLhvs5w9Q.jpeg" /><figcaption>Burned desert, cactus, Teotihuacan</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eadc066fb41c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/teotihuacan-mexico-eadc066fb41c">Teotihuacan, Mexico</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Yosemite Falls]]></title>
            <link>https://medium.com/disdj/yosemite-falls-551c8e57d0bb?source=rss-133065105ba4------2</link>
            <guid isPermaLink="false">https://medium.com/p/551c8e57d0bb</guid>
            <category><![CDATA[hiking]]></category>
            <category><![CDATA[yosemite-national-park]]></category>
            <category><![CDATA[california]]></category>
            <category><![CDATA[travel]]></category>
            <category><![CDATA[usa]]></category>
            <dc:creator><![CDATA[Kirill Konshin]]></dc:creator>
            <pubDate>Fri, 12 May 2017 07:00:00 GMT</pubDate>
            <atom:updated>2017-06-29T21:23:38.411Z</atom:updated>
            <content:encoded><![CDATA[<p>This time we hiked from the bottom of the valley to the very top of Yosemite Falls, the Yosemite Point.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MrDjTLdjXmAcMr7oZ7lbng.jpeg" /><figcaption>Here you can see the entire trail from the very bottom to the very top</figcaption></figure><p>As stated on NPS website (also used to have a hilarious warning about heights):</p><blockquote>Yosemite Falls Trail leads to the top of North America’s tallest waterfall, which rises 2,425 feet (739 m) above the Valley floor.</blockquote><blockquote>Don’t bring along any friends whose medication has led them to believe they can fly.</blockquote><p>The hike starts at the bottom of the valley and takes ridiculous amount of twists and turns and switchbacks on the way up.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YOSEGdKg44NpTHoolwGAXw.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GGTmng26aTaHBShfN9-Znw.jpeg" /><figcaption>Half-way to Upper Yosemite fall</figcaption></figure><p>Final section of is even steeper than the one that ends at Lower Fall.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SuLCVR1b4I-tq2eJ_zpK3A.jpeg" /><figcaption>Top of the fall was still covered with snow although it was middle of April…</figcaption></figure><p>All trails were covered with water, streams were everywhere, this winter was quite snowy so when temperatures went up all that snow began to actively melt. Good for us, waterfalls were very strong at that moment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dOqLwzNswK_c2Kqc_VPljQ.jpeg" /></figure><p>The views from the top are super-rewarding. Perfect spot to restore your breath and relax before taking a way down.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b7oyWL9wjbl2xW0IIwIpPg.jpeg" /><figcaption>At the top of Yosemite Point, you can see the Half Dome behind me</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oXCJmhmrIH-62XU6r5pdrg.jpeg" /><figcaption>Views at the top are amazing, that’s a generous reward for all the hard time :)</figcaption></figure><p>Overall the hike is quite strenuous, majority of it you basically just climb various kinds of flatter and steeper steps, some small parts of the trail are relatively flat though.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7MypdAxlH4emnIqOYkPyPA.png" /><figcaption>It’s quite visible that way down took way less time than way up ;)</figcaption></figure><p>Some more views of the valley from next day:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LZSPIbJquPlM_uK7TD1GRQ.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*madqSY9aqNOpQ1gIX73edw.jpeg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=551c8e57d0bb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/disdj/yosemite-falls-551c8e57d0bb">Yosemite Falls</a> was originally published in <a href="https://medium.com/disdj">D.i.S</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>