<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thai Pangsakulyanont</title>
    <description>The latest articles on DEV Community by Thai Pangsakulyanont (@dtinth).</description>
    <link>https://dev.to/dtinth</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F89453%2F895f7cda-fcfa-49f2-9a40-170a17b7d7da.jpeg</url>
      <title>DEV Community: Thai Pangsakulyanont</title>
      <link>https://dev.to/dtinth</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dtinth"/>
    <language>en</language>
    <item>
      <title>A survey of JWT storage locations in client-side app SDKs</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Sat, 10 Sep 2022 09:52:16 +0000</pubDate>
      <link>https://dev.to/dtinth/a-survey-of-jwt-storage-locations-in-client-side-app-sdks-5n2</link>
      <guid>https://dev.to/dtinth/a-survey-of-jwt-storage-locations-in-client-side-app-sdks-5n2</guid>
      <description>&lt;p&gt;The question about where to store JWTs (and access tokens in general) is a common one where there seems to be no consensus among developers.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; &lt;/p&gt;

&lt;p&gt;Some believe that JWTs should never ever be stored in &lt;code&gt;localStorage&lt;/code&gt;, while others believe that it’s okay to store them there. Each side has their own reasonings and explanations for their beliefs. It has been discussed at length, and in my opinion, to the point of unproductivity.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Rather than adding to this never-ending debate, let’s instead take a look at popular SDKs to see how they are helping single page applications (SPA) store their JWTs. Here we go!&lt;/p&gt;




&lt;h2&gt;
  
  
  Auth0
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhamr6e5eb8e849qlwl8s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhamr6e5eb8e849qlwl8s.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For SPAs, Auth0 does not store a JWT in client-side storage at all.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Instead, Auth0 SDK embeds a hidden &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; which performs the &lt;a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-proof-key-for-code-exchange-pkce" rel="noopener noreferrer"&gt;Authorization Code Flow with Proof Key for Code Exchange (PKCE)&lt;/a&gt; in conjection with &lt;a href="https://auth0.com/docs/authenticate/login/configure-silent-authentication" rel="noopener noreferrer"&gt;Silent Authentication&lt;/a&gt;. This flow is performed every time the SPA is loaded, so the JWT is not persisted at any storage location. Additionally, token requests are performed in Web Workers, providing additional layer of security.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9nxznv539ozb86cwthp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9nxznv539ozb86cwthp.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The signed in state is maintained inside the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; using &lt;code&gt;HttpOnly&lt;/code&gt; cookies. None of the cookies are JWT.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After the authentication flow is complete, then the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; is removed from the DOM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Safari with Intelligent Tracking Protection blocks third-party cookies, which can &lt;a href="https://auth0.com/docs/troubleshoot/authentication-issues/renew-tokens-when-using-safari" rel="noopener noreferrer"&gt;break the Silent authentication flow&lt;/a&gt;. To work around this, you can &lt;a href="https://auth0.com/docs/customize/custom-domains" rel="noopener noreferrer"&gt;set up custom domains for Auth0 (requires a paid plan)&lt;/a&gt; so that Auth0’s session cookies become a first-party one. Alternatively, the &lt;a href="https://auth0.com/docs/troubleshoot/authentication-issues/renew-tokens-when-using-safari#workarounds" rel="noopener noreferrer"&gt;Refresh Token Rotation functionality can be used&lt;/a&gt;, but then your signed-in state would be lost when you refresh the page, &lt;a href="https://auth0.com/docs/libraries/auth0-single-page-app-sdk#change-storage-options" rel="noopener noreferrer"&gt;unless you opt to store the tokens in &lt;code&gt;localStorage&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  AWS Amplify
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjocjrn77pcmqslesgbl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjocjrn77pcmqslesgbl.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ID tokens, access tokens, and refresh tokens are stored in &lt;code&gt;localStorage&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They are all JWTs. You know because they start with &lt;code&gt;eyJ&lt;/code&gt;. They are RS256 signed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Firebase Auth
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjuw4roacyycnee15xu8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjuw4roacyycnee15xu8.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Firebase Auth stores the access token and refresh tokens in IndexedDB &lt;a href="https://firebase.google.com/docs/auth/web/auth-state-persistence" rel="noopener noreferrer"&gt;by default&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;accessToken&lt;/code&gt; is a RS256-signed JWT. It also functions as an ID token (and are referred to as such in the documentation). Using RS256 JWT is convenient because &lt;a href="https://firebase.google.com/docs/auth/admin/verify-id-tokens" rel="noopener noreferrer"&gt;third-party apps can verify the ID token&lt;/a&gt; using &lt;a href="https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com" rel="noopener noreferrer"&gt;Firebase’s public keys&lt;/a&gt; without having to share any secret keys.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Meanwhile, the refresh token is an opaque string.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Supabase
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wdyp4u9ifkq1306hztu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wdyp4u9ifkq1306hztu.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Supabase stores the access token and refresh token in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The access token is a HS256-signed JWT, while the refresh token is an opaque string.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Conclusions
&lt;/h1&gt;

&lt;p&gt;In conclusion, different auth SDKs implemented different default behaviors for storing tokens.&lt;/p&gt;

&lt;p&gt;It is important to consider the context around choices made by these SDKs. Just because a few popular SDKs store tokens in &lt;code&gt;localStorage&lt;/code&gt; doesn’t automatically mean that doing the same in our custom implementations will give us a secure authentication scheme; we also have to look at how these SDKs help developers protect the tokens from misuse.&lt;/p&gt;

&lt;p&gt;If you have more examples to add, feel free to comment!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/swyx/status/1133780714988736512" rel="noopener noreferrer"&gt;https://twitter.com/swyx/status/1133780714988736512&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/webdev/comments/bpcleu/so_whats_the_issue_with_jwts_in_localstorage/" rel="noopener noreferrer"&gt;https://www.reddit.com/r/webdev/comments/bpcleu/so_whats_the_issue_with_jwts_in_localstorage/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/wesbos/status/1310637597480411138" rel="noopener noreferrer"&gt;https://twitter.com/wesbos/status/1310637597480411138&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/44133536/is-it-safe-to-store-a-jwt-in-localstorage-with-reactjs" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/44133536/is-it-safe-to-store-a-jwt-in-localstorage-with-reactjs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;
  Please ignore my ramblings.
  &lt;p&gt;Whenever the topic of token storage is tangentially touched on, it seems someone has to proclaim “never store JWTs in localStorage” and derail the conversation. Happens so frequently, and it is fascinating to see the length people will sometimes go to to prove their point. When I pointed out about our defenses against XSS and untrusted assets, I got a reply “but the user may install a malicious software that can read storage. Here I created an extension that reads the localStorage of the website. See? localStorage bad.” &lt;/p&gt;

 ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>A workflow to generate Git patches for auto-fixable issues</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Mon, 06 Dec 2021 15:08:14 +0000</pubDate>
      <link>https://dev.to/dtinth/a-workflow-to-generate-git-patches-for-auto-fixable-issues-5068</link>
      <guid>https://dev.to/dtinth/a-workflow-to-generate-git-patches-for-auto-fixable-issues-5068</guid>
      <description>&lt;p&gt;Don’t you feel frustrated when you make a trivial change in your codebase, like editing some text using the web editor…:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fP393o-e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/edit_text.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fP393o-e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/edit_text.png" alt="" width="880" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;…or bumping some dependencies…:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IBfbH8eD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/bump_dep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IBfbH8eD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/bump_dep.png" alt="" width="880" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;…only to see your CI build failing because of reasons like these?:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prettier doesn’t like it that you don’t wrap lines at 80 characters:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GxBJ_n8T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/prettier_error.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GxBJ_n8T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/prettier_error.png" alt="" width="880" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You forgot to update your test snapshots:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zQzlmWxa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/snapshot_error.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zQzlmWxa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/snapshot_error.png" alt="" width="880" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You didn't run &lt;code&gt;yarn&lt;/code&gt; to update the lockfile:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---RUQdfXm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/lockfile_error.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---RUQdfXm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/lockfile_error.png" alt="" width="880" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Other trivially fixable errors:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jF8bmxiE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/eslint_fixable.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jF8bmxiE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/eslint_fixable.png" alt="" width="880" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I get very annoyed when issues like this happens. It’s important to put in checks to keep things tidy and ensure coding style is used consistently. But perhaps we can make it a bit less annoying…&lt;/p&gt;

&lt;p&gt;I was once on a large codebase where running the linter and the tests takes 5 minutes. When I make simple changes that breaks any formatting or any snapshot, I lose a lot of time: 5 minutes to know that there’s a problem. 5 minutes to run &lt;code&gt;eslint --fix&lt;/code&gt; or &lt;code&gt;jest -u&lt;/code&gt; and commit+push the changes. And 5 more minutes to verify the fix on CI.&lt;/p&gt;

&lt;p&gt;While it’s important to speed up the CI builds, why not also let the CI generate a patch file that you can instantly apply on your branch?&lt;/p&gt;

&lt;h3&gt;
  
  
  My Workflow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;(Before)&lt;/strong&gt; Instead of simply running checks and failing the build…:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn --frozen-lockfile&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn lint&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn prettier --check .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;(After)&lt;/strong&gt; …we let GitHub Actions try to fix the auto-fixable problems…:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn --no-immutable&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn lint --fix&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn test --updateSnapshot&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn prettier --write .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;…then we stage the changed files and finally use the &lt;a href="https://github.com/marketplace/actions/patch-generator"&gt;Patch Generator&lt;/a&gt; action (newly created for this hackathon) to generate a patch command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git add --update&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtinth/patch-generator-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When this workflow is run and there is any auto-fixable changes, Patch Generator will generate a shell command that you paste into your local machine to instantly apply the required changes. It also works with binary files. And if the changes are too big, you can download the patch file from the build artifacts and apply using the &lt;a href="https://git-scm.com/docs/git-apply"&gt;&lt;code&gt;git apply&lt;/code&gt;&lt;/a&gt; command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cW56JfaL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/with_this_command_a2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cW56JfaL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/with_this_command_a2.png" alt="" width="880" height="942"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, you can also review the diff from the actions log to see what would happen when you run the above command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4a8SQLBE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/with_this_command_b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4a8SQLBE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/12/06/with_this_command_b.png" alt="" width="880" height="1719"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Submission Category:
&lt;/h3&gt;

&lt;p&gt;Maintainer Must-Haves&lt;/p&gt;
&lt;h3&gt;
  
  
  Yaml File or Link to Code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Example project&lt;/strong&gt; (&lt;a href="https://github.com/dtinth/patch-generator-action-demo/pull/1"&gt;example PR&lt;/a&gt;, &lt;a href="https://github.com/dtinth/patch-generator-action-demo/blob/main/.github/workflows/example.yml"&gt;workflow file&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dtinth"&gt;
        dtinth
      &lt;/a&gt; / &lt;a href="https://github.com/dtinth/patch-generator-action-demo"&gt;
        patch-generator-action-demo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Demo project for Patch Generator action.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patch Generator Action&lt;/strong&gt; (&lt;a href="https://github.com/dtinth/patch-generator-action/blob/main/action.yml"&gt;action source&lt;/a&gt;, &lt;a href="https://github.com/marketplace/actions/patch-generator"&gt;marketplace&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dtinth"&gt;
        dtinth
      &lt;/a&gt; / &lt;a href="https://github.com/dtinth/patch-generator-action"&gt;
        patch-generator-action
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Frustrated by builds failing due to trivial, auto-fixable issues? Use this action to generate a shell command to apply changes.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  Additional Resources / Info
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;patch-generator-action&lt;/code&gt; is created as a &lt;a href="https://docs.github.com/en/actions/creating-actions/creating-a-composite-action"&gt;composite action&lt;/a&gt;. This approach has some benefits over &lt;a href="https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action"&gt;JavaScript actions&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;People can fork and modify the action without having to install any build tool or go through any build steps. Development for this action mostly happen in &lt;a href="https://github.com/github/dev"&gt;&lt;code&gt;github.dev&lt;/code&gt;&lt;/a&gt; without any other build tooling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since composite actions uses the same syntax as a workflow, people can also yoink the action code, and put them directly in their workflow files, if they so choose.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am using the workflow in these open source projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bemuse&lt;/strong&gt; (Rush + markdown-toc + ESLint + Prettier; &lt;a href="https://github.com/bemusic/bemuse/blob/master/.github/workflows/tidy.yml"&gt;workflow file&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/bemusic"&gt;
        bemusic
      &lt;/a&gt; / &lt;a href="https://github.com/bemusic/bemuse"&gt;
        bemuse
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      ⬤▗▚▚▚ Web-based online rhythm action game. Based on HTML5 technologies, React, Redux and Pixi.js.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fresh App Factory&lt;/strong&gt; (Yarn + Prettier; &lt;a href="https://github.com/fresh-app/factory/blob/main/.github/workflows/tidy.yml"&gt;workflow file&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/fresh-app"&gt;
        fresh-app
      &lt;/a&gt; / &lt;a href="https://github.com/fresh-app/factory"&gt;
        factory
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      We produce freshly-baked apps every midnight
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patch Generator Action&lt;/strong&gt; &lt;em&gt;(Itself!)&lt;/em&gt; (Prettier; &lt;a href="https://github.com/dtinth/patch-generator-action/blob/main/.github/workflows/tidy.yml"&gt;workflow file&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dtinth"&gt;
        dtinth
      &lt;/a&gt; / &lt;a href="https://github.com/dtinth/patch-generator-action"&gt;
        patch-generator-action
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Frustrated by builds failing due to trivial, auto-fixable issues? Use this action to generate a shell command to apply changes.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The action is released under the &lt;a href="https://unlicense.org/"&gt;Unlicense&lt;/a&gt;, i.e., into the public domain.&lt;/p&gt;

</description>
      <category>actionshackathon21</category>
      <category>github</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Embracing gradual typing — Strategies for adopting TypeScript into a large project (Talk)</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Thu, 14 Jan 2021 11:26:45 +0000</pubDate>
      <link>https://dev.to/dtinth/embracing-gradual-typing-strategies-for-adopting-typescript-into-a-large-project-talk-5cnf</link>
      <guid>https://dev.to/dtinth/embracing-gradual-typing-strategies-for-adopting-typescript-into-a-large-project-talk-5cnf</guid>
      <description>&lt;p&gt;&lt;strong&gt;There are many challenges when trying to adopt TypeScript into your JavaScript project.&lt;/strong&gt; Your project may already have tons of untyped files, changing your build system can sound risky, and your colleages may ask if the cost of investing in migrating all the code to TypeScript would be worth the effort or not.&lt;/p&gt;

&lt;p&gt;Some may argue that they wouldn’t need types because they already have tests. Some may question whether the benefit would really outweigh the TypeScript tax. Some may have had bad experience with earlier versions of TypeScript before. And there are many outdated opinion pieces everywhere.&lt;/p&gt;

&lt;p&gt;But &lt;em&gt;adopting TypeScript doesn’t have to be a big, all-or-none effort.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are strategies for &lt;em&gt;incrementally&lt;/em&gt; introducing pieces and bits of TypeScript into your JavaScript project, each little step &lt;em&gt;immediately improving the developer experience&lt;/em&gt;, without having to install extra dependencies or make any changes to the build system.&lt;/p&gt;

&lt;p&gt;This is what I discuss about in this talk. It also contains real-world examples which is kinda hard to convey just in text.&lt;/p&gt;

&lt;p&gt;
  Topics discussed
  &lt;ul&gt;
&lt;li&gt;How you may already be using TypeScript in your JavaScript project.&lt;/li&gt;
&lt;li&gt;TypeScript the language, the compiler, and the language service.&lt;/li&gt;
&lt;li&gt;Using JSDoc to improve type inference, code completions, and IntelliSense.&lt;/li&gt;
&lt;li&gt;Configuring &lt;code&gt;jsconfig.json&lt;/code&gt; for improved code actions and automatic refactoring.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;// @ts-check&lt;/code&gt; to type-check JavaScript files (with examples on dealing with a few type-checking errors).&lt;/li&gt;
&lt;li&gt;Enabling &lt;code&gt;checkJs&lt;/code&gt; to type-check JavaScript files project-wide.&lt;/li&gt;
&lt;li&gt;Creating a &lt;code&gt;.d.ts&lt;/code&gt; file next to a JavaScript file to keep the &lt;code&gt;.js&lt;/code&gt; file unmodified.&lt;/li&gt;
&lt;li&gt;Creating a global &lt;code&gt;.d.ts&lt;/code&gt; file to declare modules and global variables.&lt;/li&gt;
&lt;li&gt;Discussion on strategies for improving developer productivity, improving code documentation, and reducing chance of runtime errors.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The talk is in Thai language but I added English captions, and all slides are in English.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/xATsf5nm2yc"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Hope you find it useful, and thanks for watching! Also, please &lt;a href="https://dt.in.th/go/youtube"&gt;consider subscribing to my YouTube channel&lt;/a&gt; for more content.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>techtalks</category>
      <category>video</category>
    </item>
    <item>
      <title>A local file storage for the web and interopearability layer for web-based apps (submission)</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Sun, 10 Jan 2021 17:11:23 +0000</pubDate>
      <link>https://dev.to/dtinth/a-local-file-storage-for-the-web-and-interopearability-layer-for-web-based-apps-submission-1hcf</link>
      <guid>https://dev.to/dtinth/a-local-file-storage-for-the-web-and-interopearability-layer-for-web-based-apps-submission-1hcf</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://tmp.spacet.me/"&gt;tmp.spacet.me&lt;/a&gt;, a &lt;strong&gt;web-based local file storage application&lt;/strong&gt;. Basically, you can put files into it, and you can get files out of it. It aims to support every possible methods that browsers allow web apps to work with files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RgLgq0xn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/09/tmp-builtins.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RgLgq0xn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/09/tmp-builtins.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also comes with an &lt;strong&gt;extensions system&lt;/strong&gt;. Users can add extensions to enhance the functionalities of the app. The goal is to improve &lt;strong&gt;operability&lt;/strong&gt; between web-based applications that work with files.&lt;/p&gt;

&lt;p&gt;For example, the &lt;a href="https://tmp-docs.spacet.me/tmp-webrtc.html"&gt;tmp-webrtc&lt;/a&gt; extension lets me send files from one device to another using peer-to-peer technology WebRTC.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--H4h8oh_I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/09/image.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--H4h8oh_I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/09/image.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://tmp-docs.spacet.me/tmp-photopea.html"&gt;tmp-photopea&lt;/a&gt; extension lets me edit image files in Photopea.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8W4CqOYF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://tmp-docs.spacet.me/images/edit-with-photopea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8W4CqOYF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://tmp-docs.spacet.me/images/edit-with-photopea.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users can build their own extensions to suit their needs.&lt;/p&gt;

&lt;p&gt;For example, I built an extension to upload images to my static file hosting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--th0sfkl_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/09/tmp-uploader.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--th0sfkl_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/09/tmp-uploader.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although tmp.spacet.me is built using React and Next.js, this is just an implementation detail and extension authors don’t need to know any React. That’s because &lt;strong&gt;extensions run on their own page&lt;/strong&gt; and communicate with tmp.spacet.me via the &lt;code&gt;postMessage&lt;/code&gt; API only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Category Submission
&lt;/h3&gt;

&lt;p&gt;Random Roulette&lt;/p&gt;

&lt;h3&gt;
  
  
  App Link
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://tmp.spacet.me/"&gt;https://tmp.spacet.me/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://tmp.spacet.me/"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--20mFKz0P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/dtinth/timelapse/blob/master/projects/tmp.spacet.me_initial.png%3Fraw%3Dtrue" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Description
&lt;/h3&gt;

&lt;p&gt;A web-based local file storage. You can put files into it, and you can get files out of it. More functionalities can be added through extensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Link to Source Code
&lt;/h3&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vJ70wriM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dtinth"&gt;
        dtinth
      &lt;/a&gt; / &lt;a href="https://github.com/dtinth/tmp"&gt;
        tmp
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Web-based local storage
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;h1&gt;
tmp.spacet.me&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://tmp.spacet.me/" rel="nofollow"&gt;&lt;strong&gt;tmp.spacet.me&lt;/strong&gt;&lt;/a&gt; is a web-based local file storage application. Basically, you can put files into it, and you can get files out of it. It aims to support every possible methods that browsers allow web apps to work with files.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://camo.githubusercontent.com/1b2f4b4dc7a41f29b59afa402b4b5b735ce6a1cfc50a85f68e9694f2db13c25c/68747470733a2f2f7374617469632e64742e696e2e74682f75706c6f6164732f323032312f30312f30392f746d702d6275696c74696e732e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/1b2f4b4dc7a41f29b59afa402b4b5b735ce6a1cfc50a85f68e9694f2db13c25c/68747470733a2f2f7374617469632e64742e696e2e74682f75706c6f6164732f323032312f30312f30392f746d702d6275696c74696e732e706e67" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It also comes with an &lt;strong&gt;extensions system&lt;/strong&gt;. Users can add extensions to enhance the functionalities of the app.&lt;/p&gt;
&lt;p&gt;For example, the &lt;a href="https://tmp-docs.spacet.me/tmp-webrtc.html" rel="nofollow"&gt;tmp-webrtc&lt;/a&gt; extension lets me send files from one device to another using peer-to-peer technology WebRTC.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://camo.githubusercontent.com/b6620e628106077421fa5de50df705d56f5f8f6219f576fd4b57b584c6f233cf/68747470733a2f2f7374617469632e64742e696e2e74682f75706c6f6164732f323032312f30312f30392f696d6167652e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/b6620e628106077421fa5de50df705d56f5f8f6219f576fd4b57b584c6f233cf/68747470733a2f2f7374617469632e64742e696e2e74682f75706c6f6164732f323032312f30312f30392f696d6167652e706e67" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://tmp-docs.spacet.me/tmp-photopea.html" rel="nofollow"&gt;tmp-photopea&lt;/a&gt; extension lets me edit image files in Photopea.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://camo.githubusercontent.com/7bf80c81a92f4bebbdb15852e0c5a3088363a1f3c2a2281cd956bca2c76486f0/68747470733a2f2f746d702d646f63732e7370616365742e6d652f696d616765732f656469742d776974682d70686f746f7065612e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/7bf80c81a92f4bebbdb15852e0c5a3088363a1f3c2a2281cd956bca2c76486f0/68747470733a2f2f746d702d646f63732e7370616365742e6d652f696d616765732f656469742d776974682d70686f746f7065612e706e67" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Users can build their own extensions to suit their needs.&lt;/p&gt;
&lt;p&gt;For example, I built an extension to upload images to my static file hosting.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://camo.githubusercontent.com/3ecbc86dccf5446eaf38103571916cadb8fb800f011c8580ba46dd7a1244bc9b/68747470733a2f2f7374617469632e64742e696e2e74682f75706c6f6164732f323032312f30312f30392f746d702d75706c6f616465722e706e67"&gt;&lt;img src="https://camo.githubusercontent.com/3ecbc86dccf5446eaf38103571916cadb8fb800f011c8580ba46dd7a1244bc9b/68747470733a2f2f7374617469632e64742e696e2e74682f75706c6f6164732f323032312f30312f30392f746d702d75706c6f616465722e706e67" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For more information, including the Extensions API, take a look at the &lt;a href="https://tmp-docs.spacet.me/" rel="nofollow"&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
Devlog&lt;/h2&gt;
&lt;p&gt;This project is participating in &lt;a href="https://dev.to/devteam/announcing-the-digitalocean-app-platform-hackathon-on-dev-2i1k" rel="nofollow"&gt;the DigitalOcean App Platform Hackathon on DEV&lt;/a&gt; and is deployed to &lt;a href="https://try.digitalocean.com/app-platform/" rel="nofollow"&gt;DigitalOcean® App Platform&lt;/a&gt; as a static site.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/dtinth/tmp-spacet-me-devlog-part-1-3bnb" rel="nofollow"&gt;Part&lt;/a&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/dtinth/tmp"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dtinth/tmp"&gt;dtinth/tmp&lt;/a&gt; — the main app&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dtinth/tmp-webrtc"&gt;dtinth/tmp-webrtc&lt;/a&gt; — extension to send/receive files over WebRTC&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dtinth/tmp-photopea"&gt;dtinth/tmp-photopea&lt;/a&gt; — extension to edit pictures in Photopea&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dtinth/tmp-uploader"&gt;dtinth/tmp-uploader&lt;/a&gt; — extension to let me upload pictures to my static files hosting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dtinth/tmp-dtinth"&gt;dtinth/tmp-dtinth&lt;/a&gt; — extension for my personal use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Permissive License
&lt;/h3&gt;

&lt;p&gt;All code is MIT Licensed&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;There are so many great web-based tools. From &lt;a href="https://stackedit.io/"&gt;Markdown&lt;/a&gt; &lt;a href="https://dillinger.io/"&gt;editors&lt;/a&gt; to &lt;a href="https://www.sharedrop.io/"&gt;file&lt;/a&gt; &lt;a href="http://www.instant.io/"&gt;sharing&lt;/a&gt; &lt;a href="http://webdrop.space/"&gt;services&lt;/a&gt; to &lt;a href="https://www.diagrams.net/"&gt;diagramming tool&lt;/a&gt; to &lt;a href="https://figma.com/"&gt;design tool&lt;/a&gt; to &lt;a href="http://www.photopea.com/"&gt;image editor&lt;/a&gt; and &lt;a href="https://squoosh.app/"&gt;compressor&lt;/a&gt; to &lt;a href="https://vox.spacet.me/"&gt;voice&lt;/a&gt; and &lt;a href="https://gifcap.dev/"&gt;screen&lt;/a&gt; recorder to myriad of cloud storage services, etc.&lt;/p&gt;

&lt;p&gt;Many of these tools work with files. &lt;a href="https://paul.kinlan.me/unintended-silos/"&gt;But they tend to be its own silo&lt;/a&gt; and do not interoperate with other tools.&lt;/p&gt;

&lt;p&gt;There has been attempts to solve this from the web platform side. There was an experimental &lt;a href="https://en.wikipedia.org/wiki/Web_Intents"&gt;Web Intents&lt;/a&gt; API which &lt;a href="https://paul.kinlan.me/what-happened-to-web-intents/"&gt;failed&lt;/a&gt;. More recently, there are also the &lt;a href="https://web.dev/web-share/"&gt;Web Share API&lt;/a&gt; and the &lt;a href="https://web.dev/web-share-target/"&gt;Web Share Target API&lt;/a&gt; which integrates natively with the system’s Share dialog. Unfortunately Desktop devices don’t have them and so it’s only available on Android right now.&lt;/p&gt;

&lt;p&gt;So, as I become more reliant on web-based tools, I want to try my take at a &lt;strong&gt;userland solution&lt;/strong&gt;. So the missing piece, I think, is a central hub, where files can be sent to, which would then be forwarded to other web-based app or saved to disk. I want it to have an extension system, so anyone can build more integrations without having to modify the main app. Furthermore, deeper integrations with existing web-based apps can be implemented via browser extensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it
&lt;/h3&gt;

&lt;p&gt;For the main app, &lt;code&gt;tmp.spacet.me&lt;/code&gt;, I used &lt;strong&gt;DigitalOcean App Platform&lt;/strong&gt; to host the app, which is a static web application. The main application is built using &lt;strong&gt;React&lt;/strong&gt; and &lt;strong&gt;Next.js&lt;/strong&gt;. Deployment to DigitalOcean App Platform is very straightforward and I’m impressed by how easy it is to set up. I’m looking forward to support for deployment previews in the future.&lt;/p&gt;

&lt;p&gt;For the extension system, when the extension URL is entered, the main app fetches the manifest file, which contains the information about the extension, as well as which file types can be handled. Then, when the extension is activated, it opens a separate browser window and communicate with the main app via a &lt;a href="https://www.jsonrpc.org/specification"&gt;JSON-RPC&lt;/a&gt;-based protocol. This gave me opportunities to put cross-site messaging and micro-frontend concepts into practice.&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;tmp-webrtc&lt;/code&gt; extension, at first I wanted to create a backend service to implement &lt;a href="https://github.com/feross/simple-peer#a-simpler-example"&gt;peer discovery&lt;/a&gt;. That would also give me an opportunity to try out DigitalOcean’s paid services (like running backend apps and using a managed database). Unfortunately due to time constraints I didn’t get around to doing that and used the &lt;a href="https://github.com/subins2000/p2pt"&gt;P2PT&lt;/a&gt; library for peer discovery instead. P2PT uses public &lt;a href="https://github.com/webtorrent/bittorrent-tracker"&gt;WebTorrent trackers&lt;/a&gt; as a WebRTC signaling server, and so no backend services have to be deployed. So it ended up being another static app. This is my first time using P2PT and WebRTC for file transfers though, so I still think I learned a lot.&lt;/p&gt;

&lt;p&gt;I really like that DigitalOcean’s App Platform makes setting up CORS very straightforward (can be set up via the console), while &lt;a href="https://community.netlify.com/t/access-control-allow-origin-policy/1813/2"&gt;Netlify&lt;/a&gt;, &lt;a href="https://vercel.com/knowledge/how-to-enable-cors#enabling-cors-using-vercel.json"&gt;Vercel&lt;/a&gt; requires creating a configuration file. Having CORS is crucial for the extension system to work, because &lt;code&gt;tmp.spacet.me&lt;/code&gt; needs to fetch the manifest file to discover the extension's functionalities.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tmp-webrtc&lt;/code&gt; extension is built using &lt;strong&gt;Vue 3&lt;/strong&gt; and &lt;strong&gt;Vite&lt;/strong&gt;. I have never used Vite before, so that’s another thing I learned. By the way, I don’t like running commands to generate new projects, so &lt;a href="https://dev.to/dtinth/freshly-baked-apps-every-midnight-1h99"&gt;I took the opportunity to shave that yak, and created the Fresh App Factory&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also used other other cloud services other than DigialOcean. The &lt;a href="https://tmp-docs.spacet.me/"&gt;documentation site&lt;/a&gt; is just a Jekyll site hosted on GitHub Pages, which I chose for simplicity. I like that DigitalOcean App Platform allows multiple static apps (or “components”) to be hosted on the same domain, but I could not get Jekyll to work on it.&lt;/p&gt;

&lt;p&gt;The application’s UI is not polished at all and could use a lot of improvements. But I think it’s good enough for a proof-of-concept. Since I started building this, I have been using it a lot in my own personal workflow. Sending pictures and videos between my 4 devices, now I just use tmp.spacet.me.&lt;/p&gt;

&lt;p&gt;Some areas for future improvements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improving the user experience, accessibility and performance.&lt;/li&gt;
&lt;li&gt;Allowing files to be stored temporarily in the in-memory storage first before committing to PouchDB.&lt;/li&gt;
&lt;li&gt;Allowing other web apps to directly &lt;code&gt;postMessage&lt;/code&gt; to tmp.spacet.me (without an extension), reducing the irony.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you find the tool useful, and thanks for reading!&lt;/p&gt;

</description>
      <category>dohackathon</category>
      <category>showdev</category>
    </item>
    <item>
      <title>tmp.spacet.me devlog part 4</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Fri, 08 Jan 2021 15:14:11 +0000</pubDate>
      <link>https://dev.to/dtinth/tmp-spacet-me-devlog-part-4-30k8</link>
      <guid>https://dev.to/dtinth/tmp-spacet-me-devlog-part-4-30k8</guid>
      <description>&lt;p&gt;I implemented a few more integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sending and receiving files via WebRTC.&lt;/strong&gt; I used &lt;a href="https://github.com/subins2000/p2pt"&gt;P2PT&lt;/a&gt; for peer discovery and &lt;a href="https://github.com/subins2000/simple-peer-files"&gt;simple-peer-files&lt;/a&gt; for file transfer. They are based on the &lt;a href="https://github.com/feross/simple-peer"&gt;simple-peer&lt;/a&gt; library. I’ve never used them, and a hackathon is a great time to try out things I’ve never tried before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JELaSpYH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://static.dt.in.th/uploads/2021/01/07/tmp-webrtc-receive.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JELaSpYH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://static.dt.in.th/uploads/2021/01/07/tmp-webrtc-receive.gif" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After adding the extension, the “&lt;strong&gt;New item&lt;/strong&gt;” button will have a option to receive files via WebRTC. And every file will also has a new option to send them over WebRTC.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3XaPVyJn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/07/tmp-webrtc1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3XaPVyJn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/07/tmp-webrtc1.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On first use, &lt;code&gt;tmp-webrtc&lt;/code&gt; must be configured with a unique channel name. While active, each “sender” will distribute the file to every receiver on the same channel. Also, a single receiver can receive multiple files at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--M3nO2dk4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/08/tmp-webrtc2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--M3nO2dk4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/08/tmp-webrtc2.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This makes it easier for me to beam files between my Windows, Mac, iPadOS and Android devices.&lt;/p&gt;

&lt;p&gt;While &lt;code&gt;tmp.spacet.me&lt;/code&gt; is built using Next.js, &lt;code&gt;tmp-webrtc&lt;/code&gt; is built using Vue 3 with Vite, mainly because I’ve never used Vite before, so why not! I don’t like running commands to generate a new project though, so &lt;a href="https://dev.to/dtinth/freshly-baked-apps-every-midnight-1h99"&gt;I shaved that yak and created the Fresh App Factory&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then I deployed this to DigitalOcean App Platform as another app. I got hit by CORS issue, but thankfully DigitalOcean App Platform has the CORS setting right inside the tool. It is very straightforward to configure (unlike S3 where I had to write a JSON configuration).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EjGQphWT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/07/do-app-platform-cors.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EjGQphWT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/07/do-app-platform-cors.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Photopea integration:&lt;/strong&gt; Thanks to &lt;a href="https://www.photopea.com/api/"&gt;Photopea's API&lt;/a&gt; I can now edit images imported to &lt;code&gt;tmp.spacet.me&lt;/code&gt; in Photopea. The whole extension is a single &lt;a href="https://github.com/dtinth/tmp-photopea/blob/master/index.html"&gt;index.html&lt;/a&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improved extensions UI:&lt;/strong&gt; It’s now possible to delete added extensions 😂.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CAn3_Gmr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/08/tmp-ext.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CAn3_Gmr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/08/tmp-ext.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With only two days left I think what I have now is good enough for a proof-of-concept, although I think it’s not polished at all. I will spend the remaining two days on documentation and writing up the submission post.&lt;/p&gt;

</description>
      <category>dohackathon</category>
    </item>
    <item>
      <title>Freshly-baked apps every midnight</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Wed, 06 Jan 2021 20:26:08 +0000</pubDate>
      <link>https://dev.to/dtinth/freshly-baked-apps-every-midnight-1h99</link>
      <guid>https://dev.to/dtinth/freshly-baked-apps-every-midnight-1h99</guid>
      <description>&lt;p&gt;I hate running commands to generate new projects because it is relatively cumbersome compared to jumping into an existing project.&lt;/p&gt;

&lt;p&gt;Whenever I want to work on an existing project, I just jump right in and begin developing it, either on my local machine, or using many cloud-based development tool. &lt;a href="https://github.com/codespaces"&gt;Codespaces&lt;/a&gt;, &lt;a href="https://gitpod.io/"&gt;Gitpod&lt;/a&gt;, &lt;a href="https://codesandbox.io/"&gt;CodeSandbox&lt;/a&gt;, etc. which can launch a development environment right from a GitHub repository.&lt;/p&gt;

&lt;p&gt;On the other hand, when generating projects using a CLI, there is no repository to begin with. So I must start from my machine. Furthermore, I had to install dependencies twice — once for the project generator, and once more for the project itself.&lt;/p&gt;

&lt;p&gt;So I created &lt;a href="https://github.com/fresh-app/factory"&gt;&lt;strong&gt;the Fresh App Factory&lt;/strong&gt;&lt;/a&gt; which in turn creates &lt;em&gt;self-updating project templates.&lt;/em&gt; Every midnight UTC, it runs popular project scaffolding commands and pushes the resulting project to GitHub repositories. All of this is powered by GitHub Actions.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vJ70wriM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/fresh-app"&gt;
        fresh-app
      &lt;/a&gt; / &lt;a href="https://github.com/fresh-app/factory"&gt;
        factory
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      We produce freshly-baked apps every midnight
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;h1&gt;
factory&lt;/h1&gt;
&lt;p&gt;This repository generates various fresh apps every midnight (UTC time).&lt;/p&gt;
&lt;h2&gt;
Why?&lt;/h2&gt;
&lt;p&gt;Instead of having to run a script to generate a new project each time, you can just fork the templates and begin coding.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;a href="https://github.com/codespaces"&gt;Codespaces&lt;/a&gt;, &lt;a href="https://gitpod.io/" rel="nofollow"&gt;Gitpod&lt;/a&gt; or &lt;a href="https://codesandbox.io/" rel="nofollow"&gt;CodeSandbox&lt;/a&gt; to work on the project right away.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unlike many boilerplates, our fresh apps are regenerated &lt;strong&gt;daily&lt;/strong&gt;, so you get up-to-date dependencies by the time you clone them.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/fresh-app/factory"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  React
&lt;/h2&gt;

&lt;p&gt;Create React App&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-react-app"&gt;https://github.com/fresh-app/fresh-react-app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-react-app-typescript"&gt;https://github.com/fresh-app/fresh-react-app-typescript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;I initially experimented with this idea since late 2019, so you can &lt;a href="https://github.com/fresh-app/fresh-react-app-typescript/blame/main/README.md"&gt;take a look at how a freshly-generated React app's README file evolves over the years&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vite&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-vite-app-react"&gt;https://github.com/fresh-app/fresh-vite-app-react&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-vite-app-react-ts"&gt;https://github.com/fresh-app/fresh-vite-app-react-ts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next.js&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-next-app"&gt;https://github.com/fresh-app/fresh-next-app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-next-app-typescript"&gt;https://github.com/fresh-app/fresh-next-app-typescript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Vue
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-vite-app-vue"&gt;https://github.com/fresh-app/fresh-vite-app-vue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-vite-app-vue-ts"&gt;https://github.com/fresh-app/fresh-vite-app-vue-ts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Vanilla
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fresh-app/fresh-vite-app"&gt;https://github.com/fresh-app/fresh-vite-app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  etc.
&lt;/h2&gt;

&lt;p&gt;Further template projects may be added in the future. Check out &lt;a href="https://github.com/fresh-app"&gt;https://github.com/fresh-app&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>react</category>
      <category>vue</category>
      <category>javascript</category>
    </item>
    <item>
      <title>tmp.spacet.me devlog part 3</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Tue, 05 Jan 2021 20:55:46 +0000</pubDate>
      <link>https://dev.to/dtinth/tmp-spacet-me-devlog-part-3-3eb2</link>
      <guid>https://dev.to/dtinth/tmp-spacet-me-devlog-part-3-3eb2</guid>
      <description>&lt;p&gt;I added support for external extensions. The settings panel now has an area where extension URLs can be added.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zEOODFv1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-extension-settings.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zEOODFv1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-extension-settings.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first external extension I will write will be for uploading files to a custom endpoint. This helps make it easier for me to upload files to my static files hosting.&lt;/p&gt;

&lt;p&gt;I put in the extension URL: &lt;code&gt;https://raw.githubusercontent.com/dtinth/tmp-uploader/master/&lt;/code&gt;. This effectively installs the extension. The "Uploader" menu now shows up in the files list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UU_R02fc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-uploader-integration.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UU_R02fc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-uploader-integration.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I click on it, it opens a new browser window and uploads the file to my static files hosting. Then it returns the URL to the image, ready to be copied.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3FcX2_BY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-uploader-result.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3FcX2_BY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-uploader-result.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Extension installation
&lt;/h3&gt;

&lt;p&gt;When I put in the extension's URL, &lt;code&gt;tmp.spacet.me&lt;/code&gt; goes ahead and fetches manifest file by appending &lt;code&gt;/tmp-manifest.json&lt;/code&gt; to the URL. The JSON looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dtinth's uploader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upload files to cloud storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"integrations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"uploader"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Uploader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://tmp-uploader.glitch.me/"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This caused the "Uploader" to appear in the file's menu. And when clicked, &lt;code&gt;tmp&lt;/code&gt; will launch the URL specified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uploading process
&lt;/h3&gt;

&lt;p&gt;The first time I go to &lt;a href="https://tmp-uploader.glitch.me/"&gt;https://tmp-uploader.glitch.me/&lt;/a&gt;, I am shown the &lt;strong&gt;Configuration&lt;/strong&gt; screen, and asked to enter an &lt;strong&gt;Upload URL request endpoint&lt;/strong&gt;. This makes the tool generic and can theoretically but with any host that implements the specified interface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--W3Eaw4P_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-uploader-config.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--W3Eaw4P_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2021/01/05/tmp-uploader-config.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now having to configure the URL on every computer may feel cumbersome, but I normally store credentialed URLs like these in my password manager, I know exactly where to recall them should I ever need to.&lt;/p&gt;

&lt;p&gt;Now, when a file will be uploaded, the configured endpoint will be called with the filename and MIME type. It returns a JSON that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uploadUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://s3.ap-southeast-1.amazonaws.com/static.dt.in.th/uploads/2021/01/05/tmp-uploader-result.png?AWSAccessKeyId=..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"downloadUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://static.dt.in.th/uploads/2021/01/05/tmp-uploader-result.png"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the &lt;code&gt;tmp-uploader&lt;/code&gt; will make a PUT request to &lt;code&gt;uploadUrl&lt;/code&gt;. Once done the &lt;code&gt;downloadUrl&lt;/code&gt; is displayed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Endpoint implementation
&lt;/h3&gt;

&lt;p&gt;My uploaded files are hosted on Amazon S3. So I created a Lambda function according to the above specification. For &lt;code&gt;uploadUrl&lt;/code&gt;, it returns a signed URL &lt;a href="https://aws.amazon.com/blogs/compute/uploading-to-amazon-s3-directly-from-a-web-or-mobile-application/"&gt;that can be used to upload a file to S3 directly from the web browser&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;AWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queryStringParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;toJSON&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\W&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`uploads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queryStringParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3Params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;static.dt.in.th&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queryStringParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploadUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getSignedUrlPromise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;putObject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s3Params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;downloadUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://static.dt.in.th/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;uploadUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;downloadUrl&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I need to grant the Lambda function essays to the S3 bucket by attaching this policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VisualEditor0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::static.dt.in.th/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and finally I need to allow file uploads from the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowedHeaders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowedMethods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUT"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowedOrigins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://tmp-uploader.glitch.me"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ExposeHeaders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other things done
&lt;/h2&gt;

&lt;p&gt;Also implemented rudimentary Rename functionality.&lt;/p&gt;

</description>
      <category>dohackathon</category>
    </item>
    <item>
      <title>tmp.spacet.me devlog part 2</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Fri, 01 Jan 2021 09:03:47 +0000</pubDate>
      <link>https://dev.to/dtinth/tmp-spacet-me-devlog-part-2-34hi</link>
      <guid>https://dev.to/dtinth/tmp-spacet-me-devlog-part-2-34hi</guid>
      <description>&lt;p&gt;Now that the core is usable, I want to tackle a first use case — opening a file in an external viewer.&lt;/p&gt;

&lt;p&gt;To simplify this task, for now I will use a hardcoded list of external integrations. Making this list customizable will come later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;postMessage protocol:&lt;/strong&gt; The messaging protocol I decided to use is based on &lt;a href="https://www.jsonrpc.org/specification" rel="noopener noreferrer"&gt;JSON-RPC&lt;/a&gt; specification, following how &lt;a href="https://microsoft.github.io/language-server-protocol/specifications/specification-current/" rel="noopener noreferrer"&gt;Microsoft also uses it in its Language Server Protocol Specification&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I implemented integrations for these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://json.spacet.me" rel="noopener noreferrer"&gt;json.spacet.me&lt;/a&gt; - JSON viewer&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vdo.glitch.me" rel="noopener noreferrer"&gt;vdo.glitch.me&lt;/a&gt; - Video player&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F12%2F28%2Fexternal-viewer.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F12%2F28%2Fexternal-viewer.png" alt="External viewer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; When opening a file with an external viewer, tmp.spacet.me opens up a new window, passing along a session ID. Once the viewer loads and detects the session ID, it makes a JSON-RPC call to the opener to obtain the file contents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F12%2F28%2Ftmp-communication.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F12%2F28%2Ftmp-communication.png" alt="Communication"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation and refactoring
&lt;/h2&gt;

&lt;p&gt;I started by &lt;a href="http://wiki.c2.com/?DoTheSimplestThingThatCouldPossiblyWork" rel="noopener noreferrer"&gt;doing the simplest thing that could possibly work&lt;/a&gt; and hardcoded the &lt;code&gt;postMessage&lt;/code&gt; handling code. Here the code is infested with type assertions just to make TypeScript compiler happy. When experimenting, I think this is a good thing to do, just don't forget to clean it up later, which is coming up right next.&lt;/p&gt;

&lt;p&gt;You don't need to understand the whole code here, just look at the code around the comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Window&lt;/span&gt;

  &lt;span class="c1"&gt;// Check for a method call.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tmp/getOpenedFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In this block, `e.data` is `any`, so no IntelliSense.&lt;/span&gt;

    &lt;span class="c1"&gt;// (hovertip) const sessionId: any&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`session:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFilesDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openedFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;fromWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="c1"&gt;// Send a reply.&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Right now this object can be arbitrary payload;&lt;/span&gt;
        &lt;span class="c1"&gt;// there's currently no type-checking here.&lt;/span&gt;
        &lt;span class="c1"&gt;// So I might introduce a bug at some point...&lt;/span&gt;
        &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_attachments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;_rev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_rev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now while this works, I can see that this way of writing code can cause trouble soon. Right now to answer these questions you would need to read how the code is implemented, because it has never been explicitly stated, nor documented, in the code base:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which methods are available?&lt;/li&gt;
&lt;li&gt;What kind of parameters do these methods take?&lt;/li&gt;
&lt;li&gt;What do the results look like for each method?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the interface prone to break, and I don't want that. I think in this case static typing could help here. So I went ahead and created a TypeScript interface as a way to document the RPC interface. &lt;a href="http://me.dt.in.th/page/ImplementLater" rel="noopener noreferrer"&gt;I wrote it in a way that I want to read it in the future.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RpcInterface&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcDefinition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tmp/getOpenedFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nl"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Blob&lt;/span&gt;
      &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;_rev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the message handler looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcPayloadChecker&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RpcInterface&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Window&lt;/span&gt;
  &lt;span class="c1"&gt;// Changed&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isMethodCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tmp/getOpenedFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In this block, `e.data.params` shall have type&lt;/span&gt;
    &lt;span class="c1"&gt;// `RpcInterface['tmp/getOpenedFile']` which is `{ sessionId: string }`&lt;/span&gt;

    &lt;span class="c1"&gt;// (hovertip) const sessionId: string&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`session:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFilesDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openedFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;fromWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="c1"&gt;// Changed&lt;/span&gt;
      &lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replyResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// In here, the type should flow to this object.&lt;/span&gt;
        &lt;span class="c1"&gt;// So any deviations from the declared interface&lt;/span&gt;
        &lt;span class="c1"&gt;// would cause a type-checking error.&lt;/span&gt;
        &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_attachments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;_rev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_rev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the type flow, here is the rest of that code. This is, IMO, one of the very few places where I would write advanced types. IMO advanced types hurt the readability of code, so it is best used in an isolated place, where it will make types flow better to the code that uses it. TypeScript tax is a thing, but let's manage it instead of avoiding it.&lt;/p&gt;

&lt;p&gt;A litmus test I use for when to use advanced types is this: Does it reduce the amount of type annotations in other files? Don't let advanced types infect the rest of your codebase!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JsonRpcPayloadChecker&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcDefinition&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isMethodCall&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcMethodCall&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;params&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isJsonRpcMethodCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;replyResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcMethodCall&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcDefinition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;methodName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;JsonRpcMethodCall&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MethodName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MethodParams&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MethodName&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MethodParams&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isJsonRpcMethodCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>dohackathon</category>
    </item>
    <item>
      <title>Let's deploy the simplest URL redirection service to Netlify!</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Tue, 29 Dec 2020 11:49:34 +0000</pubDate>
      <link>https://dev.to/dtinth/let-s-deploy-the-simplest-url-redirection-service-to-netlify-3n7b</link>
      <guid>https://dev.to/dtinth/let-s-deploy-the-simplest-url-redirection-service-to-netlify-3n7b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Sometimes it's a good idea to post links under your own domain,&lt;/strong&gt; so that you can change the link's target when needed&lt;sup id="fnref1"&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;For example, I have a Ko-Fi page at &lt;a href="https://ko-fi.com/dtinth"&gt;https://ko-fi.com/dtinth&lt;/a&gt; but I never link to that URL directly; I use &lt;a href="http://link.dt.in.th/coffee"&gt;http://link.dt.in.th/coffee&lt;/a&gt; instead. Should I ever decide to use something else&lt;sup id="fnref2"&gt;2&lt;/sup&gt;, I only need to change the link destination.&lt;/p&gt;

&lt;p&gt;One simple, low-code way to accomplish this is use Netlify's redirect feature.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create a GitHub repo&lt;/strong&gt; and create a file &lt;code&gt;public/_redirects&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; /youtube   https://www.youtube.com/channel/UClKPjyxFSkk_dPg6YzN0Miw/   302
 /coffee    https://ko-fi.com/dtinth                                    302
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a Netlify site&lt;/strong&gt; linking to the GitHub repo and set up your domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is no step 3.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file"&gt;The &lt;code&gt;_redirects&lt;/code&gt; file&lt;/a&gt; has a simple, machine-and-human-readable, plain-text format. To add or change links, just update the file on GitHub. I find this approach powerful because I can also update this file programmatically using &lt;a href="https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#create-or-update-file-contents"&gt;GitHub's API&lt;/a&gt;, and teams can collaborate on this file like how they collaborate on code. One downside is that this approach doesn't track how many people used the link, we you'd have to track from the destination instead.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Some URL shortener services doesn't allow you to change the link's destination unless you pay them money ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;like Patreon or GitHub Sponsors (the latter of which is not available in my country yet) ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>netlify</category>
      <category>github</category>
    </item>
    <item>
      <title>tmp.spacet.me devlog part 1</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Sun, 27 Dec 2020 15:04:45 +0000</pubDate>
      <link>https://dev.to/dtinth/tmp-spacet-me-devlog-part-1-3bnb</link>
      <guid>https://dev.to/dtinth/tmp-spacet-me-devlog-part-1-3bnb</guid>
      <description>&lt;p&gt;This is part of DigitalOcean App Platform Hackathon. The project I want to build is a web-based local file storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Nowadays, there are so many web-based tools that work with files, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://squoosh.app/"&gt;Squoosh&lt;/a&gt; image compressor&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.photopea.com/"&gt;Photopea&lt;/a&gt; photo editor&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gifcap.dev/"&gt;gifcap&lt;/a&gt; screen recorder&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.diagrams.net/"&gt;Diagrams.net&lt;/a&gt; diagram and flowchart maker&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vox.spacet.me/"&gt;vox.spacet.me&lt;/a&gt; voice recorder (I made this one)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://json.spacet.me/"&gt;json.spacet.me&lt;/a&gt; JSON viewer (I made this one too)&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.instant.io/"&gt;instant.io&lt;/a&gt; streaming file transfer over WebTorrent&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.sharedrop.io/"&gt;ShareDrop&lt;/a&gt; file transfer over WebRTC&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stackedit.io/"&gt;StackEdit&lt;/a&gt; and &lt;a href="https://dillinger.io/"&gt;Dillinger&lt;/a&gt; Markdown editor&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://carbon.now.sh/"&gt;Carbon&lt;/a&gt; code image generator&lt;/li&gt;
&lt;li&gt;other specialized tools that I create for specific situations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Now comes my pain point:&lt;/strong&gt; When I want to use multiple apps together, I have to go through a lot of Download-then-Upload experience, and thus my Downloads folder fills up with many temporary files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Many apps are inconsistent in how they handle input/output.&lt;/strong&gt; To get data into a web app, you can use the usual &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications"&gt;File open&lt;/a&gt; dialog (via &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt;), the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications#Selecting_files_using_drag_and_drop"&gt;Drag and Drop API&lt;/a&gt;, the &lt;a href="https://stackoverflow.com/questions/6333814/how-does-the-paste-image-from-clipboard-functionality-work-in-gmail-and-google-c"&gt;Clipboard API&lt;/a&gt;, and the &lt;a href="https://web.dev/web-share-target/"&gt;Share Target API&lt;/a&gt;. To get data out, there's &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;'s &lt;a href="https://www.w3schools.com/tags/att_a_download.asp"&gt;download&lt;/a&gt; attribute, &lt;a href="https://webplatform.news/issues/2017-07-26"&gt;DownloadURL DataTransfer type for dragging files out of the browser&lt;/a&gt;, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API"&gt;Clipboard API&lt;/a&gt;, the &lt;a href="https://web.dev/web-share/"&gt;Web Share API&lt;/a&gt;, and the classic "Save Link As", "Save Image As" and "Copy Image" menu items.&lt;/p&gt;

&lt;p&gt;Most apps support only a subset of these APIs. Some apps also have integrations with cloud storage services like Google Drive, but many don't. &lt;a href="https://paul.kinlan.me/unintended-silos/"&gt;Paul Kinlan (2017), Web sites as unintended silos: The problem with getting data in and out of the web client&lt;/a&gt; is an interesting read.&lt;/p&gt;

&lt;p&gt;Now the web has the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage"&gt;&lt;code&gt;postMessage&lt;/code&gt;&lt;/a&gt; API that lets web applications throw data around among different websites. So, I'm pondering about the idea of having a web-based portal, a hub of sort, that takes care of importing/exporting files using all the available Web APIs...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b9miHOyP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2020/12/18/tmp-core.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b9miHOyP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2020/12/18/tmp-core.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;...and integrates with all the different apps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8t7NDACk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2020/12/18/tmp-extension.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8t7NDACk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2020/12/18/tmp-extension.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It should be extensible.&lt;/strong&gt; Of course I'm lazy and I won't be integrating all the different apps myself. Instead, integrations will be provided through an extensions system. (In the above picture, the text in blue are to be implemented as an extension rather than as a core feature.)&lt;/p&gt;

&lt;p&gt;If all goes according to the plan, extensions will be HTML pages and communicate with this app through the &lt;code&gt;postMessage&lt;/code&gt; API. This way new extensions can be written using any tool, and added to the app without having to touch the main app. This is a nice opportunity to learn about "&lt;a href="https://martinfowler.com/articles/micro-frontends.html"&gt;micro-frontends&lt;/a&gt;."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment structure:&lt;/strong&gt; The main app will be deployed as a Static Site application, while extensions may be deployed as separate apps, and some of them might require a database. This is also an opportunity to try out App Platform services such as &lt;a href="https://www.digitalocean.com/products/managed-databases/"&gt;DigitalOcean Managed Databases&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project name:&lt;/strong&gt; This is one of the hardest thing in computer science. I intended it to be a place where I temporarily put files in. For the lack of better name, I just called it "tmp".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project setup:&lt;/strong&gt; I applied for a DigitalOcean credit, created a &lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt; static app, set up &lt;a href="https://tailwindcss.com/docs/guides/nextjs"&gt;Tailwind&lt;/a&gt;, &lt;a href="https://nextjs.org/docs/basic-features/typescript"&gt;TypeScript&lt;/a&gt; and &lt;a href="https://github.com/shadowwalker/next-pwa"&gt;next-pwa&lt;/a&gt;. I deployed the app to DigitalOcean App Platform and it just works (except the part where I waited for the spinner to finish. By the way everyone, please don't add a spinner if you will not also automatically refresh it.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--drROGCOQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://static.dt.in.th/uploads/2020/12/16/spin.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--drROGCOQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://static.dt.in.th/uploads/2020/12/16/spin.gif" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementating the core features:&lt;/strong&gt; I then implemented the basic import/export functionalities using various Browser APIs and npm packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API"&gt;Clipboard API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/web-share-target/"&gt;Web Share Target API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webplatform.news/issues/2017-07-26"&gt;DownloadURL for Drag and Drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/open"&gt;window.open()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/downloadjs"&gt;downloadjs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/browser-nativefs"&gt;Browser-NativeFS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;File data and all other data will be stored inside IndexedDB, through the help of &lt;a href="https://pouchdb.com/"&gt;PouchDB&lt;/a&gt;. Querying data from the local database in the React app is done using &lt;a href="https://react-query.tanstack.com/"&gt;React Query&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After a few days of coding, most of the core features are implemented.&lt;br&gt;
Now, for example, I can share photos from Google Photos directly to tmp.&lt;br&gt;
Within tmp, I can view the image (using the browser's built-in image viewer) and share it to other apps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ugGsJ1Qr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2020/12/18/tmp-sharing.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ugGsJ1Qr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://static.dt.in.th/uploads/2020/12/18/tmp-sharing.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right now the UI is very unpolished, and the rename functionality has not yet been implemented. But there are more important stuff to do first. The elephant in the room -- &lt;code&gt;postMessage&lt;/code&gt;-based integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source contributions:&lt;/strong&gt; Along the way I sent some quick PRs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/shadowwalker/next-pwa/pull/132"&gt;shadowwalker/next-pwa#132&lt;/a&gt; improves error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next up:&lt;/strong&gt; I will probably be implementing a first iteration of a &lt;code&gt;postMessage&lt;/code&gt;-based integration system. It will integrate with an external tool I have already created. Meanwhile, the source code is on GitHub at &lt;a href="https://github.com/dtinth/tmp"&gt;dtinth/tmp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dohackathon</category>
    </item>
    <item>
      <title>Test your frontend knowledge with this little fun quiz</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Fri, 20 Nov 2020 17:56:40 +0000</pubDate>
      <link>https://dev.to/dtinth/test-your-frontend-knowledge-with-this-little-fun-quiz-2684</link>
      <guid>https://dev.to/dtinth/test-your-frontend-knowledge-with-this-little-fun-quiz-2684</guid>
      <description>&lt;p&gt;In February 2019, I created this little quiz for a &lt;a href="http://codeinthedark.com/"&gt;Code in the Dark&lt;/a&gt; event&lt;sup id="fnref1"&gt;1&lt;/sup&gt; in Chiang Mai, Thailand. Now I’m making this quiz available online.&lt;/p&gt;

&lt;p&gt;This quiz tests for general knowledge of HTML and CSS, as well as topics related to frontend development such as colors, typography and animation. It does not include JavaScript though.&lt;/p&gt;

&lt;p&gt;It’s just for fun and is not indicative of skill (even skilled developers may get only half of them correct) but rather experience&lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;Here is it (&lt;a href="https://dt.in.th/citd3quiz.html"&gt;direct link to quiz&lt;/a&gt;):&lt;/p&gt;


&lt;div class="glitch-embed-wrap"&gt;
  &lt;iframe src="https://glitch.com/embed/#!/embed/citd3quiz?previewSize=100&amp;amp;path=index.html" alt="citd3quiz on glitch"&gt;&lt;/iframe&gt;
&lt;/div&gt;


&lt;p&gt;I created this quiz while I was preparing to run the 3rd Code in the Dark event in Thailand.&lt;/p&gt;

&lt;p&gt;I recalled that in the past events, many people would register as an observer (not a contestant), even though I see them as very good developers. When I asked why, some friends said that they are not confident enough to compete.&lt;/p&gt;

&lt;p&gt;So in the 3rd event I changed the registration process. Now everyone registered as a participant, and before each round, we’d run a quiz and offer the top scorers a chance to be a contestant in that round.&lt;/p&gt;

&lt;p&gt;This way, the truly skilled and experienced would get a better chance to compete, regardless of their self-confidence. That’s also why the quiz is separated into 3 rounds — people who did not make it in the first round are given 2 more chances.&lt;/p&gt;

&lt;p&gt;Anyways, hope you enjoyed it, and thanks for reading!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;It is a competition where contestants are asked to create a webpage according to the design within 15 minutes. Contestants must stay in text editor the whole time and no preview is allowed. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;I wouldn’t expect anyone to memorize all the little things. However, with experience, the chance of recalling obscure details increase. It’s like how one may have been doing web design and development for so long that at one point they realized they now posses the ability to mix RGB colors in their head. Anyways, this quiz would be a very bad way to assess someone’s web development skills. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>quiz</category>
      <category>frontend</category>
      <category>css</category>
      <category>watercooler</category>
    </item>
    <item>
      <title>Building a personal but multi-tenant web page screenshotting service with Puppeteer and Vercel</title>
      <dc:creator>Thai Pangsakulyanont</dc:creator>
      <pubDate>Tue, 03 Nov 2020 17:56:41 +0000</pubDate>
      <link>https://dev.to/dtinth/building-a-personal-but-multi-tenant-web-page-screenshotting-service-with-puppeteer-and-vercel-15b</link>
      <guid>https://dev.to/dtinth/building-a-personal-but-multi-tenant-web-page-screenshotting-service-with-puppeteer-and-vercel-15b</guid>
      <description>&lt;p&gt;In the past 2 years I found myself having to install and configure &lt;a href="https://github.com/puppeteer/puppeteer" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; to capture webpage screenshots on so many occasions.&lt;/p&gt;

&lt;p&gt;So I thought it would be great to have a generic API that captures webpage screenshots that I can reuse across multiple projects. The existence of serverless platforms like &lt;a href="https://vercel.com/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt; made it all the more easier to do this, even for personal projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The need for screenshotting web pages
&lt;/h2&gt;

&lt;p&gt;I regularly find need to programmatically take screenshots of web pages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/dtinth/timelapse" rel="noopener noreferrer"&gt;To capture a timelapse of my personal projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dtinth/html5-animation-video-renderer" rel="noopener noreferrer"&gt;To render web-based animations into a video file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dtinth/applitools-hackathon#canvas-chart-test" rel="noopener noreferrer"&gt;To perform visual regression testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/reactbkk/3.0.0-posters-nametags/blob/master/src/take-screenshot.js" rel="noopener noreferrer"&gt;To procedurally generate image assets in batch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In each of these use cases I installed &lt;a href="https://github.com/puppeteer/puppeteer" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; and wrote similar code: Go to the web page, take a screenshot, output the image. But Puppeteer is quite a heavy dependency, weighting over 100 MB in &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In cases where the code is used in dynamic sites, I would had to set up &lt;a href="https://rominirani.com/using-puppeteer-in-google-cloud-functions-809a14856e14" rel="noopener noreferrer"&gt;a Google Cloud function&lt;/a&gt; or &lt;a href="https://github.com/alixaxel/chrome-aws-lambda" rel="noopener noreferrer"&gt;an AWS Lambda function&lt;/a&gt; for that use cases.&lt;/p&gt;

&lt;p&gt;Wouldn’t it be great if, instead of setting up Puppeteer every time, there is an API that I can immediately use? Well, there is a &lt;a href="https://phantomjscloud.com/" rel="noopener noreferrer"&gt;plenty&lt;/a&gt; &lt;a href="https://www.browserless.io/" rel="noopener noreferrer"&gt;of&lt;/a&gt; &lt;a href="https://puppet-master.sh/" rel="noopener noreferrer"&gt;&lt;del&gt;existing&lt;/del&gt;&lt;/a&gt; &lt;a href="https://urlbox.io/" rel="noopener noreferrer"&gt;offerings&lt;/a&gt;, but most of them are paid and &lt;a href="https://github.com/GoogleChromeLabs/pptraas.com/issues/48" rel="noopener noreferrer"&gt;the free ones went down&lt;/a&gt;. Even some of the paid ones went down (the links that are struck out are now dead).&lt;/p&gt;

&lt;p&gt;Since I wanted to re-use it across multiple projects, spanning multiple years to come, I want some degree of control. The service should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run on my own domain name.&lt;/strong&gt; I don't want to have to go through all my projects and migrate to a new service, just because the old service is sunsetted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be affordable or free.&lt;/strong&gt; It’s for personal use and is non-commercial. I don’t want to spend too much money on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal yet multi-tenant.&lt;/strong&gt; I may use it on a project that is shared with others. In a rare case that I need to revoke an access to one project, it should not disrupt other projects. Therefore, it should support multiple keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure.&lt;/strong&gt; Others should not be able to use the service to screenshot arbitrary web pages without my permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-shot API.&lt;/strong&gt; Service consumers should be able to construct the image URL without having to make any extra API requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introducing personal-puppeteer
&lt;/h2&gt;

&lt;p&gt;So this is what I created. Here’s how it works:&lt;/p&gt;

&lt;p&gt;First, let’s say I want to generate a &lt;strong&gt;social card image&lt;/strong&gt; for the URL at &lt;code&gt;https://capture.the.spacet.me/&lt;/code&gt;. As an API consumer, I would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a request.&lt;/li&gt;
&lt;li&gt;Cryptographically-sign the request into a JWT and construct an image URL.&lt;/li&gt;
&lt;li&gt;Send the image URL to client (in a &lt;code&gt;&amp;lt;meta property="og:image"&amp;gt;&lt;/code&gt; tag).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-1.png" alt="Diagram for steps 1-3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser would then make a request to the service, which would, on request, take a screenshot&lt;/li&gt;
&lt;li&gt;…and return the image back to the browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-2.png" alt="Diagram for steps 4-5"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service maintains a list of tenants which are allowed to use the service. This allows the service to be reused in multiple projects without them having to share the same secret key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-tenants.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-tenants.png" alt="A list of tenants"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;I was able to quickly build the first version of this service thanks to Vercel’s &lt;a href="https://vercel.com/home" rel="noopener noreferrer"&gt;Edge Network&lt;/a&gt; and &lt;a href="https://vercel.com/docs/serverless-functions/introduction" rel="noopener noreferrer"&gt;Serverless Functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-3.png" alt="Diagram for the components inside the service"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first time a request is received:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It would enter Vercel’s network.&lt;/li&gt;
&lt;li&gt;Since this was the first time the request was processed, it would be a cache MISS.&lt;/li&gt;
&lt;li&gt;Vercel would then call the underlying serverless function.&lt;/li&gt;
&lt;li&gt;Which in turn validates the request and captures a screenshot of the webpage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-4.png" alt="Diagram for cache miss scenario (1)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The image is returned from the serverless function with a very aggressive &lt;a href="https://vercel.com/docs/serverless-functions/edge-caching#cache-control" rel="noopener noreferrer"&gt;caching&lt;/a&gt; header.&lt;/li&gt;
&lt;li&gt;Vercel would return the response and put it into its cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-5.png" alt="Diagram for cache miss scenario (2)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next time the request is received, it would be served by &lt;a href="https://vercel.com/docs/edge-network/caching" rel="noopener noreferrer"&gt;Vercel’s cache&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-6.png" alt="Diagram for cache miss scenario (3)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Use cases unlocked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Automatic social image generation for all my web projects
&lt;/h3&gt;

&lt;p&gt;If I create a web project or a blog post, but don’t want to spend the time crafting an &lt;code&gt;og:image&lt;/code&gt; for each page, I can just use &lt;code&gt;personal-puppeteer&lt;/code&gt; to generate a default &lt;code&gt;og:image&lt;/code&gt; from the webpage’s screenshot.&lt;/p&gt;

&lt;p&gt;Now when I share my article on Facebook, people would see the webpage’s screenshot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F11%2F04%2Fpersonal-puppeteer-cover.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F11%2F04%2Fpersonal-puppeteer-cover.png" alt="A Facebook post where I posted an article. The preview image is the screenshot of the article’s contents."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although it may not look as good as a handcrafted image, I think this is still way better than using a generic image or a profile picture as a default preview image.&lt;/p&gt;

&lt;p&gt;So far, I am doing this for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dt.in.th/" rel="noopener noreferrer"&gt;https://dt.in.th/&lt;/a&gt; — My personal website&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://notes.dt.in.th/" rel="noopener noreferrer"&gt;https://notes.dt.in.th/&lt;/a&gt; — This website where I publish notes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://capture.the.spacet.me/" rel="noopener noreferrer"&gt;https://capture.the.spacet.me/&lt;/a&gt; — &lt;code&gt;personal-puppeteer&lt;/code&gt;’s landing page&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sending procedurally generated images in chat rooms
&lt;/h3&gt;

&lt;p&gt;By using &lt;code&gt;data:&lt;/code&gt; URLs, an HTML page can be embedded in the JWT. &lt;br&gt;
This can be useful when I want to create a chat bot that can generate infographics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-discord.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-discord.gif" alt="GIF demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Adding web page screenshot to &lt;code&gt;README.md&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The URL can be embedded into other websites to provide an auto-updating screenshot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-readme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.dt.in.th%2Fuploads%2F2020%2F10%2F15%2Fpp-readme.png" alt="An image of the documentation website embedded in README"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Open source
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/dtinth/personal-puppeteer" rel="noopener noreferrer"&gt;This project is open source&lt;/a&gt;, so you can run your own instance too!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dtinth" rel="noopener noreferrer"&gt;
        dtinth
      &lt;/a&gt; / &lt;a href="https://github.com/dtinth/personal-puppeteer" rel="noopener noreferrer"&gt;
        personal-puppeteer
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A personal web page screenshotting service. Basically, it exposes an API that I can use to generate screenshot of any URL.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;personal-puppeteer&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;This is a &lt;strong&gt;personal web page screenshotting service&lt;/strong&gt;. Basically, it exposes an
API that I can use to generate screenshot of any URL
By &lt;em&gt;personal&lt;/em&gt; I mean that only I can use it. It is secured with JWT. But it is
open source, and you can run your own instance.&lt;/p&gt;
&lt;p&gt;It is &lt;strong&gt;deployed to &lt;a href="https://vercel.com/" rel="nofollow noopener noreferrer"&gt;Vercel&lt;/a&gt;&lt;/strong&gt; and is inspired by
&lt;a href="https://github.com/vercel/og-image" rel="noopener noreferrer"&gt;Vercel’s Open Graph Image as a Service&lt;/a&gt;
The main difference is that this service is designed to be reusable across many
use cases. It can capture arbitrary URLs and run arbitrary code.
It is multi-tenant. I can &lt;a href="https://github.com/dtinth/personal-puppeteer#adding-a-new-tenant" rel="noopener noreferrer"&gt;reuse this service&lt;/a&gt; without
having to share secrets.&lt;/p&gt;
&lt;p&gt;→ Read the &lt;a href="https://dev.to/dtinth/building-a-personal-but-multi-tenant-web-page-screenshotting-service-with-puppeteer-and-vercel-15b" rel="nofollow"&gt;project introduction post&lt;/a&gt; for more info.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dev.to/dtinth/building-a-personal-but-multi-tenant-web-page-screenshotting-service-with-puppeteer-and-vercel-15b" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/0142aa7f7652b5e2c4893c0e834d19a0a3005f90db6736414f3a7616ac34976b/68747470733a2f2f636170747572652e7468652e7370616365742e6d652f65794a68624763694f694a53557a49314e694973496e523563434936496b705856434a392e65794a31636d77694f694a6f64485277637a6f764c32526c646935306279396b64476c7564476776596e567062475270626d6374595331775a584a7a6232356862433169645851746258567364476b7464475675595735304c58646c596931775957646c4c584e6a636d566c626e4e6f623352306157356e4c584e6c636e5a705932557464326c3061433177645842775a58526c5a5849745957356b4c585a6c636d4e6c624330784e5749694c434a3361575230614349364d5451774d437769614756705a326830496a6f344d444173496d526c646d6c6a5a564e6a5957786c526d466a64473979496a6f784c434a7063334d694f694a6b64476c756447676966512e4877744b694d7448754764444b392d577a6a5031512d364e68743549536b59686f704349625a654a6d6a634d45756d7775463166764173556a785a6e497632466d506d30714431736e55327a4273735858324f6c4843766c6a3245316444497159334c35444d6d757355786f78636c7756796b67446572717770316e6f4e387268676d78796e71585a72575f39484e56316e63684666374d304c4352446366456a502d34716f6d57682d5057675251597a4b4e4658536c556938676f45756f71724d5379696a5948745649396e65315068506a524136566e73394153656a7347626b7853336e776631326362696f524d6558586579516b626d35397a704836584b54596a563971714f6a516569734e6652576576454e527947795248673850635a6f525051597042487a4552516b736d5544645246466a474359355a557a43794d6f30577062766751725a64554970667455625670672e706e67" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;See &lt;a href="https://capture.the.spacet.me/" rel="nofollow noopener noreferrer"&gt;the website&lt;/a&gt; for usage information.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://snapit.now.sh/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f1aefd768810c7bce854c52ae0ee325616cd9f75d1d2536754b17072d47871e2/68747470733a2f2f636170747572652e7468652e7370616365742e6d652f65794a68624763694f694a53557a49314e694973496e523563434936496b705856434a392e65794a31636d77694f694a6f64485277637a6f764c334e75595842706443357562336375633267764969776964326c6b644767694f6a677a4f437769614756705a326830496a6f324f444173496d526c646d6c6a5a564e6a5957786c526d466a64473979496a6f794c434a7063334d694f694a6b64476c756447676966512e53654e43445a313244685f515034394756697757413043694b5572663236387550446a574632423863387066476f4b5654415a706d3375394c796b643442503175636a626b7a555a3067336d5a567368314365696c56504835384f49374755496d5375695752637858684d4e445f46774f545172536638594b6d7432326b715a79726e386744735164443232762d5631486e674d314a337445396f544335577254337272486a503162666d6a457a48773555554a387942596e766a79576d506c6759554f41434341737a703567776f686e45344f47675047506a5254624e55686e33634779434670766f4f5935573756312d64426b56666368685a766a774f564f313050434b4f32633343494f61776d695165553631584f6e596c4d5576314a38426543534959362d3770577a6938687a674a5f7a77426f306e50705a67417252634f4b384f6f61664e6f59424f726e4e7464574b412e706e67" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Adding a new tenant&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Do not send pull requests to this repository to add a new tenant. Instead, please deploy &lt;code&gt;personal-puppeteer&lt;/code&gt; to your own account.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Generate…&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/dtinth/personal-puppeteer" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>puppeteer</category>
      <category>vercel</category>
    </item>
  </channel>
</rss>
